Skip to content

Commit

Permalink
[Kotlin-Spring] add Kotlin based Spring-cloud openfeign generator
Browse files Browse the repository at this point in the history
  • Loading branch information
gr4cza authored and David Gracza committed Apr 18, 2023
1 parent f5b4490 commit e249b6b
Show file tree
Hide file tree
Showing 34 changed files with 1,902 additions and 15 deletions.
1 change: 1 addition & 0 deletions .github/workflows/samples-kotlin-client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
- samples/client/petstore/kotlin-jvm-vertx-jackson
- samples/client/petstore/kotlin-jvm-vertx-jackson-coroutines
- samples/client/petstore/kotlin-jvm-vertx-moshi
- samples/client/petstore/kotlin-spring-cloud
steps:
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
Expand Down
12 changes: 12 additions & 0 deletions bin/configs/kotlin-spring-cloud.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
generatorName: kotlin-spring
outputDir: samples/client/petstore/kotlin-spring-cloud
library: spring-cloud
inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
templateDir: modules/openapi-generator/src/main/resources/kotlin-spring
additionalProperties:
documentationProvider: none
annotationLibrary: none
useSwaggerUI: "false"
serializableModel: "true"
beanValidations: "true"
interfaceOnly: "true"
4 changes: 3 additions & 1 deletion docs/generators/kotlin-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|artifactVersion|Generated artifact's package version.| |1.0.0|
|basePackage|base package (invokerPackage) for generated code| |org.openapitools|
|beanQualifiers|Whether to add fully-qualifier class names as bean qualifiers in @Component and @RestController annotations. May be used to prevent bean names clash if multiple generated libraries (contexts) added to single project.| |false|
|configPackage|configuration package for generated code| |org.openapitools.configuration|
|delegatePattern|Whether to generate the server files using the delegate pattern| |false|
|documentationProvider|Select the OpenAPI documentation provider.|<dl><dt>**none**</dt><dd>Do not publish an OpenAPI specification.</dd><dt>**source**</dt><dd>Publish the original input OpenAPI specification.</dd><dt>**springfox**</dt><dd>Generate an OpenAPI 2 (fka Swagger RESTful API Documentation Specification) specification using SpringFox 2.x. Deprecated (for removal); use springdoc instead.</dd><dt>**springdoc**</dt><dd>Generate an OpenAPI 3 specification using SpringDoc.</dd></dl>|springdoc|
|enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |camelCase|
|exceptionHandler|generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )| |true|
|gradleBuildFile|generate a gradle build file using the Kotlin DSL| |true|
|groupId|Generated artifact package's organization (i.e. maven groupId).| |org.openapitools|
|interfaceOnly|Whether to generate only API interface stubs without the server files.| |false|
|library|library template (sub-template)|<dl><dt>**spring-boot**</dt><dd>Spring-boot Server application.</dd></dl>|spring-boot|
|library|library template (sub-template)|<dl><dt>**spring-boot**</dt><dd>Spring-boot Server application.</dd><dt>**spring-cloud**</dt><dd>Spring-Cloud-Feign client with Spring-Boot auto-configured settings.</dd></dl>|spring-boot|
|modelMutable|Create mutable models| |false|
|modelPackage|model package for generated code| |org.openapitools.model|
|packageName|Generated artifact package name.| |org.openapitools|
Expand All @@ -50,6 +51,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|sourceFolder|source folder for generated code| |src/main/kotlin|
|title|server title name or client service name| |OpenAPI Kotlin Spring|
|useBeanValidation|Use BeanValidation API annotations to validate data types| |true|
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
|useSpringBoot3|Generate code and provide dependencies for use with Spring Boot 3.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.| |false|
|useSwaggerUI|Open the OpenApi specification in swagger-ui. Will also import and configure needed dependencies| |true|
|useTags|Whether to use tags for creating interface and controller class names| |false|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,24 @@
import com.samskivert.mustache.Template;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import org.openapitools.codegen.*;
import org.openapitools.codegen.CliOption;
import org.openapitools.codegen.CodegenConstants;
import org.openapitools.codegen.CodegenModel;
import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.CodegenParameter;
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.CodegenResponse;
import org.openapitools.codegen.CodegenType;
import org.openapitools.codegen.SupportingFile;
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures;
import org.openapitools.codegen.languages.features.SwaggerUIFeatures;
import org.openapitools.codegen.meta.features.*;
import org.openapitools.codegen.meta.features.DocumentationFeature;
import org.openapitools.codegen.meta.features.GlobalFeature;
import org.openapitools.codegen.meta.features.ParameterFeature;
import org.openapitools.codegen.meta.features.SchemaSupportFeature;
import org.openapitools.codegen.meta.features.SecurityFeature;
import org.openapitools.codegen.meta.features.WireFormatFeature;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationMap;
Expand All @@ -39,7 +52,14 @@
import java.io.IOException;
import java.io.Writer;
import java.net.URL;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;

