Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[kotlin-spring] add reactive behavior via Kotlin coroutines #2934

Merged
merged 16 commits into from
Jun 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions bin/kotlin-springboot-petstore-all.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh

./bin/kotlin-springboot-petstore-server.sh
./bin/kotlin-springboot-petstore-server-reactive.sh
35 changes: 35 additions & 0 deletions bin/kotlin-springboot-petstore-server-reactive.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/sh

SCRIPT="$0"
echo "# START SCRIPT: $SCRIPT"

while [ -h "$SCRIPT" ] ; do
ls=$(ls -ld "$SCRIPT")
link=$(expr "$ls" : '.*-> \(.*\)$')
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=$(dirname "$SCRIPT")/"$link"
fi
done

if [ ! -d "${APP_DIR}" ]; then
APP_DIR=$(dirname "$SCRIPT")/..
APP_DIR=$(cd "${APP_DIR}"; pwd)
fi

executable="./modules/openapi-generator-cli/target/openapi-generator-cli.jar"

if [ ! -f "$executable" ]
then
mvn clean package
fi

export JAVA_OPTS="${JAVA_OPTS} -Xmx1024M -DloggerPath=conf/log4j.properties"
ags="$@ generate -i modules/openapi-generator/src/test/resources/2_0/petstore.yaml -t modules/openapi-generator/src/main/resources/kotlin-spring -g kotlin-spring -o samples/server/petstore/kotlin-springboot-reactive --additional-properties=library=spring-boot,beanValidations=true,swaggerAnnotations=true,serviceImplementation=true,reactive=true"

echo "Cleaning previously generated files if any from samples/server/petstore/kotlin-springboot-reactive"
rm -rf samples/server/petstore/kotlin-springboot-reactive

echo "Generating Kotling Spring Boot reactive server..."
java $JAVA_OPTS -jar $executable $ags
4 changes: 4 additions & 0 deletions bin/openapi3/kotlin-springboot-petstore-all.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh

./bin/openapi3/kotlin-springboot-petstore-server.sh
./bin/openapi3/kotlin-springboot-petstore-server-reactive.sh
35 changes: 35 additions & 0 deletions bin/openapi3/kotlin-springboot-petstore-server-reactive.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/sh

SCRIPT="$0"
echo "# START SCRIPT: $SCRIPT"

while [ -h "$SCRIPT" ] ; do
ls=$(ls -ld "$SCRIPT")
link=$(expr "$ls" : '.*-> \(.*\)$')
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=$(dirname "$SCRIPT")/"$link"
fi
done

if [ ! -d "${APP_DIR}" ]; then
APP_DIR=$(dirname "$SCRIPT")/..
APP_DIR=$(cd "${APP_DIR}"; pwd)
fi

executable="./modules/openapi-generator-cli/target/openapi-generator-cli.jar"

if [ ! -f "$executable" ]
then
mvn clean package
fi

export JAVA_OPTS="${JAVA_OPTS} -Xmx1024M -DloggerPath=conf/log4j.properties"
ags="$@ generate -i modules/openapi-generator/src/test/resources/3_0/petstore.yaml -t modules/openapi-generator/src/main/resources/kotlin-spring -g kotlin-spring -o samples/server/openapi3/petstore/kotlin-springboot-reactive --additional-properties=library=spring-boot,beanValidations=true,swaggerAnnotations=true,serviceImplementation=true,reactive=true"

echo "Cleaning previously generated files if any from samples/server/openapi3/petstore/kotlin-springboot-reactive"
rm -rf samples/server/openapi3/petstore/kotlin-springboot-reactive

