Skip to content

Commit

Permalink
Spring WebClient integration (#297)
Browse files Browse the repository at this point in the history
Co-authored-by: nsimonides <[email protected]>
Co-authored-by: Willem Veelenturf <[email protected]>
  • Loading branch information
3 people authored Dec 5, 2024
1 parent 197b3f8 commit 0803f3d
Show file tree
Hide file tree
Showing 30 changed files with 533 additions and 44 deletions.
4 changes: 4 additions & 0 deletions examples/maven-spring-integration/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package community.flock.wirespec.examples.maven.spring.integration;

import community.flock.wirespec.integration.spring.java.configuration.WirespecConfiguration;
import community.flock.wirespec.integration.spring.java.configuration.EnableWirespec;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import(WirespecConfiguration.class)
@EnableWirespec
public class TodoApplication {
public static void main(String[] args) {
SpringApplication.run(TodoApplication.class, args);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package community.flock.wirespec.examples.maven.spring.integration.client;

import community.flock.wirespec.generated.examples.spring.GetTodosEndpoint;
import community.flock.wirespec.integration.spring.java.client.WirespecWebClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.CompletableFuture;

@Component
public class TodoWebClient implements GetTodosEndpoint.Handler {

private final WirespecWebClient wirespecWebClient;

@Autowired
public TodoWebClient(WirespecWebClient wirespecWebClient) {
this.wirespecWebClient = wirespecWebClient;
}

@Override
public CompletableFuture<GetTodosEndpoint.Response<?>> getTodos(GetTodosEndpoint.Request request) {
return wirespecWebClient.send(request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package community.flock.wirespec.examples.maven.spring.integration.client;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WirespecClientConfig {

@Bean("wirespecSpringWebClient")
public WebClient webClient(
WebClient.Builder builder
) {
return builder.baseUrl("http://localhost:8080").build();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package community.flock.wirespec.examples.maven.spring.integration;
package community.flock.wirespec.examples.maven.spring.integration.controller;

import community.flock.wirespec.generated.examples.spring.CreateTodoEndpoint;
import community.flock.wirespec.generated.examples.spring.DeleteTodoEndpoint;
Expand All @@ -7,6 +7,7 @@
import community.flock.wirespec.generated.examples.spring.Todo;
import community.flock.wirespec.generated.examples.spring.UpdateTodoEndpoint;
import org.springframework.web.bind.annotation.RestController;
import community.flock.wirespec.examples.maven.spring.integration.service.TodoService;

import java.util.UUID;
import java.util.concurrent.CompletableFuture;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package community.flock.wirespec.examples.maven.spring.integration;
package community.flock.wirespec.examples.maven.spring.integration.service;

import community.flock.wirespec.generated.examples.spring.Todo;
import org.springframework.stereotype.Service;
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ maven_plugin_api = "3.9.8"
maven_plugin_annotations = "3.13.1"
maven_project = "2.2.1"
spring_boot = "3.3.4"
spring_webflux = "6.1.13"
spring_dependency_management = "1.1.6"

[libraries]
Expand Down Expand Up @@ -51,6 +52,7 @@ maven_plugin_api = { module = "org.apache.maven:maven-plugin-api", version.ref =
maven_plugin_annotations = { module = "org.apache.maven.plugin-tools:maven-plugin-annotations", version.ref = "maven_plugin_annotations" }
maven_project = { module = "org.apache.maven:maven-project", version.ref = "maven_project" }
spring_boot_web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring_boot" }
spring_webflux = { module = "org.springframework:spring-webflux", version.ref = "spring_webflux" }
spring_boot_test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring_boot" }

[bundles]
Expand Down
136 changes: 115 additions & 21 deletions src/integration/spring/README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,66 @@
# Spring integration lib
# Wirespec Spring Integration

This module offers Spring configuration which binds Wirespec endpoints as request mappings.
A Spring integration library that enables:
- Spring RestController endpoint generation
- Spring WebClient integration

## Install
## Installation

Add the Maven plugin to your `pom.xml`:

```xml
<dependency>
<groupId>community.flock.wirespec.integration</groupId>
<artifactId>spring-jvm</artifactId>
<plugin>
<groupId>community.flock.wirespec.plugin.maven</groupId>
<artifactId>wirespec-maven-plugin</artifactId>
<version>{VERSION}</version>
</dependency>
<executions>
<execution>
<phase>generate-sources</phase>
<id>java</id>
<goals>
<goal>custom</goal>
</goals>
<configuration>
<input>${maven.multiModuleProjectDirectory}/wirespec</input>
<output>${project.build.directory}/generated-sources</output>
<packageName>generated.community.flock.wirespec.api</packageName>
<emitterClass>community.flock.wirespec.integration.spring.kotlin.emit.SpringJavaEmitter</emitterClass>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>community.flock.wirespec.integration</groupId>
<artifactId>spring-jvm</artifactId>
<version>{VERSION}</version>
</dependency>
</dependencies>
</plugin>
```

## Usage
Use the custom Java or Kotlin spring emitters to generate wirespec spring enabled endpoint interfaces
- [SpringJavaEmitter.kt](src%2FjvmMain%2Fkotlin%2Fcommunity%2Fflock%2Fwirespec%2Fintegration%2Fspring%2Femit%2FSpringJavaEmitter.kt)
- [SpringKotlinEmitter.kt](src%2FjvmMain%2Fkotlin%2Fcommunity%2Fflock%2Fwirespec%2Fintegration%2Fspring%2Femit%2FSpringKotlinEmitter.kt)
Choose your emitter:
- [SpringJavaEmitter.kt](src/jvmMain/kotlin/community/flock/wirespec/integration/spring/java/emit/SpringJavaEmitter.kt)
- [SpringKotlinEmitter.kt](src/jvmMain/kotlin/community/flock/wirespec/integration/spring/kotlin/emit/SpringKotlinEmitter.kt)

## RestController Integration

Load the wirspec spring configuration
- [WirespecConfiguration.kt](src%2FjvmMain%2Fkotlin%2Fcommunity%2Fflock%2Fwirespec%2Fintegration%2Fspring%2Fconfiguration%2FWirespecConfiguration.kt)
1. Enable the controller in your Spring Boot application:

```java
@SpringBootApplication
@Import(WirespecConfiguration.class)
public class TodoApplication {
@EnableWirespecController
public class Application {
public static void main(String[] args) {
SpringApplication.run(TodoApplication.class, args);
SpringApplication.run(Application.class, args);
}
}

```

2. Implement the generated endpoint interface:

```java
@RestController
class TodoController implements GetTodosEndpoint {

private final TodoService service;

public TodoController(TodoService service) {
Expand All @@ -52,10 +78,78 @@ class TodoController implements GetTodosEndpoint {
todoInput.done()
);
service.create(todo);
var res = new CreateTodoEndpoint.Response200ApplicationJson(Map.of(), todo);
return CompletableFuture.completedFuture(res);
return CompletableFuture.completedFuture(
new CreateTodoEndpoint.Response200ApplicationJson(Map.of(), todo)
);
}
}
```

For a more extensive example go to [Spring integration example](examples/spring-boot-integration)
## WebClient Integration

1. Enable the client in your Spring Boot application:

```java
@SpringBootApplication
@EnableWirespecClient
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```

2. Configure the WebClient:

```java
@Configuration
public class WirespecClientConfig {

@Bean("wirespecSpringWebClient")
public WebClient webClient(
WebClient.Builder builder
) {
return builder.baseUrl("http://localhost:8080").build();
}
}

```

3. Use the WirespecWebClient in your API client:

```java
@Component
public class TodoWebClient implements GetTodosEndpoint.Handler {

private final WirespecWebClient wirespecWebClient;

@Autowired
public TodoWebClient(WirespecWebClient wirespecWebClient) {
this.wirespecWebClient = wirespecWebClient;
}

@Override
public CompletableFuture<GetTodosEndpoint.Response<?>> getTodos(GetTodosEndpoint.Request request) {
return wirespecWebClient.send(request);
}
}
```

## Both Controller and WebClient support

Use the following annotation to enable both Wirespec Controller and WebClient support:

```java
@SpringBootApplication
@EnableWirespec
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
```

For more examples, see the:

- [WebClientIntegrationTest](src/jvmTest/kotlin/community/flock/wirespec/integration/spring/kotlin/it/client/WebClientIntegrationTest.kt)
- [RestControllerIntegrationTest](src/jvmTest/kotlin/community/flock/wirespec/integration/spring/kotlin/it/controller/RestControllerIntegrationTest.kt)
1 change: 1 addition & 0 deletions src/integration/spring/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ kotlin {
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.reactor)
implementation(libs.spring.boot.web)
implementation(libs.spring.webflux)
runtimeOnly(libs.junit.launcher)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package community.flock.wirespec.integration.spring.java.client

import community.flock.wirespec.java.Wirespec
import community.flock.wirespec.java.Wirespec.Serialization
import org.springframework.http.HttpMethod
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientResponseException
import reactor.core.publisher.Mono
import java.util.concurrent.CompletableFuture
import kotlin.reflect.full.companionObjectInstance

class WirespecWebClient(
private val client: WebClient,
private val wirespecSerde: Serialization<String>,
) {
fun <Req : Wirespec.Request<*>, Res : Wirespec.Response<*>> send(
request: Req,
): CompletableFuture<Res> {
val declaringClass= request::class.java.declaringClass
val handler = declaringClass.declaredClasses.toList()
.find { it.simpleName == "Handler" }
?: error("Handler not found")

val instance = handler.kotlin.companionObjectInstance as Wirespec.Client<Req, Res>

return with(instance.getClient(wirespecSerde)) {
executeRequest(to(request), client).thenApply {
from(it)
}
}
}

private fun executeRequest(
request: Wirespec.RawRequest,
client: WebClient,
): CompletableFuture<Wirespec.RawResponse> = client
.method(HttpMethod.valueOf(request.method))
.uri { uriBuilder ->
uriBuilder
.path(request.path.joinToString("/"))
.apply { request.queries.forEach { (key, value) -> queryParam(key, value) } }
.build()
}
.headers { headers ->
request.headers.forEach { (key, value) -> headers.add(key, value) }
}
.bodyValue(request.body)
.exchangeToMono { response ->
response.bodyToMono(String::class.java)
.map { body ->
Wirespec.RawResponse(
response.statusCode().value(),
response.headers().asHttpHeaders().toSingleValueMap(),
body
)
}
}
.onErrorResume { throwable ->
when (throwable) {
is WebClientResponseException ->
Wirespec.RawResponse(
throwable.statusCode.value(),
throwable.headers.toSingleValueMap(),
throwable.responseBodyAsString
).let { Mono.just(it) }

else -> Mono.error(throwable)
}
}.toFuture()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package community.flock.wirespec.integration.spring.java.configuration

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@EnableWirespecController
@EnableWirespecWebClient
annotation class EnableWirespec
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package community.flock.wirespec.integration.spring.java.configuration

import org.springframework.context.annotation.Import

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Import(WirespecSerializationConfiguration::class)
annotation class EnableWirespecController
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package community.flock.wirespec.integration.spring.java.configuration

import org.springframework.context.annotation.Import

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Import(WirespecSerializationConfiguration::class, WirespecWebClientConfiguration::class)
annotation class EnableWirespecWebClient
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import org.springframework.context.annotation.Import

@Configuration
@Import(WirespecResponseBodyAdvice::class, WirespecWebMvcConfiguration::class)
open class WirespecConfiguration {
open class WirespecSerializationConfiguration {

@Bean
open fun wirespecSerialization(objectMapper: ObjectMapper) = object : Wirespec.Serialization<String> {
Expand Down
Loading

0 comments on commit 0803f3d

Please sign in to comment.