Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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,87 @@ 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(java.nio.file.Files.readAllBytes(petApiPath));
Assert.assertFalse(content.contains("suspend fun"),
"suspend should not be present when suspendFunctions is not enabled");
}
}
Loading