Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/generators/kotlin-spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |null|
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |null|
|sourceFolder|source folder for generated code| |src/main/kotlin|
|suspendFunctions|Whether to generate suspend functions for API operations. Useful for Spring MVC with Kotlin coroutines without requiring the full reactive stack.| |false|
|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|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ public class KotlinSpringServerCodegen extends AbstractKotlinCodegen
public static final String AUTO_X_SPRING_PAGINATED = "autoXSpringPaginated";
public static final String USE_SEALED_RESPONSE_INTERFACES = "useSealedResponseInterfaces";
public static final String COMPANION_OBJECT = "companionObject";
public static final String SUSPEND_FUNCTIONS = "suspendFunctions";

@Getter
public enum DeclarativeInterfaceReactiveMode {
Expand Down Expand Up @@ -166,6 +167,7 @@ public String getDescription() {
@Setter private boolean autoXSpringPaginated = false;
@Setter private boolean useSealedResponseInterfaces = false;
@Setter private boolean companionObject = false;
@Setter private boolean suspendFunctions = false;

@Getter @Setter
protected boolean useSpringBoot3 = false;
Expand Down Expand Up @@ -273,6 +275,7 @@ public KotlinSpringServerCodegen() {
addOption(SCHEMA_IMPLEMENTS_FIELDS, "A map of single field or a list of fields per schema name that should be prepended with `override` (serves similar purpose as `x-kotlin-implements-fields`, but is fully decoupled from the api spec). Example: yaml `schemaImplementsFields: {Pet: id, Category: [name, id], Dog: [bark, breed]}` marks fields to be prepended with `override` in schemas `Pet` (field `id`), `Category` (fields `name`, `id`) and `Dog` (fields `bark`, `breed`)", "empty map");
addSwitch(AUTO_X_SPRING_PAGINATED, "Automatically add x-spring-paginated to operations that have 'page', 'size', and 'sort' query parameters. When enabled, operations with all three parameters will have Pageable support automatically applied. Operations with x-spring-paginated explicitly set to false will not be auto-detected.", autoXSpringPaginated);
addSwitch(COMPANION_OBJECT, "Whether to generate companion objects in data classes, enabling companion extensions.", companionObject);
addSwitch(SUSPEND_FUNCTIONS, "Whether to generate suspend functions for API operations. Useful for Spring MVC with Kotlin coroutines without requiring the full reactive stack.", suspendFunctions);
supportedLibraries.put(SPRING_BOOT, "Spring-boot Server application.");
supportedLibraries.put(SPRING_CLOUD_LIBRARY,
"Spring-Cloud-Feign client with Spring-Boot auto-configured settings.");
Expand Down Expand Up @@ -663,6 +666,11 @@ public void processOpts() {
writePropertyBack(EXCEPTION_HANDLER, exceptionHandler);
writePropertyBack(USE_FLOW_FOR_ARRAY_RETURN_TYPE, useFlowForArrayReturnType);

if (additionalProperties.containsKey(SUSPEND_FUNCTIONS)) {
this.setSuspendFunctions(convertPropertyToBoolean(SUSPEND_FUNCTIONS));
}
writePropertyBack(SUSPEND_FUNCTIONS, suspendFunctions);

if (additionalProperties.containsKey(BEAN_QUALIFIERS) && library.equals(SPRING_BOOT)) {
this.setBeanQualifiers(convertPropertyToBoolean(BEAN_QUALIFIERS));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) v
produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}},
consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}}
)
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}
{{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}}
{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>cookieParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}},
{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
{{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface {{classname}}Delegate {
/**
* @see {{classname}}#{{operationId}}
*/
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{^-last}},
{{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}}Flow<{{{baseType}}}>{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{^-last}},
{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}},
{{/hasParams}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
{{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ interface {{classname}} {
produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}},
consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}}
)
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}
{{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}}
{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>cookieParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}{{#includeHttpRequestContext}}{{#hasParams}},
{{/hasParams}}{{#swagger1AnnotationLibrary}}@ApiParam(hidden = true) {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}}@Parameter(hidden = true) {{/swagger2AnnotationLibrary}}{{#reactive}}exchange: org.springframework.web.server.ServerWebExchange{{/reactive}}{{^reactive}}request: {{javaxPackage}}.servlet.http.HttpServletRequest{{/reactive}}{{/includeHttpRequestContext}}{{#vendorExtensions.x-spring-paginated}}{{#hasParams}},
{{/hasParams}}{{^hasParams}}{{#includeHttpRequestContext}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ interface {{classname}}Service {
{{#isDeprecated}}
@Deprecated(message="Operation is deprecated")
{{/isDeprecated}}
{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}}
{{#suspendFunctions}}suspend {{/suspendFunctions}}{{^suspendFunctions}}{{#reactive}}{{^isArray}}suspend {{/isArray}}{{#isArray}}{{^useFlowForArrayReturnType}}suspend {{/useFlowForArrayReturnType}}{{/isArray}}{{/reactive}}{{/suspendFunctions}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}}
{{/operation}}
}
{{/operations}}
Original file line number Diff line number Diff line change
Expand Up @@ -5054,4 +5054,111 @@ public void shouldAddParameterWithInHeaderWhenImplicitHeadersIsTrue() throws IOE
Assert.assertTrue(content.contains("testHeader"),
"Header name 'testHeader' should appear in the annotation");
}

@Test
public void suspendFunctionsInterfaceOnly() throws Exception {
Path root = generateApiSources(Map.of(
KotlinSpringServerCodegen.SUSPEND_FUNCTIONS, true,
KotlinSpringServerCodegen.DOCUMENTATION_PROVIDER, "none",
KotlinSpringServerCodegen.ANNOTATION_LIBRARY, "none",
KotlinSpringServerCodegen.INTERFACE_ONLY, true,
KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true
), Map.of(
CodegenConstants.MODELS, "false",
CodegenConstants.MODEL_TESTS, "false",
CodegenConstants.MODEL_DOCS, "false",
CodegenConstants.APIS, "true",
CodegenConstants.SUPPORTING_FILES, "false"
));
verifyGeneratedFilesContain(
Map.of(
root.resolve("src/main/kotlin/org/openapitools/api/PetApi.kt"), List.of(
"suspend fun deletePet(",
"suspend fun getPetById("),
root.resolve("src/main/kotlin/org/openapitools/api/UserApi.kt"), List.of(
"suspend fun logoutUser()"),
root.resolve("src/main/kotlin/org/openapitools/api/StoreApi.kt"), List.of(
"suspend fun getInventory()")
)
);
}

@Test
public void suspendFunctionsWithDelegatePattern() throws Exception {
Path root = generateApiSources(Map.of(
KotlinSpringServerCodegen.SUSPEND_FUNCTIONS, true,
KotlinSpringServerCodegen.DOCUMENTATION_PROVIDER, "none",
KotlinSpringServerCodegen.ANNOTATION_LIBRARY, "none",
KotlinSpringServerCodegen.DELEGATE_PATTERN, true,
KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true
), Map.of(
CodegenConstants.MODELS, "false",
CodegenConstants.MODEL_TESTS, "false",
CodegenConstants.MODEL_DOCS, "false",
CodegenConstants.APIS, "true",
CodegenConstants.SUPPORTING_FILES, "false"
));
verifyGeneratedFilesContain(
Map.of(
root.resolve("src/main/kotlin/org/openapitools/api/PetApi.kt"), List.of(
"suspend fun deletePet(",
"suspend fun getPetById("),
root.resolve("src/main/kotlin/org/openapitools/api/PetApiDelegate.kt"), List.of(
"suspend fun deletePet(",
"suspend fun getPetById(")
)
);
}

@Test
public void suspendFunctionsDefaultsToFalse() throws Exception {
Path root = generateApiSources(Map.of(
KotlinSpringServerCodegen.DOCUMENTATION_PROVIDER, "none",
KotlinSpringServerCodegen.ANNOTATION_LIBRARY, "none",
KotlinSpringServerCodegen.INTERFACE_ONLY, true,
KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true
), Map.of(
CodegenConstants.MODELS, "false",
CodegenConstants.MODEL_TESTS, "false",
CodegenConstants.MODEL_DOCS, "false",
CodegenConstants.APIS, "true",
CodegenConstants.SUPPORTING_FILES, "false"
));
verifyGeneratedFilesContain(
Map.of(
root.resolve("src/main/kotlin/org/openapitools/api/PetApi.kt"), List.of(
"fun deletePet(",
"fun getPetById(")
)
);
// Verify no suspend keyword appears
Path petApiPath = root.resolve("src/main/kotlin/org/openapitools/api/PetApi.kt");
String content = new String(Files.readAllBytes(petApiPath), java.nio.charset.StandardCharsets.UTF_8);
Assert.assertFalse(content.contains("suspend fun"),
"suspend should not be present when suspendFunctions is not enabled");
}

@Test
public void suspendFunctionsWithServiceInterface() throws Exception {
Path root = generateApiSources(Map.of(
KotlinSpringServerCodegen.SUSPEND_FUNCTIONS, true,
KotlinSpringServerCodegen.SERVICE_INTERFACE, true,
KotlinSpringServerCodegen.DOCUMENTATION_PROVIDER, "none",
KotlinSpringServerCodegen.ANNOTATION_LIBRARY, "none",
KotlinSpringServerCodegen.USE_RESPONSE_ENTITY, true
), Map.of(
CodegenConstants.MODELS, "false",
CodegenConstants.MODEL_TESTS, "false",
CodegenConstants.MODEL_DOCS, "false",
CodegenConstants.APIS, "true",
CodegenConstants.SUPPORTING_FILES, "false"
));
verifyGeneratedFilesContain(
Map.of(
root.resolve("src/main/kotlin/org/openapitools/api/PetApiService.kt"), List.of(
"suspend fun deletePet(",
"suspend fun getPetById(")
)
);
}
}
Loading