echo "Generating Kotling Spring Boot server..."
java $JAVA_OPTS -jar $executable $ags
3 changes: 2 additions & 1 deletion docs/generators/kotlin-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ sidebar_label: kotlin-spring
|serverPort|configuration the port in which the sever is to run on| |8080|
|modelPackage|model package for generated code| |org.openapitools.model|
|apiPackage|api package for generated code| |org.openapitools.api|
|exceptionHandler|generate default global exception handlers| |true|
|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|
|swaggerAnnotations|generate swagger annotations to go alongside controllers and models| |false|
|serviceInterface|generate service interfaces to go alongside controllers. In most cases this option would be used to update an existing project, so not to override implementations. Useful to help facilitate the generation gap pattern| |false|
|serviceImplementation|generate stub service implementations that extends service interfaces. If this is set to true service interfaces will also be generated| |false|
|useBeanValidation|Use BeanValidation API annotations to validate data types| |true|
|reactive|use coroutines for reactive behavior| |false|
|library|library template (sub-template)|<dl><dt>**spring-boot**</dt><dd>Spring-boot Server application.</dd><dl>|spring-boot|
Original file line number Diff line number Diff line change
Expand Up @@ -4698,7 +4698,7 @@ public CodegenParameter fromRequestBody(RequestBody body, Set<String> imports, S
codegenParameter.items = codegenProperty.items;
codegenParameter.mostInnerItems = codegenProperty.mostInnerItems;
codegenParameter.dataType = getTypeDeclaration(arraySchema);
codegenParameter.baseType = getSchemaType(arraySchema);
codegenParameter.baseType = getSchemaType(inner);
codegenParameter.isContainer = Boolean.TRUE;
codegenParameter.isListContainer = Boolean.TRUE;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
public static final String SWAGGER_ANNOTATIONS = "swaggerAnnotations";
public static final String SERVICE_INTERFACE = "serviceInterface";
public static final String SERVICE_IMPLEMENTATION = "serviceImplementation";
public static final String REACTIVE = "reactive";


private String basePackage;
private String invokerPackage;
Expand All @@ -72,6 +74,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
private boolean swaggerAnnotations = false;
private boolean serviceInterface = false;
private boolean serviceImplementation = false;
private boolean reactive = false;

public KotlinSpringServerCodegen() {
super();
Expand Down Expand Up @@ -139,7 +142,7 @@ public KotlinSpringServerCodegen() {
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);
addOption(CodegenConstants.API_PACKAGE, "api package for generated code", apiPackage);
addSwitch(EXCEPTION_HANDLER, "generate default global exception handlers", exceptionHandler);
addSwitch(EXCEPTION_HANDLER, "generate default global exception handlers (not compatible with reactive. enabling reactive will disable exceptionHandler )", exceptionHandler);
addSwitch(GRADLE_BUILD_FILE, "generate a gradle build file using the Kotlin DSL", gradleBuildFile);
addSwitch(SWAGGER_ANNOTATIONS, "generate swagger annotations to go alongside controllers and models", swaggerAnnotations);
addSwitch(SERVICE_INTERFACE, "generate service interfaces to go alongside controllers. In most " +
Expand All @@ -148,7 +151,7 @@ public KotlinSpringServerCodegen() {
addSwitch(SERVICE_IMPLEMENTATION, "generate stub service implementations that extends service " +
"interfaces. If this is set to true service interfaces will also be generated", serviceImplementation);
addSwitch(USE_BEANVALIDATION, "Use BeanValidation API annotations to validate data types", useBeanValidation);

addSwitch(REACTIVE, "use coroutines for reactive behavior", reactive);
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
setLibrary(SPRING_BOOT);

Expand Down Expand Up @@ -239,6 +242,14 @@ public void setUseBeanValidation(boolean useBeanValidation) {
this.useBeanValidation = useBeanValidation;
}

public boolean isReactive() {
return reactive;
}

public void setReactive(boolean reactive) {
this.reactive = reactive;
}

@Override
public CodegenType getTag() {
return CodegenType.SERVER;
Expand Down Expand Up @@ -331,6 +342,14 @@ public void processOpts() {
}
writePropertyBack(USE_BEANVALIDATION, useBeanValidation);

if (additionalProperties.containsKey(REACTIVE) && library.equals(SPRING_BOOT)) {
this.setReactive(convertPropertyToBoolean(REACTIVE));
// spring webflux doesn't support @ControllerAdvice
this.setExceptionHandler(false);
}
writePropertyBack(REACTIVE, reactive);
writePropertyBack(EXCEPTION_HANDLER, exceptionHandler);

modelTemplateFiles.put("model.mustache", ".kt");
apiTemplateFiles.put("api.mustache", ".kt");
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ import io.swagger.annotations.AuthorizationScope
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller

import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
{{#useBeanValidation}}
import org.springframework.validation.annotation.Validated
{{/useBeanValidation}}
Expand All @@ -39,10 +40,13 @@ import javax.validation.constraints.Pattern
import javax.validation.constraints.Size
{{/useBeanValidation}}

{{#reactive}}
import kotlinx.coroutines.flow.Flow;
{{/reactive}}
import kotlin.collections.List
import kotlin.collections.Map

@Controller
@RestController
{{#useBeanValidation}}
@Validated
{{/useBeanValidation}}
Expand All @@ -68,12 +72,12 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v
value = [{{#responses}}ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{baseType}}}::class{{/baseType}}{{#containerType}}, responseContainer = "{{{containerType}}}"{{/containerType}}){{#hasMore}},{{/hasMore}}{{/responses}}]){{/swaggerAnnotations}}
@RequestMapping(
value = ["{{#lambda.escapeDoubleQuote}}{{path}}{{/lambda.escapeDoubleQuote}}"],{{#singleContentTypes}}{{#hasProduces}}
produces = "{{{vendorExtensions.x-accepts}}}", {{/hasProduces}}{{#hasConsumes}}
produces = "{{{vendorExtensions.x-accepts}}}",{{/hasProduces}}{{#hasConsumes}}
consumes = "{{{vendorExtensions.x-contentType}}}",{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}}
produces = [{{#produces}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/produces}}], {{/hasProduces}}{{#hasConsumes}}
consumes = [{{#consumes}}"{{{mediaType}}}"{{#hasMore}}, {{/hasMore}}{{/consumes}}],{{/hasConsumes}}{{/singleContentTypes}}
method = [RequestMethod.{{httpMethod}}])
fun {{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}},{{/hasMore}}{{/allParams}}): ResponseEntity<{{>returnTypes}}> {
{{#reactive}}{{^isListContainer}}suspend {{/isListContainer}}{{/reactive}}fun {{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{#hasMore}},{{/hasMore}}{{/allParams}}): ResponseEntity<{{>returnTypes}}> {
Copy link

@elijah-pl elijah-pl Feb 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sylvainmoindron @Zomzog Could you please help to understand how to generate a controller API with suspend functions that return ResponseEntity<T> ResponseEntity<List<T>>. Because my generated code contains non-suspended functions with ResponseEntity<Flow<T>> return type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does your response is a single item or a list ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sylvainmoindron A list

paths:
  /something:
    get:
      operationId: getSomething
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Something'

Generated method looks like

fun getSomething(): ResponseEntity<Flow<Something>>

And I want it to be

suspend fun getSomething(): ResponseEntity<List<Something>>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if your result is a list it's nomal that the method return a flow. flow are member of coroutine.
in your flow you can call suspend method or methos the return a flow.
when you define a flow with flow{ action here }, your action are in a coroutineScope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you want single List you can define your return as an object SometingList with contain your list

return {{>returnValue}}
}
{{/operation}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isBodyParam}}{{#swaggerAnnotations}}@ApiParam(value = "{{{description}}}" {{#required}},required=true{{/required}} {{^isContainer}}{{#allowableValues}}, allowableValues="{{{allowableValues}}}"{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}){{/swaggerAnnotations}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestBody {{paramName}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isListContainer}}Mono{{/isListContainer}}{{#isListContainer}}Flux{{/isListContainer}}<{{{baseType}}}>{{/reactive}}{{/isBodyParam}}
{{#isBodyParam}}{{#swaggerAnnotations}}@ApiParam(value = "{{{description}}}" {{#required}},required=true{{/required}} {{^isContainer}}{{#allowableValues}}, allowableValues="{{{allowableValues}}}"{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}){{/swaggerAnnotations}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestBody {{paramName}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isListContainer}}{{>optionalDataType}}{{/isListContainer}}{{#isListContainer}}Flow<{{{baseType}}}>{{/isListContainer}}{{/reactive}}{{/isBodyParam}}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.3.RELEASE")
classpath("org.springframework.boot:spring-boot-gradle-plugin:2.2.0.M3")
}
}

Expand All @@ -23,18 +23,26 @@ tasks.withType<KotlinCompile> {
}

plugins {
val kotlinVersion = "1.2.60"
val kotlinVersion = "1.3.30"
id("org.jetbrains.kotlin.jvm") version kotlinVersion
id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion
id("org.springframework.boot") version "2.0.3.RELEASE"
id("org.springframework.boot") version "2.2.0.M3"
id("io.spring.dependency-management") version "1.0.5.RELEASE"
}

dependencies {
val kotlinxCoroutinesVersion="1.2.0"
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compile("org.jetbrains.kotlin:kotlin-reflect")
{{^reactive}}
compile("org.springframework.boot:spring-boot-starter-web")
{{/reactive}}
{{#reactive}}
compile("org.springframework.boot:spring-boot-starter-webflux")
compile("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion")
compile("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion")
{{/reactive}}
{{#swaggerAnnotations}}
compile("io.swagger:swagger-annotations:1.5.21")
{{/swaggerAnnotations}}
Expand All @@ -46,3 +54,9 @@ dependencies {
exclude(module = "junit")
}
}

repositories {
mavenCentral()
maven { url = uri("https://repo.spring.io/snapshot") }
maven { url = uri("https://repo.spring.io/milestone") }
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
<name>{{artifactId}}</name>
<version>{{artifactVersion}}</version>
<properties>
<kotlin.version>1.2.60</kotlin.version>
<kotlin.version>1.3.30</kotlin.version>
<kotlinx-coroutines.version>1.2.0</kotlinx-coroutines.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<version>2.2.0.M3</version>
</parent>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
Expand Down Expand Up @@ -77,8 +78,26 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
{{^reactive}}
<artifactId>spring-boot-starter-web</artifactId>
{{/reactive}}
{{#reactive}}
<artifactId>spring-boot-starter-webflux</artifactId>
{{/reactive}}
</dependency>
{{#reactive}}
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-core</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<version>${kotlinx-coroutines.version}</version>
</dependency>
{{/reactive}}

{{#swaggerAnnotations}}
<dependency>
<groupId>io.swagger</groupId>
Expand Down Expand Up @@ -110,4 +129,34 @@
</dependency>
{{/useBeanValidation}}
</dependencies>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
</project>
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
pluginManagement {
repositories {
maven { url = uri("https://repo.spring.io/snapshot") }
maven { url = uri("https://repo.spring.io/milestone") }
gradlePluginPortal()
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "org.springframework.boot") {
useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}")
}
}
}
}
rootProject.name = "{{artifactId}}"
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isMapContainer}}{{#reactive}}Mono<{{/reactive}}Map<String, {{{returnType}}}{{#reactive}}>{{/reactive}}>{{/isMapContainer}}{{#isListContainer}}{{#reactive}}Flux{{/reactive}}{{^reactive}}List{{/reactive}}<{{{returnType}}}>{{/isListContainer}}{{^returnContainer}}{{#reactive}}Mono<{{{returnType}}}>{{/reactive}}{{^reactive}}{{{returnType}}}{{/reactive}}{{/returnContainer}}
{{#isMapContainer}}Map<String, {{{returnType}}}>{{/isMapContainer}}{{#isListContainer}}{{#reactive}}Flow{{/reactive}}{{^reactive}}List{{/reactive}}<{{{returnType}}}>{{/isListContainer}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package {{package}}

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

{{#reactive}}
import kotlinx.coroutines.flow.Flow;
{{/reactive}}
{{#operations}}
interface {{classname}}Service {
{{#operation}}

fun {{operationId}}({{#allParams}}{{paramName}}: {{>optionalDataType}}{{#hasMore}}, {{/hasMore}}{{/allParams}}): {{>returnTypes}}
{{#reactive}}{{^isListContainer}}suspend {{/isListContainer}}{{/reactive}}fun {{operationId}}({{#allParams}}{{paramName}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isListContainer}}{{>optionalDataType}}{{/isListContainer}}{{#isListContainer}}Flow<{{{baseType}}}>{{/isListContainer}}{{/reactive}}{{/isBodyParam}}{{#hasMore}}, {{/hasMore}}{{/allParams}}): {{>returnTypes}}
{{/operation}}
}
{{/operations}}
Loading