import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
Expand All @@ -58,24 +78,31 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
"ApiResponse"
));

public static final String OPEN_BRACE = "{";
public static final String CLOSE_BRACE = "}";

public static final String TITLE = "title";
public static final String SERVER_PORT = "serverPort";
public static final String CONFIG_PACKAGE = "configPackage";
public static final String BASE_PACKAGE = "basePackage";
public static final String SPRING_BOOT = "spring-boot";
public static final String SPRING_CLOUD_LIBRARY = "spring-cloud";
public static final String EXCEPTION_HANDLER = "exceptionHandler";
public static final String GRADLE_BUILD_FILE = "gradleBuildFile";
public static final String SERVICE_INTERFACE = "serviceInterface";
public static final String SERVICE_IMPLEMENTATION = "serviceImplementation";
public static final String SKIP_DEFAULT_INTERFACE = "skipDefaultInterface";
public static final String REACTIVE = "reactive";
public static final String INTERFACE_ONLY = "interfaceOnly";
public static final String USE_FEIGN_CLIENT_URL = "useFeignClientUrl";
public static final String DELEGATE_PATTERN = "delegatePattern";
public static final String USE_TAGS = "useTags";
public static final String BEAN_QUALIFIERS = "beanQualifiers";

public static final String USE_SPRING_BOOT3 = "useSpringBoot3";

