Skip to content

Commit

Permalink
feat(rest-client-from-swagger): Generate reactive http client from sw… (
Browse files Browse the repository at this point in the history
#409)

* feat(rest-client-from-swagger): Generate reactive http client from swagger file
  • Loading branch information
juancgalvis authored Dec 12, 2023
1 parent d088e47 commit 50d6717
Show file tree
Hide file tree
Showing 35 changed files with 242 additions and 57 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ The **`generateDrivenAdapter | gda`** task will generate a module in Infrastruct
| jpa | JPA Repository | --secret [true-false] | ☑ | ☑ |
| mongodb | Mongo Repository | --secret [true-false] | ☑ | ☑ |
| asynceventbus | Async Event Bus | | ☑ | ☑ |
| restconsumer | Rest Client Consumer | --url [url] | ☑ | ☑ |
| restconsumer | Rest Client Consumer | --url [url] --from-swagger swagger.yaml | ☑ | ☑ |
| redis | Redis | --mode [template-repository] --secret [true-false] | ☑ | ☑ |
| rsocket | RSocket Requester | | ☑ | ☑ |
| r2dbc | R2dbc Postgresql Client | | ☑ | ☑ |
Expand Down
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ dependencies {
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.1'
implementation 'org.reflections:reflections:0.10.2'

// swagger generators
implementation 'io.swagger.codegen.v3:swagger-codegen-generators:1.0.43'
implementation 'com.googlecode.lambdaj:lambdaj:2.3.3'
implementation 'com.google.googlejavaformat:google-java-format:1.18.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ public Object getParam(String key) {
}

public boolean getBooleanParam(String key) {
return (Boolean) params.get(key);
return (Boolean) params.getOrDefault(key, false);
}

public boolean isReactive() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import co.com.bancolombia.exceptions.CleanException;
import co.com.bancolombia.factory.ModuleBuilder;
import co.com.bancolombia.factory.ModuleFactory;
import co.com.bancolombia.utils.swagger.Swagger;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import org.gradle.api.logging.Logger;
Expand All @@ -24,6 +25,7 @@ public void buildModule(ModuleBuilder builder) throws IOException, CleanExceptio
builder.isKotlin(), "org.springframework.boot:spring-boot-starter-webflux");
builder.appendDependencyToModule(APP_SERVICE, implementation);
builder.appendToProperties("adapter.restconsumer").put("timeout", 5000);
Swagger.fromBuilder(builder, "infrastructure/driven-adapters/rest-consumer", false);
} else {
logger.lifecycle("Generating rest-consumer for imperative project");
builder.setupFromTemplate("driven-adapter/consumer-rest/rest-consumer");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ public void buildModule(ModuleBuilder builder) throws IOException, CleanExceptio
.appendToProperties("cors")
.put("allowed-origins", "http://localhost:4200,http://localhost:8080");
new EntryPointRestMvcServer().buildModule(builder);
Swagger.fromBuilder(builder, "infrastructure/entry-points/api-rest");
Swagger.fromBuilder(builder, "infrastructure/entry-points/api-rest", true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void buildModule(ModuleBuilder builder) throws IOException, CleanExceptio
}
}
}
Swagger.fromBuilder(builder, "infrastructure/entry-points/reactive-web");
Swagger.fromBuilder(builder, "infrastructure/entry-points/reactive-web", true);
builder.appendToSettings("reactive-web", "infrastructure/entry-points");
String dependency = buildImplementationFromProject(builder.isKotlin(), ":reactive-web");
builder.appendDependencyToModule(APP_SERVICE, dependency);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
description = "Generate driven adapter in infrastructure layer")
public class GenerateDrivenAdapterTask extends AbstractResolvableTypeTask {
private String url = "http://localhost:8080";
private String swaggerFile = null;
private DrivenAdapterRedis.Mode mode = DrivenAdapterRedis.Mode.TEMPLATE;
private DrivenAdapterBinStash.CacheMode cacheMode = DrivenAdapterBinStash.CacheMode.LOCAL;

Expand Down Expand Up @@ -56,13 +57,19 @@ public void setCacheMode(DrivenAdapterBinStash.CacheMode cacheMode) {
this.cacheMode = cacheMode;
}

@Option(option = "from-swagger", description = "Generation will be from a swagger.yaml file")
public void setFromSwagger(String swaggerFile) {
this.swaggerFile = swaggerFile;
}

@Override
protected void prepareParams() {
builder.addParam("task-param-cache-mode", cacheMode);
builder.addParam("include-secret", secret == BooleanOption.TRUE);
builder.addParam("eda", eda == BooleanOption.TRUE);
builder.addParam(DrivenAdapterRedis.PARAM_MODE, mode);
builder.addParam("task-param-url", url);
builder.addParam("swagger-file", swaggerFile);
}

@Override
Expand Down
87 changes: 53 additions & 34 deletions src/main/java/co/com/bancolombia/utils/swagger/Swagger.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,69 @@
import io.swagger.codegen.v3.ClientOptInput;
import io.swagger.codegen.v3.DefaultGenerator;
import io.swagger.codegen.v3.config.CodegenConfigurator;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import lombok.experimental.UtilityClass;
import org.gradle.api.logging.Logger;

@UtilityClass
public class Swagger {

public static void fromBuilder(ModuleBuilder builder, String outputDir) {
String swaggerFile = builder.getStringParam("swagger-file");
if (swaggerFile != null) {
String packageName = builder.getStringParam("package");
Map<String, Object> params =
Map.of(
"async",
builder.isReactive(),
"lombok",
builder.isEnableLombok(),
"router",
builder.isReactive() && builder.getBooleanParam("task-param-router"));
public static void fromBuilder(ModuleBuilder builder, String outputDir, boolean entryPoint) {
String swagger = builder.getStringParam("swagger-file");
if (swagger != null) {
Logger logger = builder.getProject().getLogger();
generateEntrypoint(
packageName, swaggerFile, builder.getProject().getRootDir() + "/" + outputDir, params)
String realOutputDir = builder.getProject().getRootDir() + "/" + outputDir;

String packageName = builder.getStringParam("package");
if (!entryPoint) {
packageName = packageName + ".consumer";
}

// Generator setup
CodegenConfigurator configurator = new CodegenConfigurator();

configurator.addAdditionalProperty("async", builder.isReactive());
configurator.addAdditionalProperty("lombok", builder.isEnableLombok());
configurator.addAdditionalProperty("invokerPackage", packageName + ".api");
configurator.addAdditionalProperty("fullController", true);
configurator.addAdditionalProperty("jakarta", true);
configurator.addAdditionalProperty("useBeanValidation", false);

configurator.setInputSpecURL(swagger);
configurator.setOutputDir(realOutputDir);
configurator.setApiPackage(packageName + ".api");
configurator.setModelPackage(packageName + ".api.model");

configurator.setLang(resolveLang(builder, entryPoint));

final ClientOptInput clientOptInput = configurator.toClientOptInput();
new DefaultGenerator()
.opts(clientOptInput)
.generate()
.forEach(file -> logger.lifecycle("File generated from swagger: {}", file));
try {
Files.deleteIfExists(Path.of(realOutputDir, ".swagger-codegen-ignore"));
Files.deleteIfExists(Path.of(realOutputDir, ".swagger-codegen", "VERSION"));
Files.deleteIfExists(Path.of(realOutputDir, ".swagger-codegen"));
} catch (IOException e) {
logger.info("file not found", e);
}
}
}

public static List<File> generateEntrypoint(
String packageName, String swagger, String outputDir, Map<String, Object> additionalProps) {
CodegenConfigurator configurator = new CodegenConfigurator();

additionalProps.forEach(configurator::addAdditionalProperty);

configurator.addAdditionalProperty("invokerPackage", packageName + ".api");
configurator.addAdditionalProperty("fullController", true);
configurator.addAdditionalProperty("jakarta", true);
configurator.addAdditionalProperty("useBeanValidation", false);
configurator.setInputSpecURL(swagger);
configurator.setOutputDir(outputDir);
configurator.setApiPackage(packageName + ".api");
configurator.setModelPackage(packageName + ".api.model");
configurator.setLang("io.swagger.codegen.v3.generators.WebFluxCodegen");
final ClientOptInput clientOptInput = configurator.toClientOptInput();
return new DefaultGenerator().opts(clientOptInput).generate();
private String resolveLang(ModuleBuilder builder, boolean entryPoint) {
if (entryPoint) {
if (builder.isReactive() && builder.getBooleanParam("task-param-router")) {
return "io.swagger.codegen.v3.generators.WebFluxRouterCodegen";
} else {
return "io.swagger.codegen.v3.generators.RestControllerCodegen";
}
} else if (builder.isReactive()) {
return "io.swagger.codegen.v3.generators.WebClientCodegen";
} else {
return "io.swagger.codegen.v3.generators.RestConsumerCodegen";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,22 @@
import java.util.regex.Matcher;
import java.util.stream.Collectors;

public class WebFluxCodegen extends AbstractJavaCodegen {
public abstract class AbstractScaffoldCodegen extends AbstractJavaCodegen {

public WebFluxCodegen() {
public AbstractScaffoldCodegen() {
super();
}

@Override
public String getDefaultTemplateDir() {
return "entry-point/rest-from-swagger";
return "commons/rest-from-swagger";
}

@Override
public CodegenType getTag() {
return CodegenType.SERVER;
}

@Override
public String getName() {
return "webflux";
}

@Override
public String getHelp() {
return "Generates an entrypoint rest with WebFlux.";
}

@Override
protected String getTemplateDir() {
return getDefaultTemplateDir();
Expand All @@ -47,13 +37,8 @@ public void processOpts() {
apiTestTemplateFiles.clear();
apiDocTemplateFiles.clear();
apiTemplateFiles.clear();

importMapping.put("OffsetDateTime", "java.time.OffsetDateTime");
if (((Boolean) additionalProperties.getOrDefault("router", false))) {
apiTemplateFiles.put("apiHandler.mustache", "Handler.java");
apiTemplateFiles.put("apiRouter.mustache", "Router.java");
} else {
apiTemplateFiles.put("apiController.mustache", "Controller.java");
}
modelDocTemplateFiles.clear();
additionalProperties.put(
"lambdaEscapeDoubleQuote",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.swagger.codegen.v3.generators;

import io.swagger.codegen.v3.CodegenType;

public class RestControllerCodegen extends AbstractScaffoldCodegen {

public RestControllerCodegen() {
super();
}

@Override
public CodegenType getTag() {
return CodegenType.SERVER;
}

@Override
public String getName() {
return "RestController";
}

@Override
public String getHelp() {
return "Generates a entry point rest with RestController.";
}

@Override
public void processOpts() {
super.processOpts();
apiTemplateFiles.put("apiController.mustache", "Controller.java");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.swagger.codegen.v3.generators;

public class WebClientCodegen extends AbstractScaffoldCodegen {

public WebClientCodegen() {
super();
}

@Override
public String getName() {
return "WebClient";
}

@Override
public String getHelp() {
return "Generates an entrypoint rest with WebClient.";
}

@Override
public void processOpts() {
super.processOpts();
apiTemplateFiles.put("apiClientReactive.mustache", ".java");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.swagger.codegen.v3.generators;

public class WebFluxRouterCodegen extends AbstractScaffoldCodegen {

public WebFluxRouterCodegen() {
super();
}

@Override
public String getName() {
return "WebFlux";
}

@Override
public String getHelp() {
return "Generates an entrypoint rest with WebFlux.";
}

@Override
public void processOpts() {
super.processOpts();
apiTemplateFiles.put("apiHandler.mustache", "Handler.java");
apiTemplateFiles.put("apiRouter.mustache", "Router.java");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package {{package}};

{{#imports}}import {{import}};
{{/imports}}

import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

{{^fullJavaUtil}}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
{{/fullJavaUtil}}
{{#lombok}}
import lombok.extern.log4j.Log4j2;
import lombok.AllArgsConstructor;
{{/lombok}}

{{#operations}}
{{#lombok}}
@Log4j2
@AllArgsConstructor
{{/lombok}}
@Service
public class {{classname}} {
private final WebClient client;
{{^lombok}}
private static final Logger log = LoggerFactory.getLogger({{classname}}Controller.class);

public {{classname}}(WebClient client) {
this.client = client;
}
{{/lombok}}

{{#operation}}
{{#contents}}
/**
* Build call for {{operationId}}{{#parameters}}
* @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{{.}}}{{/defaultValue}}){{/required}}{{/parameters}}
* @return Mono<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> response
{{#externalDocs}}
* {{description}}
* @see <a href="{{url}}">{{summary}} Documentation</a>
{{/externalDocs}}
*/
public Mono<{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{operationId}}Request({{#parameters}}{{{dataType}}} {{paramName}}{{#hasMore}}, {{/hasMore}}{{/parameters}}) {
return client.method(HttpMethod.{{httpMethod}})
.uri("{{{path}}}"{{#pathParams}}, {{{paramName}}}{{/pathParams}}){{#headerParams}}
.header("{{baseName}}", {{paramName}}){{/headerParams}}{{#consumes}}{{#@first}}
.contentType(MediaType.parseMediaType("{{{mediaType}}}")){{/@first}}{{/consumes}}{{^isForm}}{{#bodyParam}}
.body(BodyInserters.fromValue({{paramName}})){{/bodyParam}}{{/isForm}}{{#isForm}}{{#formParams}}
{{#@first}}.body(BodyInserters.fromFormData("{{baseName}}", {{paramName}}){{/@first}}{{^@first}} .with("{{baseName}}", {{paramName}}){{/@first}}{{#@last}}){{/@last}}{{/formParams}}{{/isForm}}
{{#produces}}{{#@first}}.accept(MediaType.parseMediaType("{{{mediaType}}}"))
{{/@first}}{{/produces}}.retrieve()
.bodyToMono({{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}Void{{/returnType}}.class);
}
{{/contents}}
{{/operation}}
}
{{/operations}}
Loading

0 comments on commit 50d6717

Please sign in to comment.