private String basePackage;
protected String configPackage;
private String invokerPackage;
private String serverPort = "8080";
private String title = "OpenAPI Kotlin Spring";
Expand All @@ -88,6 +115,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
private boolean serviceImplementation = false;
private boolean reactive = false;
private boolean interfaceOnly = false;
protected boolean useFeignClientUrl = true;
private boolean delegatePattern = false;
protected boolean useTags = false;
private boolean beanQualifiers = false;
Expand Down Expand Up @@ -128,12 +156,16 @@ public KotlinSpringServerCodegen() {

artifactId = "openapi-spring";
basePackage = invokerPackage = "org.openapitools";
configPackage = "org.openapitools.configuration";
apiPackage = "org.openapitools.api";
modelPackage = "org.openapitools.model";

// cliOptions default redefinition need to be updated
updateOption(CodegenConstants.ARTIFACT_ID, this.artifactId);

additionalProperties.put("openbrace", OPEN_BRACE);
additionalProperties.put("closebrace", CLOSE_BRACE);

// Use lists instead of arrays
typeMapping.put("array", "kotlin.collections.List");
typeMapping.put("list", "kotlin.collections.List");
Expand All @@ -142,6 +174,7 @@ public KotlinSpringServerCodegen() {
typeMapping.put("file", "org.springframework.core.io.Resource");

addOption(TITLE, "server title name or client service name", title);
addOption(CONFIG_PACKAGE, "configuration package for generated code", this.getConfigPackage());
addOption(BASE_PACKAGE, "base package (invokerPackage) for generated code", basePackage);
addOption(SERVER_PORT, "configuration the port in which the sever is to run on", serverPort);
addOption(CodegenConstants.MODEL_PACKAGE, "model package for generated code", modelPackage);
Expand All @@ -158,13 +191,16 @@ public KotlinSpringServerCodegen() {
addSwitch(SKIP_DEFAULT_INTERFACE, "Whether to skip generation of default implementations for interfaces", skipDefaultInterface);
addSwitch(REACTIVE, "use coroutines for reactive behavior", reactive);
addSwitch(INTERFACE_ONLY, "Whether to generate only API interface stubs without the server files.", interfaceOnly);
addSwitch(USE_FEIGN_CLIENT_URL, "Whether to generate Feign client with url parameter.", useFeignClientUrl);
addSwitch(DELEGATE_PATTERN, "Whether to generate the server files using the delegate pattern", delegatePattern);
addSwitch(USE_TAGS, "Whether to use tags for creating interface and controller class names", useTags);
addSwitch(BEAN_QUALIFIERS, "Whether to add fully-qualifier class names as bean qualifiers in @Component and " +
"@RestController annotations. May be used to prevent bean names clash if multiple generated libraries" +
" (contexts) added to single project.", beanQualifiers);
addSwitch(USE_SPRING_BOOT3, "Generate code and provide dependencies for use with Spring Boot 3.x. (Use jakarta instead of javax in imports). Enabling this option will also enable `useJakartaEe`.", useSpringBoot3);
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
"Spring-Cloud-Feign client with Spring-Boot auto-configured settings.");
setLibrary(SPRING_BOOT);

CliOption cliOpt = new CliOption(CodegenConstants.LIBRARY, CodegenConstants.LIBRARY_DESC);
Expand Down Expand Up @@ -243,6 +279,14 @@ private boolean selectedDocumentationProviderRequiresSwaggerUiBootstrap() {
getDocumentationProvider().equals(DocumentationProvider.SOURCE);
}

public void setConfigPackage(String configPackage) {
this.configPackage = configPackage;
}

public String getConfigPackage() {
return configPackage;
}

public String getBasePackage() {
return this.basePackage;
}
Expand Down Expand Up @@ -300,6 +344,10 @@ public void setServiceInterface(boolean serviceInterface) {
this.serviceInterface = serviceInterface;
}

public void setUseFeignClientUrl(boolean useFeignClientUrl) {
this.useFeignClientUrl = useFeignClientUrl;
}

public boolean getServiceImplementation() {
return this.serviceImplementation;
}
Expand Down Expand Up @@ -454,6 +502,12 @@ public void processOpts() {
LOGGER.info("Set base package to invoker package ({})", basePackage);
}

if (additionalProperties.containsKey(CONFIG_PACKAGE)) {
this.setConfigPackage((String) additionalProperties.get(CONFIG_PACKAGE));
} else {
additionalProperties.put(CONFIG_PACKAGE, configPackage);
}

if (additionalProperties.containsKey(BASE_PACKAGE)) {
this.setBasePackage((String) additionalProperties.get(BASE_PACKAGE));
} else {
Expand Down Expand Up @@ -504,10 +558,15 @@ public void processOpts() {
}
writePropertyBack(SKIP_DEFAULT_INTERFACE, skipDefaultInterface);

if (additionalProperties.containsKey(REACTIVE) && library.equals(SPRING_BOOT)) {
this.setReactive(convertPropertyToBoolean(REACTIVE));
// spring webflux doesn't support @ControllerAdvice
this.setExceptionHandler(false);
if (additionalProperties.containsKey(REACTIVE)) {
if (SPRING_CLOUD_LIBRARY.equals(library)) {
throw new IllegalArgumentException("Currently, reactive option doesn't supported by Spring Cloud");
}
if (library.equals(SPRING_BOOT)) {
this.setReactive(convertPropertyToBoolean(REACTIVE));
// spring webflux doesn't support @ControllerAdvice
this.setExceptionHandler(false);
}
}
writePropertyBack(REACTIVE, reactive);
writePropertyBack(EXCEPTION_HANDLER, exceptionHandler);
Expand All @@ -521,6 +580,15 @@ public void processOpts() {
this.setInterfaceOnly(Boolean.parseBoolean(additionalProperties.get(INTERFACE_ONLY).toString()));
}

if (library.equals(SPRING_CLOUD_LIBRARY)) {
this.setInterfaceOnly(true);
}

if (additionalProperties.containsKey(USE_FEIGN_CLIENT_URL)) {
this.setUseFeignClientUrl(Boolean.parseBoolean(additionalProperties.get(USE_FEIGN_CLIENT_URL).toString()));
}
writePropertyBack(USE_FEIGN_CLIENT_URL, useFeignClientUrl);

if (additionalProperties.containsKey(DELEGATE_PATTERN)) {
this.setDelegatePattern(Boolean.parseBoolean(additionalProperties.get(DELEGATE_PATTERN).toString()));
}
Expand Down Expand Up @@ -557,11 +625,6 @@ public void processOpts() {
apiTestTemplateFiles.put("api_test.mustache", ".kt");
}

if (SPRING_BOOT.equals(library)) {
supportingFiles.add(new SupportingFile("apiUtil.mustache",
(sourceFolder + File.separator + apiPackage).replace(".", java.io.File.separator), "ApiUtil.kt"));
}

if (this.serviceInterface) {
apiTemplateFiles.put("service.mustache", "Service.kt");
} else if (this.serviceImplementation) {
Expand All @@ -579,13 +642,17 @@ public void processOpts() {
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));


if (this.exceptionHandler) {
if (this.exceptionHandler && !library.equals(SPRING_CLOUD_LIBRARY)) {
supportingFiles.add(new SupportingFile("exceptions.mustache",
sanitizeDirectory(sourceFolder + File.separator + apiPackage), "Exceptions.kt"));
}

if (library.equals(SPRING_BOOT)) {
LOGGER.info("Setup code generator for Kotlin Spring Boot");

supportingFiles.add(new SupportingFile("apiUtil.mustache",
(sourceFolder + File.separator + apiPackage).replace(".", java.io.File.separator), "ApiUtil.kt"));

if (isUseSpringBoot3()) {
supportingFiles.add(new SupportingFile("pom-sb3.mustache", "", "pom.xml"));
} else {
Expand Down Expand Up @@ -621,7 +688,36 @@ public void processOpts() {
}
}

if (!reactive) {
if (library.equals(SPRING_CLOUD_LIBRARY)) {
LOGGER.info("Setup code generator for Kotlin Spring Cloud Client");

if (isUseSpringBoot3()) {
supportingFiles.add(new SupportingFile("pom-sb3.mustache", "pom.xml"));
} else {
supportingFiles.add(new SupportingFile("pom.mustache", "pom.xml"));
}

if (this.gradleBuildFile) {
if (isUseSpringBoot3()) {
supportingFiles.add(new SupportingFile("buildGradle-sb3-Kts.mustache", "build.gradle.kts"));
} else {
supportingFiles.add(new SupportingFile("buildGradleKts.mustache", "build.gradle.kts"));
}
supportingFiles.add(new SupportingFile("settingsGradle.mustache", "settings.gradle"));
}

supportingFiles.add(new SupportingFile("apiKeyRequestInterceptor.mustache",
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator),
"ApiKeyRequestInterceptor.kt"));
supportingFiles.add(new SupportingFile("clientConfiguration.mustache",
(sourceFolder + File.separator + configPackage).replace(".", java.io.File.separator),
"ClientConfiguration.kt"));
apiTemplateFiles.put("apiClient.mustache", "Client.kt");

apiTestTemplateFiles.clear();
}

if (!reactive && !library.equals(SPRING_CLOUD_LIBRARY)) {
if (DocumentationProvider.SPRINGFOX.equals(getDocumentationProvider())) {
supportingFiles.add(new SupportingFile("springfoxDocumentationConfig.mustache",
(sourceFolder + File.separator + basePackage).replace(".", java.io.File.separator),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{{^interfaceOnly}}
# {{artifactId}}

## Requirements

Building the API client library requires [Maven](https://maven.apache.org/) to be installed.

## Installation

To install the API client library to your local Maven repository, simply execute:

```shell
mvn install
```

To deploy it to a remote Maven repository instead, configure the settings of the repository and execute:

```shell
mvn deploy
```

Refer to the [official documentation](https://maven.apache.org/plugins/maven-deploy-plugin/usage.html) for more information.

### Maven users

Add this dependency to your project's POM:

```xml
<dependency>
<groupId>{{{groupId}}}</groupId>
<artifactId>{{{artifactId}}}</artifactId>
<version>{{{artifactVersion}}}</version>
<scope>compile</scope>
</dependency>
```

### Gradle users

Add this dependency to your project's build file:

```groovy
compile "{{{groupId}}}:{{{artifactId}}}:{{{artifactVersion}}}"
```

### Others

At first generate the JAR by executing:

mvn package

Then manually install the following JARs:

* target/{{{artifactId}}}-{{{artifactVersion}}}.jar
* target/lib/*.jar
{{/interfaceOnly}}
{{#interfaceOnly}}
# OpenAPI generated API stub

Spring Framework stub


## Overview
This code was generated by the [OpenAPI Generator](https://openapi-generator.tech) project.
By using the [OpenAPI-Spec](https://openapis.org), you can easily generate an API stub.
This is an example of building API stub interfaces in Java using the Spring framework.

The stubs generated can be used in your existing Spring-MVC or Spring-Boot application to create controller endpoints
by adding ```@Controller``` classes that implement the interface. Eg:
```java
@Controller
public class PetController implements PetApi {
// implement all PetApi methods
}
```

You can also use the interface to create [Spring-Cloud Feign clients](http://projects.spring.io/spring-cloud/spring-cloud.html#spring-cloud-feign-inheritance).Eg:
```java
@FeignClient(name="pet", url="http://petstore.swagger.io/v2")
public interface PetClient extends PetApi {
}
```
{{/interfaceOnly}}
Loading

0 comments on commit e249b6b

Please sign in to comment.