From d11eb44bdeeac3b64b6c1402be0bcdd2d62a729d Mon Sep 17 00:00:00 2001 From: Alain-Michel Chomnoue Nghemning Date: Fri, 27 Feb 2026 15:50:13 +0100 Subject: [PATCH 01/10] Generating All Apis class and delegate interfaces for ktor library --- .../languages/KotlinServerCodegen.java | 19 +++++++++++++++++++ .../libraries/ktor/AllApis.kt.mustache | 18 ++++++++++++++++++ .../libraries/ktor/_api_body.mustache | 12 +++++++++++- .../ktor/_api_delegate_body.mustache | 0 .../kotlin-server/libraries/ktor/api.mustache | 2 +- .../libraries/ktor/apiDelegate.mustache | 16 ++++++++++++++++ 6 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/AllApis.kt.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java index 06755794d42e..9a52c0ef9337 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java @@ -72,6 +72,9 @@ public class KotlinServerCodegen extends AbstractKotlinCodegen implements BeanVa @Getter @Setter private boolean fixJacksonJsonTypeInfoInheritance = true; + @Getter + @Setter + private Boolean delegatePatternEnabled = false; // This is here to potentially warn the user when an option is not supported by the target framework. private Map> optionsSupportedPerFramework = new ImmutableMap.Builder>() @@ -176,6 +179,7 @@ public KotlinServerCodegen() { addSwitch(Constants.OMIT_GRADLE_WRAPPER, Constants.OMIT_GRADLE_WRAPPER_DESC, omitGradleWrapper); addSwitch(USE_JAKARTA_EE, Constants.USE_JAKARTA_EE_DESC, useJakartaEe); addSwitch(Constants.FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE, Constants.FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE_DESC, fixJacksonJsonTypeInfoInheritance); + addSwitch(Constants.DELEGATE_PATTERN, Constants.DELEGATE_PATTERN_DESC, getAutoHeadFeatureEnabled()); } @Override @@ -305,6 +309,13 @@ public void processOpts() { } else { additionalProperties.put(Constants.METRICS, getMetricsFeatureEnabled()); } + if(isKtor()){ + if (additionalProperties.containsKey(Constants.DELEGATE_PATTERN)) { + setDelegatePatternEnabled(convertPropertyToBooleanAndWriteBack(Constants.DELEGATE_PATTERN)); + } else { + additionalProperties.put(Constants.DELEGATE_PATTERN, getDelegatePatternEnabled()); + } + } boolean generateApis = additionalProperties.containsKey(CodegenConstants.GENERATE_APIS) && (Boolean) additionalProperties.get(CodegenConstants.GENERATE_APIS); String packageFolder = (sourceFolder + File.separator + packageName).replace(".", File.separator); @@ -345,6 +356,12 @@ public void processOpts() { if (!getOmitGradleWrapper()) { supportingFiles.add(new SupportingFile("gradle-wrapper.properties", "gradle" + File.separator + "wrapper", "gradle-wrapper.properties")); } + if(isKtor()){ + supportingFiles.add(new SupportingFile("AllApis.kt.mustache", packageFolder, "AllApis.kt")); + if(delegatePatternEnabled){ + apiTemplateFiles.put("apiDelegate.mustache", "Delegate.kt"); + } + } } else if (isJavalin()) { supportingFiles.add(new SupportingFile("Main.kt.mustache", packageFolder, "Main.kt")); @@ -398,6 +415,8 @@ public static class Constants { public static final String IS_KTOR = "isKtor"; public static final String FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE = "fixJacksonJsonTypeInfoInheritance"; public static final String FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE_DESC = "When true (default), ensures Jackson polymorphism works correctly by: (1) always setting visible=true on @JsonTypeInfo, and (2) adding the discriminator property to child models with appropriate default values. When false, visible is only set to true if all children already define the discriminator property."; + public static final String DELEGATE_PATTERN = "delegatePattern"; + public static final String DELEGATE_PATTERN_DESC = "Whether to generate the server files using the delegate pattern. This option is currently supported only when using ktor library."; } @Override diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/AllApis.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/AllApis.kt.mustache new file mode 100644 index 000000000000..ab500b6cd545 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/AllApis.kt.mustache @@ -0,0 +1,18 @@ +package {{packageName}} + +import io.ktor.server.routing.* +{{#generateApis}}{{#apiInfo}}{{#apis}}import {{apiPackage}}.{{classname}} +{{/apis}}{{/apiInfo}}{{/generateApis}} + + +{{#generateApis}} +fun Route.AllApis() { +{{#apiInfo}} +{{#apis}} +{{#operations}} + {{classname}}() +{{/operations}} +{{/apis}} +{{/apiInfo}} +} +{{/generateApis}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache index 54ddf9ed4fe9..a8675925d2aa 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache @@ -1,3 +1,4 @@ +{{^delegatePattern}} {{#hasAuthMethods}} {{>libraries/ktor/_principal}} @@ -20,4 +21,13 @@ call.respond(HttpStatusCode.NotImplemented) {{^examples}} call.respond(HttpStatusCode.NotImplemented) {{/examples}} -{{/hasAuthMethods}} \ No newline at end of file +{{/hasAuthMethods}} +{{/delegatePattern}} +{{#delegatePattern}} +{{#hasAuthMethods}} +{{#lambda.indented}}{{>_api_delegate_body}}{{/lambda.indented}} +{{/hasAuthMethods}} +{{#hasAuthMethods}} +{{>_api_delegate_body}} +{{/hasAuthMethods}} +{{/delegatePattern}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache index 885ba6ae50dc..6a30806182db 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache @@ -38,7 +38,7 @@ fun Route.{{classname}}() { } {{/featureResources}} {{#featureResources}} - {{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}} { + {{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}} { {{operationId}} -> {{#lambda.indented_8}}{{>libraries/ktor/_api_body}}{{/lambda.indented_8}} } {{/featureResources}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache new file mode 100644 index 000000000000..04851f15f690 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache @@ -0,0 +1,16 @@ +{{>licenseInfo}} +package {{apiPackage}} + +import io.ktor.server.routing.RoutingCall +import {{packageName}}.infrastructure.ApiPrincipal +{{#imports}}import {{import}} +{{/imports}} + +{{#operations}} +interface {{classname}}Delegate { + +{{#operation}} + suspend fun {{operationId}}({{#allParams}} {{paramName}}: {{{dataType}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^defaultValue}}{{^required}}?{{/required}}{{/defaultValue}}{{/isNullable}}, {{/allParams}}call:RoutingCall) : {{{returnType}}} +{{/operation}} +} +{{/operations}} From a68899b659d26b0a84d2a75f77f0e5ae992207c3 Mon Sep 17 00:00:00 2001 From: Alain-Michel Chomnoue Nghemning Date: Mon, 16 Mar 2026 10:32:10 +0100 Subject: [PATCH 02/10] Working delegates --- .../languages/KotlinServerCodegen.java | 8 ++ .../kotlin-server/data_class.mustache | 25 ++++ .../libraries/ktor/_api_body.mustache | 7 +- .../ktor/_api_delegate_body.mustache | 129 ++++++++++++++++++ .../kotlin-server/libraries/ktor/api.mustache | 10 +- .../libraries/ktor/apiDelegate.mustache | 10 +- 6 files changed, 179 insertions(+), 10 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java index 9a52c0ef9337..3ca007d97cca 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java @@ -315,6 +315,10 @@ public void processOpts() { } else { additionalProperties.put(Constants.DELEGATE_PATTERN, getDelegatePatternEnabled()); } + if (delegatePatternEnabled) { + typeMapping.put("file", "io.ktor.http.content.PartData.FileItem"); + importMapping.put("io.ktor.http.content.PartData.FileItem", "io.ktor.http.content.PartData.FileItem"); + } } boolean generateApis = additionalProperties.containsKey(CodegenConstants.GENERATE_APIS) && (Boolean) additionalProperties.get(CodegenConstants.GENERATE_APIS); @@ -360,6 +364,10 @@ public void processOpts() { supportingFiles.add(new SupportingFile("AllApis.kt.mustache", packageFolder, "AllApis.kt")); if(delegatePatternEnabled){ apiTemplateFiles.put("apiDelegate.mustache", "Delegate.kt"); + supportingFiles.add(new SupportingFile("Delegates.kt.mustache", infrastructureFolder, "Delegates.kt")); + supportingFiles.add(new SupportingFile("AppDelegates.kt.mustache", infrastructureFolder, "AppDelegates.kt")); + supportingFiles.add(new SupportingFile("BadRequestException.kt.mustache", infrastructureFolder, "BadRequestException.kt")); + supportingFiles.add(new SupportingFile("APINotImplementedException.kt.mustache", infrastructureFolder, "APINotImplementedException.kt")); } } diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache index 2318e574e45b..c334ec1a64da 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache @@ -1,5 +1,8 @@ {{#isKtor}} import kotlinx.serialization.Serializable +import io.ktor.http.Url +import kotlin.uuid.Uuid +import {{packageName}}.infrastructure.BadRequestException {{/isKtor}} {{^isKtor}} {{#parcelizeModels}} @@ -65,7 +68,18 @@ import java.io.Serializable {{/vars}} {{/hasEnums}} {{#vendorExtensions.x-has-data-class-body}} + {{#delegatePattern}} + {{>_data_class_validate}} + {{/delegatePattern}} } + +{{/vendorExtensions.x-has-data-class-body}} +{{^vendorExtensions.x-has-data-class-body}} + {{#delegatePattern}} +{ + {{>_data_class_validate}} +} + {{/delegatePattern}} {{/vendorExtensions.x-has-data-class-body}} {{/isKtor}} {{^isKtor}} @@ -113,7 +127,18 @@ sealed class {{classname}} {{/vars}} {{/hasEnums}} {{#vendorExtensions.x-has-data-class-body}} + {{#delegatePattern}} + {{>_data_class_validate}} + {{/delegatePattern}} +} + +{{/vendorExtensions.x-has-data-class-body}} +{{^vendorExtensions.x-has-data-class-body}} +{{#delegatePattern}} +{ + {{>_data_class_validate}} } +{{/delegatePattern}} {{/vendorExtensions.x-has-data-class-body}} {{/discriminator}} {{/isKtor}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache index a8675925d2aa..0b20ce84f1b6 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_body.mustache @@ -24,10 +24,5 @@ call.respond(HttpStatusCode.NotImplemented) {{/hasAuthMethods}} {{/delegatePattern}} {{#delegatePattern}} -{{#hasAuthMethods}} -{{#lambda.indented}}{{>_api_delegate_body}}{{/lambda.indented}} -{{/hasAuthMethods}} -{{#hasAuthMethods}} -{{>_api_delegate_body}} -{{/hasAuthMethods}} +{{>libraries/ktor/_api_delegate_body}} {{/delegatePattern}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache index e69de29bb2d1..2e8223e7392e 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache @@ -0,0 +1,129 @@ +{{#delegatePattern}} +val {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Delegate: {{classname}}Delegate? by call.delegates +{{/delegatePattern}} +{{#hasFormParams}} +{{^isMultipart}} +val formParameters = call.receiveParameters() +{{/isMultipart}} +{{/hasFormParams}} +{{#allParams}} +{{#isQueryParam}} +{{#featureResources}} +val {{paramName}} = {{operationId}}.{{paramName}} +{{/featureResources}} +{{^featureResources}} +val {{paramName}} = call.request.queryParameters["{{baseName}}"]{{>libraries/ktor/_extract_param_value}} +{{/featureResources}} +{{/isQueryParam}} +{{#isHeaderParam}} +val {{paramName}} = call.request.headers["{{baseName}}"]{{>libraries/ktor/_extract_param_value}} +{{/isHeaderParam}} +{{#isCookieParam}} +val {{paramName}} = call.request.cookies["{{baseName}}"]{{>libraries/ktor/_extract_param_value}} +{{/isCookieParam}} +{{#isBodyParam}} +val {{paramName}} = call.receive<{{{dataType}}}>() +{{#hasValidation}} +{{>libraries/ktor/_param_validation}} +{{/hasValidation}} +{{/isBodyParam}} +{{#isFormParam}} +{{^isMultipart}} +val {{paramName}} = formParameters["{{baseName}}"]{{>libraries/ktor/_extract_param_value}} +{{/isMultipart}} +{{/isFormParam}} +{{#isPathParam}} +{{#featureResources}} +val {{paramName}} = {{operationId}}.{{paramName}} +{{/featureResources}} +{{^featureResources}} +val {{paramName}} = call.parameters["{{baseName}}"]{{>libraries/ktor/_extract_param_value}} +{{/featureResources}} +{{/isPathParam}} +{{#hasValidation}} +{{>libraries/ktor/_param_validation}} +{{/hasValidation}} +{{/allParams}} + +{{#isMultipart}} +{{#allParams}} +{{#isFormParam}} +{{#isArray}} +val {{paramName}} = mutableListOf<{{{dataType}}}>() +{{/isArray}} +{{^isArray}} +var {{paramName}}: {{{dataType}}}? = null +{{/isArray}} +{{/isFormParam}} +{{/allParams}} +call.receiveMultipart().forEachPart { part -> + when (part.name) { + {{#allParams}} + {{#isFormParam}} + "{{baseName}}" -> { + {{#isFile}} + if(part is PartData.FileItem) { + {{#isArray}} + {{paramName}}.add(part) + {{/isArray}} + {{^isArray}} + {{paramName}} = part + {{/isArray}} + } + {{/isFile}} + {{^isFile}} + if(part is PartData.FormItem) { + {{paramName}} = part.value{{>libraries/ktor/_extract_param_value}} + part.dispose() + } + {{/isFile}} + } + {{/isFormParam}} + {{/allParams}} + } +} +{{#allParams}} +{{#isFormParam}} +{{#required}} +{{#isArray}} +if ({{paramName}}.isEmpty()) { + throw BadRequestException(message = "Missing form parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") +} +{{/isArray}} +{{^isArray}} +if ({{paramName}} == null) { + throw BadRequestException(message = "Missing form parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") +} +{{/isArray}} +{{/required}} +{{^required}} +{{#isArray}} +// No default value for arrays +{{/isArray}} +{{^isArray}} +{{#defaultValue}} +if ({{paramName}} == null) { + {{paramName}} = {{{defaultValue}}} +} +{{/defaultValue}} +{{/isArray}} +{{/required}} +{{#hasValidation}} + {{>libraries/ktor/_param_validation}} +{{/hasValidation}} +{{/isFormParam}} +{{/allParams}} +{{/isMultipart}} + +try { + val delegate = {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Delegate ?: throw APINotImplementedException("API operation {{operationId}} has not been implemented yet.") + val result = delegate.{{operationId}}({{#allParams}}{{paramName}}, {{/allParams}}call) + {{#returnType}} + call.respond(result) + {{/returnType}} + {{^returnType}} + call.respond(HttpStatusCode.OK) + {{/returnType}} +} catch (e: APINotImplementedException) { + call.respond(e.statusCode, e.message ?: "Not Implemented") +} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache index 6a30806182db..d7c1980a6744 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache @@ -5,6 +5,7 @@ import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.response.* +import io.ktor.server.request.* {{#featureResources}} import {{packageName}}.Paths import io.ktor.server.resources.options @@ -15,15 +16,19 @@ import io.ktor.server.resources.delete import io.ktor.server.resources.head import io.ktor.server.resources.patch {{/featureResources}} +import io.ktor.util.* +import io.ktor.http.content.* import io.ktor.server.routing.* +import kotlin.uuid.Uuid +import {{packageName}}.infrastructure.delegates +import {{packageName}}.infrastructure.BadRequestException +import {{packageName}}.infrastructure.APINotImplementedException import {{packageName}}.infrastructure.ApiPrincipal {{#imports}}import {{import}} {{/imports}} {{#operations}} fun Route.{{classname}}() { - val empty = mutableMapOf() - {{#operation}} {{#hasAuthMethods}} {{#authMethods}} @@ -45,7 +50,6 @@ fun Route.{{classname}}() { {{#hasAuthMethods}} } {{/hasAuthMethods}} - {{/operation}} } {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache index 04851f15f690..5b99cfe1491d 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache @@ -3,6 +3,7 @@ package {{apiPackage}} import io.ktor.server.routing.RoutingCall import {{packageName}}.infrastructure.ApiPrincipal +import {{packageName}}.infrastructure.APINotImplementedException {{#imports}}import {{import}} {{/imports}} @@ -10,7 +11,14 @@ import {{packageName}}.infrastructure.ApiPrincipal interface {{classname}}Delegate { {{#operation}} - suspend fun {{operationId}}({{#allParams}} {{paramName}}: {{{dataType}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^defaultValue}}{{^required}}?{{/required}}{{/defaultValue}}{{/isNullable}}, {{/allParams}}call:RoutingCall) : {{{returnType}}} + /** + * {{summary}} + * {{notes}} + {{#allParams}}* @param {{paramName}} {{description}} + {{/allParams}}* @param call The Ktor [RoutingCall] + * @return {{{returnType}}} + */ + suspend fun {{operationId}}({{#allParams}} {{paramName}}: {{{dataType}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^defaultValue}}{{^required}}?{{/required}}{{/defaultValue}}{{/isNullable}}, {{/allParams}}call:RoutingCall) : {{{returnType}}} = throw APINotImplementedException("API operation {{operationId}} has not been implemented yet.") {{/operation}} } {{/operations}} From ad327613b7a61d70e97564b88c6e0211798e110a Mon Sep 17 00:00:00 2001 From: Alain-Michel Chomnoue Nghemning Date: Mon, 16 Mar 2026 10:32:21 +0100 Subject: [PATCH 03/10] Working delegates --- .../kotlin-server/_common_validation.mustache | 356 ++++++++++++++++++ .../_data_class_validate.mustache | 12 + .../APINotImplementedException.kt.mustache | 15 + .../libraries/ktor/AppDelegates.kt.mustache | 18 + .../ktor/BadRequestException.kt.mustache | 23 ++ .../libraries/ktor/Delegates.kt.mustache | 49 +++ .../ktor/_extract_param_value.mustache | 1 + .../libraries/ktor/_param_validation.mustache | 12 + 8 files changed, 486 insertions(+) create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/_data_class_validate.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/APINotImplementedException.kt.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/AppDelegates.kt.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/BadRequestException.kt.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Delegates.kt.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_param_validation.mustache diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache new file mode 100644 index 000000000000..e68be24c01aa --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache @@ -0,0 +1,356 @@ +{{#pattern}} +if (!"{{{pattern}}}".toRegex().matches({{{name}}}{{{paramName}}}.toString())) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} does not match pattern {{{pattern}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "pattern", + expectedValue = "{{{pattern}}}", + actualValue = {{{name}}}{{{paramName}}} + ) +} +{{/pattern}} +{{#minLength}} +if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: ""{{/isNullable}}).length < {{{minLength}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} length should be >= {{{minLength}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "minLength", + expectedValue = {{{minLength}}}, + actualValue = ({{{name}}}{{{paramName}}}{{#isNullable}} ?: ""{{/isNullable}}).length + ) +} +{{/minLength}} +{{#maxLength}} +if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: ""{{/isNullable}}).length > {{{maxLength}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} length should be <= {{{maxLength}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "maxLength", + expectedValue = {{{maxLength}}}, + actualValue = ({{{name}}}{{{paramName}}}{{#isNullable}} ?: ""{{/isNullable}}).length + ) +} +{{/maxLength}} +{{#minimum}} +if ({{{name}}}{{{paramName}}} {{#exclusiveMinimum}}<={{/exclusiveMinimum}}{{^exclusiveMinimum}}<{{/exclusiveMinimum}} {{{minimum}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be {{#exclusiveMinimum}}>{{/exclusiveMinimum}}{{^exclusiveMinimum}}>={{/exclusiveMinimum}} {{{minimum}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "{{#exclusiveMinimum}}exclusiveMinimum{{/exclusiveMinimum}}{{^exclusiveMinimum}}minimum{{/exclusiveMinimum}}", + expectedValue = {{{minimum}}}, + actualValue = {{{name}}}{{{paramName}}} + ) +} +{{/minimum}} +{{#maximum}} +if ({{{name}}}{{{paramName}}} {{#exclusiveMaximum}}>={{/exclusiveMaximum}}{{^exclusiveMaximum}}>{{/exclusiveMaximum}} {{{maximum}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be {{#exclusiveMaximum}}<{{/exclusiveMaximum}}{{^exclusiveMaximum}}<={{/exclusiveMaximum}} {{{maximum}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "{{#exclusiveMaximum}}exclusiveMaximum{{/exclusiveMaximum}}{{^exclusiveMaximum}}maximum{{/exclusiveMaximum}}", + expectedValue = {{{maximum}}}, + actualValue = {{{name}}}{{{paramName}}} + ) +} +{{/maximum}} +{{#multipleOf}} +if ({{{name}}}{{{paramName}}} % {{{multipleOf}}} != 0) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be a multiple of {{{multipleOf}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "multipleOf", + expectedValue = {{{multipleOf}}}, + actualValue = {{{name}}}{{{paramName}}} + ) +} +{{/multipleOf}} +{{#minItems}} +if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).size < {{{minItems}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should have at least {{{minItems}}} items", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "minItems", + expectedValue = {{{minItems}}}, + actualValue = ({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).size + ) +} +{{/minItems}} +{{#maxItems}} +if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).size > {{{maxItems}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should have at most {{{maxItems}}} items", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "maxItems", + expectedValue = {{{maxItems}}}, + actualValue = ({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).size + ) +} +{{/maxItems}} +{{#uniqueItems}} +if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).toSet().size != ({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).size) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should have unique items", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "uniqueItems" + ) +} +{{/uniqueItems}} +{{#isEmail}} +if (!"^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$".toRegex().matches({{{name}}}{{{paramName}}}.toString())) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be a valid email", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "email", + actualValue = {{{name}}}{{{paramName}}} + ) +} +{{/isEmail}} +{{#isUuid}} +try { + kotlin.uuid.Uuid.parse({{{name}}}{{{paramName}}}.toString()) +} catch (e: IllegalArgumentException) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be a valid UUID", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "uuid", + actualValue = {{{name}}}{{{paramName}}} + ) +} +{{/isUuid}} +{{#isUri}} +try { + io.ktor.http.Url({{{name}}}{{{paramName}}}.toString()) +} catch (e: Exception) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be a valid URI", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "uri", + actualValue = {{{name}}}{{{paramName}}} + ) +} +{{/isUri}} +{{#isModel}} +{{{name}}}{{{paramName}}}{{#isNullable}}?{{/isNullable}}.validate() +{{/isModel}} +{{#isArray}} +{{#items.isModel}} +{{{name}}}{{{paramName}}}{{#isNullable}}?{{/isNullable}}.forEach { it.validate() } +{{/items.isModel}} +{{^items.isModel}} +{{{name}}}{{{paramName}}}{{#isNullable}}?{{/isNullable}}.forEach { +{{#items}} +{{#pattern}} + if (!"{{{pattern}}}".toRegex().matches(it.toString())) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it does not match pattern {{{pattern}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "pattern", + expectedValue = "{{{pattern}}}", + actualValue = it + ) + } +{{/pattern}} +{{#minLength}} + if (it.toString().length < {{{minLength}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it length should be >= {{{minLength}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "minLength", + expectedValue = {{{minLength}}}, + actualValue = it.toString().length + ) + } +{{/minLength}} +{{#maxLength}} + if (it.toString().length > {{{maxLength}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it length should be <= {{{maxLength}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "maxLength", + expectedValue = {{{maxLength}}}, + actualValue = it.toString().length + ) + } +{{/maxLength}} +{{#minimum}} + if (it.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() {{#exclusiveMinimum}}<={{/exclusiveMinimum}}{{^exclusiveMinimum}}<{{/exclusiveMinimum}} {{{minimum}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be {{#exclusiveMinimum}}>{{/exclusiveMinimum}}{{^exclusiveMinimum}}>={{/exclusiveMinimum}} {{{minimum}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "{{#exclusiveMinimum}}exclusiveMinimum{{/exclusiveMinimum}}{{^exclusiveMinimum}}minimum{{/exclusiveMinimum}}", + expectedValue = {{{minimum}}}, + actualValue = it + ) + } +{{/minimum}} +{{#maximum}} + if (it.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() {{#exclusiveMaximum}}>={{/exclusiveMaximum}}{{^exclusiveMaximum}}>{{/exclusiveMaximum}} {{{maximum}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be {{#exclusiveMaximum}}<{{/exclusiveMaximum}}{{^exclusiveMaximum}}<={{/exclusiveMaximum}} {{{maximum}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "{{#exclusiveMaximum}}exclusiveMaximum{{/exclusiveMaximum}}{{^exclusiveMaximum}}maximum{{/exclusiveMaximum}}", + expectedValue = {{{maximum}}}, + actualValue = it + ) + } +{{/maximum}} +{{#multipleOf}} + if (it.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() % {{{multipleOf}}} != 0{{#isFloat}}.0{{/isFloat}}{{#isDouble}}.0{{/isDouble}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be a multiple of {{{multipleOf}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "multipleOf", + expectedValue = {{{multipleOf}}}, + actualValue = it + ) + } +{{/multipleOf}} +{{#isEmail}} + if (!"^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$".toRegex().matches(it.toString())) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be a valid email", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "email", + actualValue = it + ) + } +{{/isEmail}} +{{#isUuid}} + try { + kotlin.uuid.Uuid.parse(it.toString()) + } catch (e: IllegalArgumentException) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be a valid UUID", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "uuid", + actualValue = it + ) + } +{{/isUuid}} +{{#isUri}} + try { + io.ktor.http.Url(it.toString()) + } catch (e: Exception) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be a valid URI", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "uri", + actualValue = it + ) + } +{{/isUri}} +{{/items}} +} +{{/items.isModel}} +{{/isArray}} +{{#isMap}} +{{#items.isModel}} +{{{name}}}{{{paramName}}}{{#isNullable}}?{{/isNullable}}.forEach { it.value.validate() } +{{/items.isModel}} +{{^items.isModel}} +{{{name}}}{{{paramName}}}{{#isNullable}}?{{/isNullable}}.forEach { (key, value) -> +{{#items}} +{{#pattern}} + if (!"{{{pattern}}}".toRegex().matches(value.toString())) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key does not match pattern {{{pattern}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "pattern", + expectedValue = "{{{pattern}}}", + actualValue = value + ) + } +{{/pattern}} +{{#minLength}} + if (value.toString().length < {{{minLength}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key length should be >= {{{minLength}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "minLength", + expectedValue = {{{minLength}}}, + actualValue = value.toString().length + ) + } +{{/minLength}} +{{#maxLength}} + if (value.toString().length > {{{maxLength}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key length should be <= {{{maxLength}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "maxLength", + expectedValue = {{{maxLength}}}, + actualValue = value.toString().length + ) + } +{{/maxLength}} +{{#minimum}} + if (value.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() {{#exclusiveMinimum}}<={{/exclusiveMinimum}}{{^exclusiveMinimum}}<{{/exclusiveMinimum}} {{{minimum}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be {{#exclusiveMinimum}}>{{/exclusiveMinimum}}{{^exclusiveMinimum}}>={{/exclusiveMinimum}} {{{minimum}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "{{#exclusiveMinimum}}exclusiveMinimum{{/exclusiveMinimum}}{{^exclusiveMinimum}}minimum{{/exclusiveMinimum}}", + expectedValue = {{{minimum}}}, + actualValue = value + ) + } +{{/minimum}} +{{#maximum}} + if (value.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() {{#exclusiveMaximum}}>={{/exclusiveMaximum}}{{^exclusiveMaximum}}>{{/exclusiveMaximum}} {{{maximum}}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be {{#exclusiveMaximum}}<{{/exclusiveMaximum}}{{^exclusiveMaximum}}<={{/exclusiveMaximum}} {{{maximum}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "{{#exclusiveMaximum}}exclusiveMaximum{{/exclusiveMaximum}}{{^exclusiveMaximum}}maximum{{/exclusiveMaximum}}", + expectedValue = {{{maximum}}}, + actualValue = value + ) + } +{{/maximum}} +{{#multipleOf}} + if (value.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() % {{{multipleOf}}} != 0{{#isFloat}}.0{{/isFloat}}{{#isDouble}}.0{{/isDouble}}) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be a multiple of {{{multipleOf}}}", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "multipleOf", + expectedValue = {{{multipleOf}}}, + actualValue = value + ) + } +{{/multipleOf}} +{{#isEmail}} + if (!"^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$".toRegex().matches(value.toString())) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be a valid email", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "email", + actualValue = value + ) + } +{{/isEmail}} +{{#isUuid}} + try { + kotlin.uuid.Uuid.parse(value.toString()) + } catch (e: IllegalArgumentException) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be a valid UUID", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "uuid", + actualValue = value + ) + } +{{/isUuid}} +{{#isUri}} + try { + io.ktor.http.Url(value.toString()) + } catch (e: Exception) { + throw BadRequestException( + message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be a valid URI", + parameterName = "{{{name}}}{{{paramName}}}", + validationType = "uri", + actualValue = value + ) + } +{{/isUri}} +{{/items}} +} +{{/items.isModel}} +{{/isMap}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/_data_class_validate.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/_data_class_validate.mustache new file mode 100644 index 000000000000..33e1a775de74 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/_data_class_validate.mustache @@ -0,0 +1,12 @@ +fun validate() { + {{#vars}} + {{#lambda.indented_4}} + {{#vendorExtensions}} + {{#-first}} + {{! use -first to only include once if vendorExtensions is a map, but it's better to just include it }} + {{/-first}} + {{/vendorExtensions}} + {{>_common_validation}} + {{/lambda.indented_4}} + {{/vars}} +} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/APINotImplementedException.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/APINotImplementedException.kt.mustache new file mode 100644 index 000000000000..551d8d733128 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/APINotImplementedException.kt.mustache @@ -0,0 +1,15 @@ +{{>licenseInfo}} +package {{packageName}}.infrastructure + +import io.ktor.http.HttpStatusCode + +/** + * Exception thrown when an API operation has not been implemented yet. + * + * @param message The error message detailing the missing implementation. + * @param statusCode The HTTP status code to respond with, defaults to [HttpStatusCode.NotImplemented]. + */ +class APINotImplementedException( + message: String = "This API operation has not been implemented yet.", + val statusCode: HttpStatusCode = HttpStatusCode.NotImplemented +) : Exception(message) diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/AppDelegates.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/AppDelegates.kt.mustache new file mode 100644 index 000000000000..7f02b472e038 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/AppDelegates.kt.mustache @@ -0,0 +1,18 @@ +{{>licenseInfo}} +package {{packageName}}.infrastructure + +import io.ktor.util.* +{{#generateApis}}{{#apiInfo}}{{#apis}}import {{apiPackage}}.{{classname}}Delegate +{{/apis}}{{/apiInfo}}{{/generateApis}} + +/** + * Data class to hold all API delegates. + */ +data class AppDelegates( +{{#generateApis}}{{#apiInfo}}{{#apis}} val {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Delegate: {{classname}}Delegate? = null{{^-last}},{{/-last}} +{{/apis}}{{/apiInfo}}{{/generateApis}} +) { + companion object { + val AttributeKey = AttributeKey("AppDelegates") + } +} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/BadRequestException.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/BadRequestException.kt.mustache new file mode 100644 index 000000000000..7b47032ace88 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/BadRequestException.kt.mustache @@ -0,0 +1,23 @@ +{{>licenseInfo}} +package {{packageName}}.infrastructure + +import io.ktor.http.HttpStatusCode + +/** + * Exception thrown when a request contains invalid parameters or fails validation. + * + * @param message The error message detailing the validation failure. + * @param statusCode The HTTP status code to respond with, defaults to [HttpStatusCode.BadRequest]. + * @param parameterName The name of the parameter that failed validation. + * @param validationType The type of validation that failed (e.g., "pattern", "minimum"). + * @param expectedValue The expected value or constraint that was not met. + * @param actualValue The actual value that failed validation. + */ +class BadRequestException( + message: String, + val statusCode: HttpStatusCode = HttpStatusCode.BadRequest, + val parameterName: String? = null, + val validationType: String? = null, + val expectedValue: Any? = null, + val actualValue: Any? = null +) : Exception(message) diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Delegates.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Delegates.kt.mustache new file mode 100644 index 000000000000..166c528e43bc --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Delegates.kt.mustache @@ -0,0 +1,49 @@ +{{>licenseInfo}} +package {{packageName}}.infrastructure + +import io.ktor.server.application.* +import io.ktor.server.routing.* +import io.ktor.util.* +import kotlin.reflect.KProperty +{{#generateApis}} +{{#apiInfo}} +{{#apis}} +import {{apiPackage}}.{{classname}}Delegate +{{/apis}} +{{/apiInfo}} +{{/generateApis}} + +/** + * Extension for [Application] to register delegates. + */ +fun Application.delegates(appDelegates: AppDelegates) { + attributes.put(AppDelegates.AttributeKey, appDelegates) +} + +/** + * Extension for [RoutingCall] to register delegates. + */ +fun RoutingCall.delegates(appDelegates: AppDelegates) { + attributes.put(AppDelegates.AttributeKey, appDelegates) +} + +/** + * Extension for [RoutingContext] to support delegate injection. + */ +inline val RoutingCall.delegates: DelegateProvider + get() = DelegateProvider(this) + +@JvmInline +value class DelegateProvider(val call: RoutingCall) { + @Suppress("UNCHECKED_CAST") + inline operator fun getValue(thisRef: Any?, property: KProperty<*>): T? { + val appDelegates = call.application.attributes.getOrNull(AppDelegates.AttributeKey) + ?: call.attributes.getOrNull(AppDelegates.AttributeKey) + + return when (T::class) { + {{#apiInfo}}{{#apis}}{{classname}}Delegate::class -> appDelegates?.{{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Delegate as T + {{/apis}}{{/apiInfo}} + else -> null as T + } + } +} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache new file mode 100644 index 000000000000..9aa223725a28 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache @@ -0,0 +1 @@ +{{^required}}{{#isString}}{{/isString}}{{#isEnum}}?.let { runCatching { enumValueOf<{{{dataType}}}>(it) }.getOrElse { throw BadRequestException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnum}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadRequestException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnum}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadRequestException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnum}}{{/isString}}{{/required}}{{#required}}{{#defaultValue}}{{#isString}}{{/isString}}{{#isEnum}}?.let { runCatching { enumValueOf<{{{dataType}}}>(it) }.getOrElse { throw BadRequestException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnum}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadRequestException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnum}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadRequestException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnum}}{{/isString}} ?: {{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}.let { it{{#isString}}{{/isString}}{{#isEnum}}?.let { runCatching { enumValueOf<{{{dataType}}}>(it) }.getOrElse { throw BadRequestException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnum}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadRequestException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnum}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadRequestException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnum}}{{/isString}} ?: throw BadRequestException(message = "Missing {{#isQueryParam}}query{{/isQueryParam}}{{#isPathParam}}path{{/isPathParam}}{{#isHeaderParam}}header{{/isHeaderParam}}{{#isCookieParam}}cookie{{/isCookieParam}}{{#isFormParam}}form{{/isFormParam}} parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") }{{/defaultValue}}{{/required}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_param_validation.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_param_validation.mustache new file mode 100644 index 000000000000..2e36718da57b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_param_validation.mustache @@ -0,0 +1,12 @@ +try { + {{>_common_validation}} +} catch (e: BadRequestException) { + call.respond(e.statusCode, mapOf( + "message" to (e.message ?: "Invalid parameter {{paramName}}"), + "parameter" to e.parameterName, + "validation" to e.validationType, + "expected" to e.expectedValue, + "actual" to e.actualValue + ).filterValues { it != null }) + return@{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}} +} From 93a8dc38ef4b7221ed7b779863decf2a2cc322c5 Mon Sep 17 00:00:00 2001 From: Alain-Michel Chomnoue Nghemning Date: Tue, 5 May 2026 15:14:12 +0200 Subject: [PATCH 04/10] Working delegates for ktor --- .../kotlin-server-ktor-delegate-pattern.yaml | 9 + .../languages/KotlinServerCodegen.java | 9 +- .../kotlin-server/_common_validation.mustache | 88 ++-- .../_data_class_validate.mustache | 13 +- .../kotlin-server/data_class.mustache | 12 +- ...ache => BadParameterException.kt.mustache} | 2 +- .../libraries/ktor/README.mustache | 175 ++++++++ .../ktor/_api_delegate_body.mustache | 116 ++--- .../libraries/ktor/_param_validation.mustache | 13 +- .../kotlin-server/libraries/ktor/api.mustache | 10 +- .../libraries/ktor/apiDelegate.mustache | 49 +- .../libraries/ktor/returnTypes.mustache | 1 + .../kotlin/KotlinServerCodegenTest.java | 40 ++ pom.xml | 12 + .../.openapi-generator/FILES | 1 + .../kotlin-server-modelMutable/README.md | 1 + .../kotlin/org/openapitools/server/AllApis.kt | 14 + .../org/openapitools/server/apis/PetApi.kt | 26 +- .../org/openapitools/server/apis/StoreApi.kt | 14 +- .../org/openapitools/server/apis/UserApi.kt | 26 +- .../ktor-delegate-pattern/README.md | 39 ++ .../generated/.openapi-generator-ignore | 23 + .../generated/.openapi-generator/FILES | 27 ++ .../generated/.openapi-generator/VERSION | 1 + .../generated/Dockerfile | 7 + .../ktor-delegate-pattern/generated/README.md | 287 ++++++++++++ .../generated/build.gradle.kts | 43 ++ .../generated/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../generated/settings.gradle | 1 + .../kotlin/org/openapitools/server/AllApis.kt | 14 + .../kotlin/org/openapitools/server/Paths.kt | 164 +++++++ .../org/openapitools/server/apis/PetApi.kt | 177 ++++++++ .../server/apis/PetApiDelegate.kt | 114 +++++ .../org/openapitools/server/apis/StoreApi.kt | 106 +++++ .../server/apis/StoreApiDelegate.kt | 54 +++ .../org/openapitools/server/apis/UserApi.kt | 159 +++++++ .../server/apis/UserApiDelegate.kt | 88 ++++ .../APINotImplementedException.kt | 25 ++ .../server/infrastructure/ApiKeyAuth.kt | 102 +++++ .../server/infrastructure/AppDelegates.kt | 32 ++ .../infrastructure/BadParameterException.kt | 33 ++ .../infrastructure/BadRequestException.kt | 33 ++ .../server/infrastructure/Delegates.kt | 57 +++ .../org/openapitools/server/models/Cat.kt | 47 ++ .../openapitools/server/models/Category.kt | 42 ++ .../org/openapitools/server/models/Dog.kt | 46 ++ .../server/models/ModelApiResponse.kt | 36 ++ .../org/openapitools/server/models/Order.kt | 55 +++ .../org/openapitools/server/models/Pet.kt | 61 +++ .../org/openapitools/server/models/Tag.kt | 33 ++ .../org/openapitools/server/models/User.kt | 52 +++ .../src/main/resources/application.conf | 23 + .../generated/src/main/resources/logback.xml | 15 + .../gradle/libs.versions.toml | 21 + .../ktor-delegate-pattern/pom.xml | 206 +++++++++ .../org/openapitools/server/Application.kt | 14 + .../org/openapitools/server/Frameworks.kt | 10 + .../openapitools/server/GreetingService.kt | 5 + .../kotlin/org/openapitools/server/Routing.kt | 25 ++ .../org/openapitools/server/Serialization.kt | 18 + .../src/main/resources/application.yaml | 6 + .../src/main/resources/logback.xml | 12 + .../openapitools/server/ApplicationTest.kt | 21 + .../org/openapitools/server/PetApiTest.kt | 419 ++++++++++++++++++ .../org/openapitools/server/StoreApiTest.kt | 138 ++++++ .../org/openapitools/server/TestUtil.kt | 84 ++++ .../org/openapitools/server/UserApiTest.kt | 156 +++++++ .../ktor/.openapi-generator/FILES | 1 + .../petstore/kotlin-server/ktor/README.md | 1 + .../kotlin/org/openapitools/server/AllApis.kt | 14 + .../org/openapitools/server/apis/PetApi.kt | 26 +- .../org/openapitools/server/apis/StoreApi.kt | 14 +- .../org/openapitools/server/apis/UserApi.kt | 26 +- 74 files changed, 3594 insertions(+), 259 deletions(-) create mode 100644 bin/configs/kotlin-server-ktor-delegate-pattern.yaml rename modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/{BadRequestException.kt.mustache => BadParameterException.kt.mustache} (96%) create mode 100644 modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/returnTypes.mustache create mode 100644 samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/AllApis.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/README.md create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator-ignore create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/FILES create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/VERSION create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/Dockerfile create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/README.md create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/build.gradle.kts create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle.properties create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle/wrapper/gradle-wrapper.properties create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/settings.gradle create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AllApis.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Paths.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/PetApi.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/PetApiDelegate.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/StoreApiDelegate.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/UserApi.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/UserApiDelegate.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/APINotImplementedException.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/ApiKeyAuth.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/AppDelegates.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/BadParameterException.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/BadRequestException.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/Delegates.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Cat.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Category.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Dog.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/ModelApiResponse.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Order.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Pet.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Tag.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/User.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/application.conf create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/logback.xml create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/gradle/libs.versions.toml create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/pom.xml create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Application.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Frameworks.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/GreetingService.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Routing.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Serialization.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/application.yaml create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/logback.xml create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/ApplicationTest.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/PetApiTest.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/StoreApiTest.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/TestUtil.kt create mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/UserApiTest.kt create mode 100644 samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/AllApis.kt diff --git a/bin/configs/kotlin-server-ktor-delegate-pattern.yaml b/bin/configs/kotlin-server-ktor-delegate-pattern.yaml new file mode 100644 index 000000000000..4fdc4e88668f --- /dev/null +++ b/bin/configs/kotlin-server-ktor-delegate-pattern.yaml @@ -0,0 +1,9 @@ +generatorName: kotlin-server +outputDir: samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated +library: ktor +inputSpec: modules/openapi-generator/src/test/resources/3_1/petstore.yaml +templateDir: modules/openapi-generator/src/main/resources/kotlin-server +additionalProperties: + hideGenerationTimestamp: "true" + serializableModel: "true" + delegatePattern: "true" diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java index 3ca007d97cca..5685923e6fd4 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java @@ -343,9 +343,10 @@ public void processOpts() { if (isKtor2Or3()) { additionalProperties.put(Constants.IS_KTOR, true); - supportingFiles.add(new SupportingFile("AppMain.kt.mustache", packageFolder, "AppMain.kt")); - supportingFiles.add(new SupportingFile("Configuration.kt.mustache", packageFolder, "Configuration.kt")); - + if(!delegatePatternEnabled) { + supportingFiles.add(new SupportingFile("AppMain.kt.mustache", packageFolder, "AppMain.kt")); + supportingFiles.add(new SupportingFile("Configuration.kt.mustache", packageFolder, "Configuration.kt")); + } if (generateApis && resourcesFeatureEnabled) { supportingFiles.add(new SupportingFile("Paths.kt.mustache", packageFolder, "Paths.kt")); } @@ -366,7 +367,7 @@ public void processOpts() { apiTemplateFiles.put("apiDelegate.mustache", "Delegate.kt"); supportingFiles.add(new SupportingFile("Delegates.kt.mustache", infrastructureFolder, "Delegates.kt")); supportingFiles.add(new SupportingFile("AppDelegates.kt.mustache", infrastructureFolder, "AppDelegates.kt")); - supportingFiles.add(new SupportingFile("BadRequestException.kt.mustache", infrastructureFolder, "BadRequestException.kt")); + supportingFiles.add(new SupportingFile("BadParameterException.kt.mustache", infrastructureFolder, "BadParameterException.kt")); supportingFiles.add(new SupportingFile("APINotImplementedException.kt.mustache", infrastructureFolder, "APINotImplementedException.kt")); } } diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache index e68be24c01aa..2fc1309ac76a 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache @@ -1,6 +1,6 @@ {{#pattern}} if (!"{{{pattern}}}".toRegex().matches({{{name}}}{{{paramName}}}.toString())) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} does not match pattern {{{pattern}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "pattern", @@ -10,30 +10,30 @@ if (!"{{{pattern}}}".toRegex().matches({{{name}}}{{{paramName}}}.toString())) { } {{/pattern}} {{#minLength}} -if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: ""{{/isNullable}}).length < {{{minLength}}}) { - throw BadRequestException( +if (({{{name}}}{{{paramName}}}{{^required}} ?: ""{{/required}}).length < {{{minLength}}}) { + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} length should be >= {{{minLength}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "minLength", expectedValue = {{{minLength}}}, - actualValue = ({{{name}}}{{{paramName}}}{{#isNullable}} ?: ""{{/isNullable}}).length + actualValue = ({{{name}}}{{{paramName}}}{{^required}} ?: ""{{/required}}).length ) } {{/minLength}} {{#maxLength}} -if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: ""{{/isNullable}}).length > {{{maxLength}}}) { - throw BadRequestException( +if (({{{name}}}{{{paramName}}}{{^required}} ?: ""{{/required}}).length > {{{maxLength}}}) { + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} length should be <= {{{maxLength}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "maxLength", expectedValue = {{{maxLength}}}, - actualValue = ({{{name}}}{{{paramName}}}{{#isNullable}} ?: ""{{/isNullable}}).length + actualValue = ({{{name}}}{{{paramName}}}{{^required}} ?: ""{{/required}}).length ) } {{/maxLength}} {{#minimum}} if ({{{name}}}{{{paramName}}} {{#exclusiveMinimum}}<={{/exclusiveMinimum}}{{^exclusiveMinimum}}<{{/exclusiveMinimum}} {{{minimum}}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be {{#exclusiveMinimum}}>{{/exclusiveMinimum}}{{^exclusiveMinimum}}>={{/exclusiveMinimum}} {{{minimum}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "{{#exclusiveMinimum}}exclusiveMinimum{{/exclusiveMinimum}}{{^exclusiveMinimum}}minimum{{/exclusiveMinimum}}", @@ -44,7 +44,7 @@ if ({{{name}}}{{{paramName}}} {{#exclusiveMinimum}}<={{/exclusiveMinimum}}{{^exc {{/minimum}} {{#maximum}} if ({{{name}}}{{{paramName}}} {{#exclusiveMaximum}}>={{/exclusiveMaximum}}{{^exclusiveMaximum}}>{{/exclusiveMaximum}} {{{maximum}}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be {{#exclusiveMaximum}}<{{/exclusiveMaximum}}{{^exclusiveMaximum}}<={{/exclusiveMaximum}} {{{maximum}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "{{#exclusiveMaximum}}exclusiveMaximum{{/exclusiveMaximum}}{{^exclusiveMaximum}}maximum{{/exclusiveMaximum}}", @@ -55,7 +55,7 @@ if ({{{name}}}{{{paramName}}} {{#exclusiveMaximum}}>={{/exclusiveMaximum}}{{^exc {{/maximum}} {{#multipleOf}} if ({{{name}}}{{{paramName}}} % {{{multipleOf}}} != 0) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be a multiple of {{{multipleOf}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "multipleOf", @@ -65,30 +65,30 @@ if ({{{name}}}{{{paramName}}} % {{{multipleOf}}} != 0) { } {{/multipleOf}} {{#minItems}} -if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).size < {{{minItems}}}) { - throw BadRequestException( +if (({{{name}}}{{{paramName}}}{{^required}} ?: emptyList() {{/required}}).size < {{{minItems}}}) { + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should have at least {{{minItems}}} items", parameterName = "{{{name}}}{{{paramName}}}", validationType = "minItems", expectedValue = {{{minItems}}}, - actualValue = ({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).size + actualValue = ({{{name}}}{{{paramName}}}{{^required}} ?: emptyList() {{/required}}).size ) } {{/minItems}} {{#maxItems}} -if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).size > {{{maxItems}}}) { - throw BadRequestException( +if (({{{name}}}{{{paramName}}}{{^required}} ?: emptyList() {{/required}}).size > {{{maxItems}}}) { + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should have at most {{{maxItems}}} items", parameterName = "{{{name}}}{{{paramName}}}", validationType = "maxItems", expectedValue = {{{maxItems}}}, - actualValue = ({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).size + actualValue = ({{{name}}}{{{paramName}}}{{^required}} ?: emptyList() {{/required}}).size ) } {{/maxItems}} {{#uniqueItems}} -if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).toSet().size != ({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).size) { - throw BadRequestException( +if (({{{name}}}{{{paramName}}}{{^required}} ?: emptyList() {{/required}}).toSet().size != ({{{name}}}{{{paramName}}}{{^required}} ?: emptyList() {{/required}}).size) { + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should have unique items", parameterName = "{{{name}}}{{{paramName}}}", validationType = "uniqueItems" @@ -97,7 +97,7 @@ if (({{{name}}}{{{paramName}}}{{#isNullable}} ?: emptyList() {{/isNullable}}).to {{/uniqueItems}} {{#isEmail}} if (!"^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$".toRegex().matches({{{name}}}{{{paramName}}}.toString())) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be a valid email", parameterName = "{{{name}}}{{{paramName}}}", validationType = "email", @@ -109,7 +109,7 @@ if (!"^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$".toRegex().matches({{{na try { kotlin.uuid.Uuid.parse({{{name}}}{{{paramName}}}.toString()) } catch (e: IllegalArgumentException) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be a valid UUID", parameterName = "{{{name}}}{{{paramName}}}", validationType = "uuid", @@ -121,7 +121,7 @@ try { try { io.ktor.http.Url({{{name}}}{{{paramName}}}.toString()) } catch (e: Exception) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be a valid URI", parameterName = "{{{name}}}{{{paramName}}}", validationType = "uri", @@ -130,18 +130,18 @@ try { } {{/isUri}} {{#isModel}} -{{{name}}}{{{paramName}}}{{#isNullable}}?{{/isNullable}}.validate() +{{{name}}}{{{paramName}}}{{^required}}?{{/required}}.validate() {{/isModel}} {{#isArray}} {{#items.isModel}} -{{{name}}}{{{paramName}}}{{#isNullable}}?{{/isNullable}}.forEach { it.validate() } +{{{name}}}{{{paramName}}}{{^required}}?{{/required}}.forEach { it.validate() } {{/items.isModel}} {{^items.isModel}} -{{{name}}}{{{paramName}}}{{#isNullable}}?{{/isNullable}}.forEach { +{{{name}}}{{{paramName}}}{{^required}}?{{/required}}.forEach { {{#items}} {{#pattern}} if (!"{{{pattern}}}".toRegex().matches(it.toString())) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it does not match pattern {{{pattern}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "pattern", @@ -152,7 +152,7 @@ try { {{/pattern}} {{#minLength}} if (it.toString().length < {{{minLength}}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it length should be >= {{{minLength}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "minLength", @@ -163,7 +163,7 @@ try { {{/minLength}} {{#maxLength}} if (it.toString().length > {{{maxLength}}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it length should be <= {{{maxLength}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "maxLength", @@ -174,7 +174,7 @@ try { {{/maxLength}} {{#minimum}} if (it.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() {{#exclusiveMinimum}}<={{/exclusiveMinimum}}{{^exclusiveMinimum}}<{{/exclusiveMinimum}} {{{minimum}}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be {{#exclusiveMinimum}}>{{/exclusiveMinimum}}{{^exclusiveMinimum}}>={{/exclusiveMinimum}} {{{minimum}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "{{#exclusiveMinimum}}exclusiveMinimum{{/exclusiveMinimum}}{{^exclusiveMinimum}}minimum{{/exclusiveMinimum}}", @@ -185,7 +185,7 @@ try { {{/minimum}} {{#maximum}} if (it.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() {{#exclusiveMaximum}}>={{/exclusiveMaximum}}{{^exclusiveMaximum}}>{{/exclusiveMaximum}} {{{maximum}}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be {{#exclusiveMaximum}}<{{/exclusiveMaximum}}{{^exclusiveMaximum}}<={{/exclusiveMaximum}} {{{maximum}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "{{#exclusiveMaximum}}exclusiveMaximum{{/exclusiveMaximum}}{{^exclusiveMaximum}}maximum{{/exclusiveMaximum}}", @@ -196,7 +196,7 @@ try { {{/maximum}} {{#multipleOf}} if (it.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() % {{{multipleOf}}} != 0{{#isFloat}}.0{{/isFloat}}{{#isDouble}}.0{{/isDouble}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be a multiple of {{{multipleOf}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "multipleOf", @@ -207,7 +207,7 @@ try { {{/multipleOf}} {{#isEmail}} if (!"^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$".toRegex().matches(it.toString())) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be a valid email", parameterName = "{{{name}}}{{{paramName}}}", validationType = "email", @@ -219,7 +219,7 @@ try { try { kotlin.uuid.Uuid.parse(it.toString()) } catch (e: IllegalArgumentException) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be a valid UUID", parameterName = "{{{name}}}{{{paramName}}}", validationType = "uuid", @@ -231,7 +231,7 @@ try { try { io.ktor.http.Url(it.toString()) } catch (e: Exception) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be a valid URI", parameterName = "{{{name}}}{{{paramName}}}", validationType = "uri", @@ -245,14 +245,14 @@ try { {{/isArray}} {{#isMap}} {{#items.isModel}} -{{{name}}}{{{paramName}}}{{#isNullable}}?{{/isNullable}}.forEach { it.value.validate() } +{{{name}}}{{{paramName}}}{{^required}}?{{/required}}.forEach { it.value.validate() } {{/items.isModel}} {{^items.isModel}} -{{{name}}}{{{paramName}}}{{#isNullable}}?{{/isNullable}}.forEach { (key, value) -> +{{{name}}}{{{paramName}}}{{^required}}?{{/required}}.forEach { (key, value) -> {{#items}} {{#pattern}} if (!"{{{pattern}}}".toRegex().matches(value.toString())) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key does not match pattern {{{pattern}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "pattern", @@ -263,7 +263,7 @@ try { {{/pattern}} {{#minLength}} if (value.toString().length < {{{minLength}}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key length should be >= {{{minLength}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "minLength", @@ -274,7 +274,7 @@ try { {{/minLength}} {{#maxLength}} if (value.toString().length > {{{maxLength}}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key length should be <= {{{maxLength}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "maxLength", @@ -285,7 +285,7 @@ try { {{/maxLength}} {{#minimum}} if (value.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() {{#exclusiveMinimum}}<={{/exclusiveMinimum}}{{^exclusiveMinimum}}<{{/exclusiveMinimum}} {{{minimum}}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be {{#exclusiveMinimum}}>{{/exclusiveMinimum}}{{^exclusiveMinimum}}>={{/exclusiveMinimum}} {{{minimum}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "{{#exclusiveMinimum}}exclusiveMinimum{{/exclusiveMinimum}}{{^exclusiveMinimum}}minimum{{/exclusiveMinimum}}", @@ -296,7 +296,7 @@ try { {{/minimum}} {{#maximum}} if (value.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() {{#exclusiveMaximum}}>={{/exclusiveMaximum}}{{^exclusiveMaximum}}>{{/exclusiveMaximum}} {{{maximum}}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be {{#exclusiveMaximum}}<{{/exclusiveMaximum}}{{^exclusiveMaximum}}<={{/exclusiveMaximum}} {{{maximum}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "{{#exclusiveMaximum}}exclusiveMaximum{{/exclusiveMaximum}}{{^exclusiveMaximum}}maximum{{/exclusiveMaximum}}", @@ -307,7 +307,7 @@ try { {{/maximum}} {{#multipleOf}} if (value.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() % {{{multipleOf}}} != 0{{#isFloat}}.0{{/isFloat}}{{#isDouble}}.0{{/isDouble}}) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be a multiple of {{{multipleOf}}}", parameterName = "{{{name}}}{{{paramName}}}", validationType = "multipleOf", @@ -318,7 +318,7 @@ try { {{/multipleOf}} {{#isEmail}} if (!"^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$".toRegex().matches(value.toString())) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be a valid email", parameterName = "{{{name}}}{{{paramName}}}", validationType = "email", @@ -330,7 +330,7 @@ try { try { kotlin.uuid.Uuid.parse(value.toString()) } catch (e: IllegalArgumentException) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be a valid UUID", parameterName = "{{{name}}}{{{paramName}}}", validationType = "uuid", @@ -342,7 +342,7 @@ try { try { io.ktor.http.Url(value.toString()) } catch (e: Exception) { - throw BadRequestException( + throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be a valid URI", parameterName = "{{{name}}}{{{paramName}}}", validationType = "uri", diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/_data_class_validate.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/_data_class_validate.mustache index 33e1a775de74..0f110811a91b 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/_data_class_validate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/_data_class_validate.mustache @@ -1,12 +1,5 @@ fun validate() { - {{#vars}} - {{#lambda.indented_4}} - {{#vendorExtensions}} - {{#-first}} - {{! use -first to only include once if vendorExtensions is a map, but it's better to just include it }} - {{/-first}} - {{/vendorExtensions}} - {{>_common_validation}} - {{/lambda.indented_4}} - {{/vars}} +{{#vars}} + {{#lambda.indented}}{{>_common_validation}}{{/lambda.indented}} +{{/vars}} } diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache index c334ec1a64da..12fa7fceef56 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/data_class.mustache @@ -1,8 +1,8 @@ {{#isKtor}} import kotlinx.serialization.Serializable -import io.ktor.http.Url -import kotlin.uuid.Uuid -import {{packageName}}.infrastructure.BadRequestException +{{#delegatePattern}} +import {{packageName}}.infrastructure.BadParameterException +{{/delegatePattern}} {{/isKtor}} {{^isKtor}} {{#parcelizeModels}} @@ -72,7 +72,6 @@ import java.io.Serializable {{>_data_class_validate}} {{/delegatePattern}} } - {{/vendorExtensions.x-has-data-class-body}} {{^vendorExtensions.x-has-data-class-body}} {{#delegatePattern}} @@ -128,15 +127,14 @@ sealed class {{classname}} {{/hasEnums}} {{#vendorExtensions.x-has-data-class-body}} {{#delegatePattern}} - {{>_data_class_validate}} + {{#lambda.indented}}{{>_data_class_validate}}{{/lambda.indented}} {{/delegatePattern}} } - {{/vendorExtensions.x-has-data-class-body}} {{^vendorExtensions.x-has-data-class-body}} {{#delegatePattern}} { - {{>_data_class_validate}} +{{#lambda.indented}}{{>_data_class_validate}}{{/lambda.indented}} } {{/delegatePattern}} {{/vendorExtensions.x-has-data-class-body}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/BadRequestException.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/BadParameterException.kt.mustache similarity index 96% rename from modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/BadRequestException.kt.mustache rename to modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/BadParameterException.kt.mustache index 7b47032ace88..43f4874117bf 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/BadRequestException.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/BadParameterException.kt.mustache @@ -13,7 +13,7 @@ import io.ktor.http.HttpStatusCode * @param expectedValue The expected value or constraint that was not met. * @param actualValue The actual value that failed validation. */ -class BadRequestException( +class BadParameterException( message: String, val statusCode: HttpStatusCode = HttpStatusCode.BadRequest, val parameterName: String? = null, diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/README.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/README.mustache index 24333d99ccbd..e95521b24bf1 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/README.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/README.mustache @@ -44,6 +44,181 @@ docker run -p 8080:8080 {{artifactId}} * ~Supports collection formats for query parameters: csv, tsv, ssv, pipes.~ * Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in OpenAPI definitions. +{{#delegatePattern}} +## Using the delegate pattern (ktor) + +When generating a server with the `delegatePattern` enabled, the router delegates request handling to small, focused interfaces you implement. This is ideal for API‑first development: you wire your business logic without touching generated routing code. + + +### What gets generated +- For each API: an interface `{{apiPackage}}.Delegate` with one `suspend` function per operation. Each function receives parsed parameters plus a `io.ktor.server.routing.RoutingCall` named `call`. +- In `{{packageName}}.infrastructure`: + - `AppDelegates` – a data holder aggregating all optional delegates. + - `Delegates` – helpers to register and inject delegates (`Application.delegates(AppDelegates(...))`, `RoutingCall.delegates(AppDelegates(...))`. + - `APINotImplementedException` – thrown when a delegate is missing. + – `BadParameterException` – used for parameter validation errors (400). + +{{#generateApis}}{{#apiInfo}}{{#apis}}- Generated interface: `{{apiPackage}}.{{classname}}Delegate` +{{/apis}}{{/apiInfo}}{{/generateApis}} + +### Implement a delegate +Implement the generated interface(s) in your codebase. Example (using the first generated API): +{{#apiInfo}}{{#apis}}{{#-first}} +```kotlin +package com.example.impl + +import {{apiPackage}}.{{classname}}Delegate +import io.ktor.server.routing.RoutingCall +{{#operations}}{{#operation}}{{#-first}}{{#imports}}import {{import}} +{{/imports}}{{/-first}}{{/operation}}{{/operations}} +class {{classname}}DelegateImpl : {{classname}}Delegate { +{{#operations}}{{#operation}}{{#-first}} + override suspend fun {{operationId}}({{#allParams}}{{paramName}}: {{{dataType}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^defaultValue}}{{^required}}?{{/required}}{{/defaultValue}}{{/isNullable}}, {{/allParams}}call: RoutingCall): {{{returnType}}}{{^returnType}}Unit{{/returnType}} { + // Access authentication, request, etc. via the call if needed + // val principal = call.principal<{{packageName}}.infrastructure.ApiPrincipal>() + // Return the payload expected by the spec (or Unit if no body) + TODO("Provide implementation") + } +{{/-first}}{{/operation}}{{/operations}} +} +``` +{{/-first}}{{/apis}}{{/apiInfo}} + + +Notes: +- Do not call `call.respond(...)` inside the delegate. Return the value; the generated route will serialize and respond for you. For operations without a response body, return `kotlin.Unit` and the route responds with 200 by default. + + +### Register your delegates +Register your implementations once during application startup so routes can resolve them: +```kotlin +import {{packageName}}.infrastructure.AppDelegates +import {{packageName}}.infrastructure.delegates +import {{packageName}}.AllApis +import io.ktor.server.application.Application +import io.ktor.server.routing.routing + +fun Application.module() { + delegates( + AppDelegates( + // Property names are lowerCamelCase of the API class + "Delegate" + // Provide only what you implement; others can be left null +{{#apiInfo}}{{#apis}} // {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Delegate = {{classname}}DelegateImpl(), +{{/apis}}{{/apiInfo}} + ) + ) + + // ... install features, etc. + routing { + AllApis() + } +} +``` + +If you prefer to register APIs individually: +```kotlin +{{#apiInfo}}{{#apis}}import {{apiPackage}}.{{classname}} +{{/apis}}{{/apiInfo}} +import io.ktor.server.application.Application +import io.ktor.server.routing.routing + +fun Application.module() { + // ... + routing { +{{#apiInfo}}{{#apis}} {{classname}}() +{{/apis}}{{/apiInfo}} + } +} +``` +If you prefer per-request scoping, you may also attach delegates to a specific `RoutingCall`: +```kotlin +{{#apiInfo}}{{#apis}}{{#-first}}call.delegates(AppDelegates({{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Delegate = {{classname}}DelegateImpl())){{/-first}}{{/apis}}{{/apiInfo}} +``` + +### Runtime behavior +- If a route is invoked and its corresponding delegate is not provided, the server throws `APINotImplementedException` and responds with 501 Not Implemented. +- If a required parameter is missing or invalid, `BadParameterException` is thrown and the server responds with 400 Bad Request. +- You can access authenticated principals within delegates via `call.principal<{{packageName}}.infrastructure.ApiPrincipal>()` when auth is enabled for the operation. + +### Handle delegate exceptions with StatusPages +To return consistent responses for delegate errors, install Ktor `StatusPages` and map both exceptions: + +```kotlin +import io.ktor.http.HttpStatusCode +import io.ktor.server.plugins.statuspages.StatusPages +import io.ktor.server.plugins.statuspages.exception +import io.ktor.server.response.respondText +import {{packageName}}.infrastructure.APINotImplementedException +import {{packageName}}.infrastructure.BadParameterException + +install(StatusPages) { + exception { call, cause -> + when (cause) { + is BadParameterException -> { + call.respondText( + status = cause.statusCode, + text = mapOf( + "message" to (cause.message ?: "Invalid parameter"), + "parameter" to cause.parameterName, + "validation" to cause.validationType, + "expected" to cause.expectedValue, + "actual" to cause.actualValue + ).filterValues { it != null }.mapValues { it.toString() }.toString() + ) + } + + is APINotImplementedException -> { + call.respondText( + status = HttpStatusCode.NotImplemented, + text = cause.message ?: "API not implemented" + ) + } + } + } +} +``` + +### Handle file uploads in delegates +For multipart endpoints, generated delegates receive an `MultiPartReceiver` (operation-specific name) so you can parse form fields and process file parts inside your implementation. + +Example (`uploadFile`): +```kotlin +import io.ktor.server.routing.RoutingCall +import io.ktor.utils.io.toByteArray +import {{apiPackage}}.PetApiDelegate +import {{modelPackage}}.ModelApiResponse + +class PetApiDelegateImpl : PetApiDelegate { + override suspend fun uploadFile( + petId: Long, + uploadFileMultipartReceiver: PetApiDelegate.UploadFileMultiPartReceiver, + call: RoutingCall + ): ModelApiResponse { + var fileName: String? = null + var fileContent: String? = null + + val multipart = uploadFileMultipartReceiver.receiveMultipart { + onReceiveFile { + fileName = originalFileName + fileContent = provider().toByteArray().decodeToString() + } + } + + val additionalMetadata = multipart.additionalMetadata + + return ModelApiResponse( + code = 200, + message = "uploaded file=$fileName metadata=$additionalMetadata content=$fileContent" + ) + } +} +``` + +Notes: +- `receiveMultipart { ... }` iterates incoming parts and maps known fields into the generated multipart model. +- Use `onReceiveFile { ... }` to handle each uploaded file stream (`PartData.FileItem`) without calling `call.receiveMultipart()` directly in your delegate. +- Scalar form values (for example `additionalMetadata`) are available on the returned multipart object. +{{/delegatePattern}} {{#generateApiDocs}} ## Documentation for API Endpoints diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache index 2e8223e7392e..2665b7da469b 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_api_delegate_body.mustache @@ -23,9 +23,6 @@ val {{paramName}} = call.request.cookies["{{baseName}}"]{{>libraries/ktor/_extra {{/isCookieParam}} {{#isBodyParam}} val {{paramName}} = call.receive<{{{dataType}}}>() -{{#hasValidation}} -{{>libraries/ktor/_param_validation}} -{{/hasValidation}} {{/isBodyParam}} {{#isFormParam}} {{^isMultipart}} @@ -40,90 +37,49 @@ val {{paramName}} = {{operationId}}.{{paramName}} val {{paramName}} = call.parameters["{{baseName}}"]{{>libraries/ktor/_extract_param_value}} {{/featureResources}} {{/isPathParam}} -{{#hasValidation}} -{{>libraries/ktor/_param_validation}} -{{/hasValidation}} {{/allParams}} {{#isMultipart}} -{{#allParams}} -{{#isFormParam}} -{{#isArray}} -val {{paramName}} = mutableListOf<{{{dataType}}}>() -{{/isArray}} -{{^isArray}} -var {{paramName}}: {{{dataType}}}? = null -{{/isArray}} -{{/isFormParam}} -{{/allParams}} -call.receiveMultipart().forEachPart { part -> - when (part.name) { - {{#allParams}} - {{#isFormParam}} - "{{baseName}}" -> { - {{#isFile}} - if(part is PartData.FileItem) { - {{#isArray}} - {{paramName}}.add(part) - {{/isArray}} - {{^isArray}} - {{paramName}} = part - {{/isArray}} +val {{operationId}}MultipartReceiver = {{classname}}Delegate.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPartReceiver { block -> + val builder = {{classname}}Delegate.{{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPartFileBuilder() + with(builder) { block() } + call.receiveMultipart().forEachPart { part -> + when (part.name) { + {{#allParams}} + {{#isFormParam}} + "{{baseName}}" -> { + {{#isFile}} + if(part is PartData.FileItem) { + builder.onReceive{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}}(part) + } + {{/isFile}} + {{^isFile}} + if(part is PartData.FormItem) { + builder.{{paramName}}(part.value{{>libraries/ktor/_extract_param_value}}) + } + {{/isFile}} } - {{/isFile}} - {{^isFile}} - if(part is PartData.FormItem) { - {{paramName}} = part.value{{>libraries/ktor/_extract_param_value}} - part.dispose() - } - {{/isFile}} + {{/isFormParam}} + {{/allParams}} } - {{/isFormParam}} - {{/allParams}} + part.dispose() } + builder.build() } +{{/isMultipart}} + {{#allParams}} -{{#isFormParam}} -{{#required}} -{{#isArray}} -if ({{paramName}}.isEmpty()) { - throw BadRequestException(message = "Missing form parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") -} -{{/isArray}} -{{^isArray}} -if ({{paramName}} == null) { - throw BadRequestException(message = "Missing form parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") -} -{{/isArray}} -{{/required}} -{{^required}} -{{#isArray}} -// No default value for arrays -{{/isArray}} -{{^isArray}} -{{#defaultValue}} -if ({{paramName}} == null) { - {{paramName}} = {{{defaultValue}}} -} -{{/defaultValue}} -{{/isArray}} -{{/required}} -{{#hasValidation}} - {{>libraries/ktor/_param_validation}} -{{/hasValidation}} -{{/isFormParam}} + {{#hasValidation}} + {{>libraries/ktor/_param_validation}} + {{/hasValidation}} + {{^hasValidation}} + {{#isModel}} + {{>libraries/ktor/_param_validation}} + {{/isModel}} + {{/hasValidation}} {{/allParams}} -{{/isMultipart}} -try { - val delegate = {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Delegate ?: throw APINotImplementedException("API operation {{operationId}} has not been implemented yet.") - val result = delegate.{{operationId}}({{#allParams}}{{paramName}}, {{/allParams}}call) - {{#returnType}} - call.respond(result) - {{/returnType}} - {{^returnType}} - call.respond(HttpStatusCode.OK) - {{/returnType}} -} catch (e: APINotImplementedException) { - call.respond(e.statusCode, e.message ?: "Not Implemented") -} +val delegate = {{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Delegate ?: throw APINotImplementedException("API operation {{operationId}} has not been implemented yet.") +val result = delegate.{{operationId}}({{^isMultipart}}{{#allParams}}{{paramName}}, {{/allParams}}{{/isMultipart}}{{#isMultipart}}{{#allParams}}{{^isFormParam}}{{paramName}}, {{/isFormParam}}{{/allParams}}{{operationId}}MultipartReceiver, {{/isMultipart}}call) +call.respond(result) + diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_param_validation.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_param_validation.mustache index 2e36718da57b..e1e76bbb46d2 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_param_validation.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_param_validation.mustache @@ -1,12 +1 @@ -try { - {{>_common_validation}} -} catch (e: BadRequestException) { - call.respond(e.statusCode, mapOf( - "message" to (e.message ?: "Invalid parameter {{paramName}}"), - "parameter" to e.parameterName, - "validation" to e.validationType, - "expected" to e.expectedValue, - "actual" to e.actualValue - ).filterValues { it != null }) - return@{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}} -} + {{#lambda.indented}}{{>_common_validation}}{{/lambda.indented}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache index d7c1980a6744..37c216f6cc12 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/api.mustache @@ -5,7 +5,6 @@ import io.ktor.http.* import io.ktor.server.application.* import io.ktor.server.auth.* import io.ktor.server.response.* -import io.ktor.server.request.* {{#featureResources}} import {{packageName}}.Paths import io.ktor.server.resources.options @@ -16,13 +15,14 @@ import io.ktor.server.resources.delete import io.ktor.server.resources.head import io.ktor.server.resources.patch {{/featureResources}} -import io.ktor.util.* -import io.ktor.http.content.* import io.ktor.server.routing.* -import kotlin.uuid.Uuid +{{#delegatePattern}} +import io.ktor.server.request.* +import io.ktor.http.content.* import {{packageName}}.infrastructure.delegates -import {{packageName}}.infrastructure.BadRequestException +import {{packageName}}.infrastructure.BadParameterException import {{packageName}}.infrastructure.APINotImplementedException +{{/delegatePattern}} import {{packageName}}.infrastructure.ApiPrincipal {{#imports}}import {{import}} {{/imports}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache index 5b99cfe1491d..ba8d2cc38e94 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache @@ -1,9 +1,11 @@ {{>licenseInfo}} package {{apiPackage}} +import io.ktor.http.content.PartData import io.ktor.server.routing.RoutingCall import {{packageName}}.infrastructure.ApiPrincipal import {{packageName}}.infrastructure.APINotImplementedException +import {{packageName}}.infrastructure.BadParameterException {{#imports}}import {{import}} {{/imports}} @@ -11,14 +13,53 @@ import {{packageName}}.infrastructure.APINotImplementedException interface {{classname}}Delegate { {{#operation}} +{{#isMultipart}} + data class {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPart({{#allParams}}{{#isFormParam}}{{^isFile}}val {{paramName}}: {{{dataType}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^defaultValue}}{{^required}}?{{/required}}{{/defaultValue}}{{/isNullable}}, {{/isFile}}{{/isFormParam}}{{/allParams}}) + class {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPartFileBuilder{ +{{#allParams}}{{#isFormParam}}{{^isFile}} + private var {{paramName}}: {{{dataType}}}? = null +{{/isFile}}{{#isFile}}{{#isArray}} + private var _onReceive{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}}: suspend PartData.FileItem.() -> Unit = {} + val onReceive{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}}: suspend PartData.FileItem.() -> Unit + get() = _onReceive{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}} +{{/isArray}}{{^isArray}} + private var _onReceive{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}}: suspend PartData.FileItem.() -> Unit = {} + val onReceive{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}}: suspend PartData.FileItem.() -> Unit + get() = _onReceive{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}} +{{/isArray}} +{{/isFile}}{{/isFormParam}}{{/allParams}} +{{#allParams}}{{#isFormParam}}{{^isFile}} + fun {{paramName}}({{paramName}}: {{{dataType}}}?) { this.{{paramName}} = {{paramName}} } +{{/isFile}}{{#isFile}} + fun onReceive{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}}(block: suspend PartData.FileItem.() -> Unit = {}) { _onReceive{{#lambda.titlecase}}{{paramName}}{{/lambda.titlecase}} = block } +{{/isFile}}{{/isFormParam}}{{/allParams}} + fun build(): {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPart { +{{#allParams}}{{#isFormParam}}{{^isFile}}{{#required}} + if ({{paramName}} == null) { + throw BadRequestException(message = "Missing form parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") + } +{{/required}}{{^required}}{{#defaultValue}} + if ({{paramName}} == null) { + {{paramName}} = {{{defaultValue}}} + } +{{/defaultValue}}{{/required}}{{/isFile}}{{/isFormParam}}{{/allParams}} + return {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPart({{#allParams}}{{#isFormParam}}{{^isFile}}{{paramName}}, {{/isFile}}{{/isFormParam}}{{/allParams}}) + } + } + fun interface {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPartReceiver{ + suspend fun receiveMultipart(block: {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPartFileBuilder.() -> Unit): {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPart + } +{{/isMultipart}} /** * {{summary}} * {{notes}} - {{#allParams}}* @param {{paramName}} {{description}} - {{/allParams}}* @param call The Ktor [RoutingCall] - * @return {{{returnType}}} + {{^isMultipart}}{{#allParams}}* @param {{paramName}} {{description}} + {{/allParams}}{{/isMultipart}}{{#isMultipart}}{{#allParams}}{{^isFormParam}}* @param {{paramName}} {{description}} + {{/isFormParam}}{{/allParams}}* @param {{operationId}}MultipartReceiver interface to receive multipart data + {{/isMultipart}}* @param call The Ktor [RoutingCall] + * @return {{>returnTypes}} */ - suspend fun {{operationId}}({{#allParams}} {{paramName}}: {{{dataType}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^defaultValue}}{{^required}}?{{/required}}{{/defaultValue}}{{/isNullable}}, {{/allParams}}call:RoutingCall) : {{{returnType}}} = throw APINotImplementedException("API operation {{operationId}} has not been implemented yet.") + suspend fun {{operationId}}({{^isMultipart}}{{#allParams}} {{paramName}}: {{{dataType}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^defaultValue}}{{^required}}?{{/required}}{{/defaultValue}}{{/isNullable}}, {{/allParams}}{{/isMultipart}}{{#isMultipart}}{{#allParams}}{{^isFormParam}} {{paramName}}: {{{dataType}}}{{#isNullable}}?{{/isNullable}}{{^isNullable}}{{^defaultValue}}{{^required}}?{{/required}}{{/defaultValue}}{{/isNullable}}, {{/isFormParam}}{{/allParams}} {{operationId}}MultipartReceiver: {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPartReceiver, {{/isMultipart}}call:RoutingCall) : {{>returnTypes}} = throw APINotImplementedException("API operation {{operationId}} has not been implemented yet.") {{/operation}} } {{/operations}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/returnTypes.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/returnTypes.mustache new file mode 100644 index 000000000000..612aa9ec0599 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/returnTypes.mustache @@ -0,0 +1 @@ +{{#isMap}}Map{{/isMap}}{{#isArray}}{{#reactive}}{{#useFlowForArrayReturnType}}Flow{{/useFlowForArrayReturnType}}{{^useFlowForArrayReturnType}}{{{returnContainer}}}{{/useFlowForArrayReturnType}}{{/reactive}}{{^reactive}}{{{returnContainer}}}{{/reactive}}<{{{returnType}}}>{{/isArray}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}} \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java index 0850466701e1..6315fbfae667 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java @@ -509,4 +509,44 @@ public void fixJacksonJsonTypeInfoInheritance_canBeDisabled() throws IOException "visible = false" ); } + + @Test + public void delegatePattern_canBeEnabled() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(LIBRARY, KTOR); + codegen.additionalProperties().put(DELEGATE_PATTERN, true); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_0/petstore.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; + Path petApi = Paths.get(outputPath + "/apis/PetApi.kt"); + + // API should use the delegate + assertFileContains( + petApi, + "val petApiDelegate: PetApiDelegate? by call.delegates" + ); + + // Delegate interface should be generated + Path petApiDelegate = Paths.get(outputPath + "/apis/PetApiDelegate.kt"); + Assert.assertTrue(Files.exists(petApiDelegate)); + assertFileContains( + petApiDelegate, + "interface PetApiDelegate" + ); + + // Supporting files should be generated + String infraPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server/infrastructure"; + Assert.assertTrue(Files.exists(Paths.get(infraPath + "/Delegates.kt"))); + Assert.assertTrue(Files.exists(Paths.get(infraPath + "/AppDelegates.kt"))); + Assert.assertTrue(Files.exists(Paths.get(infraPath + "/BadParameterException.kt"))); + Assert.assertTrue(Files.exists(Paths.get(infraPath + "/APINotImplementedException.kt"))); + } } diff --git a/pom.xml b/pom.xml index fe50f2e8c573..115cb86a7b06 100644 --- a/pom.xml +++ b/pom.xml @@ -1150,6 +1150,18 @@ samples/server/petstore/java-undertow + + ktor-delegate-pattern + + + env + java + + + + samples/server/petstore/kotlin-server/ktor-delegate-pattern + + openapi-generator diff --git a/samples/server/petstore/kotlin-server-modelMutable/.openapi-generator/FILES b/samples/server/petstore/kotlin-server-modelMutable/.openapi-generator/FILES index 52c6d592e8f2..786a5876752f 100644 --- a/samples/server/petstore/kotlin-server-modelMutable/.openapi-generator/FILES +++ b/samples/server/petstore/kotlin-server-modelMutable/.openapi-generator/FILES @@ -4,6 +4,7 @@ build.gradle.kts gradle.properties gradle/wrapper/gradle-wrapper.properties settings.gradle +src/main/kotlin/org/openapitools/server/AllApis.kt src/main/kotlin/org/openapitools/server/AppMain.kt src/main/kotlin/org/openapitools/server/Configuration.kt src/main/kotlin/org/openapitools/server/Paths.kt diff --git a/samples/server/petstore/kotlin-server-modelMutable/README.md b/samples/server/petstore/kotlin-server-modelMutable/README.md index 92500a09feda..b371e659e03e 100644 --- a/samples/server/petstore/kotlin-server-modelMutable/README.md +++ b/samples/server/petstore/kotlin-server-modelMutable/README.md @@ -42,6 +42,7 @@ docker run -p 8080:8080 kotlin-server * ~Supports collection formats for query parameters: csv, tsv, ssv, pipes.~ * Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in OpenAPI definitions. + ## Documentation for API Endpoints diff --git a/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/AllApis.kt b/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/AllApis.kt new file mode 100644 index 000000000000..402a6beadeb9 --- /dev/null +++ b/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/AllApis.kt @@ -0,0 +1,14 @@ +package org.openapitools.server + +import io.ktor.server.routing.* +import org.openapitools.server.apis.PetApi +import org.openapitools.server.apis.StoreApi +import org.openapitools.server.apis.UserApi + + + +fun Route.AllApis() { + PetApi() + StoreApi() + UserApi() +} diff --git a/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/PetApi.kt b/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/PetApi.kt index b72ec0d72cec..e854c91df315 100644 --- a/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/PetApi.kt +++ b/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/PetApi.kt @@ -29,10 +29,8 @@ import org.openapitools.server.models.ModelApiResponse import org.openapitools.server.models.Pet fun Route.PetApi() { - val empty = mutableMapOf() - authenticate("petstore_auth") { - post { + post { addPet -> val principal = call.authentication.principal() @@ -41,9 +39,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - delete { + delete { deletePet -> val principal = call.authentication.principal() @@ -52,9 +49,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - get { + get { findPetsByStatus -> val principal = call.authentication.principal() @@ -102,9 +98,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - get { + get { findPetsByTags -> val principal = call.authentication.principal() @@ -152,9 +147,8 @@ fun Route.PetApi() { } } - authenticate("api_key") { - get { + get { getPetById -> val principal = call.authentication.principal() @@ -186,9 +180,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - put { + put { updatePet -> val principal = call.authentication.principal() @@ -197,9 +190,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - post { + post { updatePetWithForm -> val principal = call.authentication.principal() @@ -208,9 +200,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - post { + post { uploadFile -> val principal = call.authentication.principal() @@ -230,5 +221,4 @@ fun Route.PetApi() { } } - } diff --git a/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt b/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt index 4a4395778401..62a7b778a36c 100644 --- a/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt +++ b/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt @@ -28,15 +28,12 @@ import org.openapitools.server.infrastructure.ApiPrincipal import org.openapitools.server.models.Order fun Route.StoreApi() { - val empty = mutableMapOf() - - delete { + delete { deleteOrder -> call.respond(HttpStatusCode.NotImplemented) } - authenticate("api_key") { - get { + get { getInventory -> val principal = call.authentication.principal() @@ -45,8 +42,7 @@ fun Route.StoreApi() { } } - - get { + get { getOrderById -> val exampleContentType = "application/json" val exampleContentString = """{ "petId" : 6, @@ -64,8 +60,7 @@ fun Route.StoreApi() { } } - - post { + post { placeOrder -> val exampleContentType = "application/json" val exampleContentString = """{ "petId" : 6, @@ -83,5 +78,4 @@ fun Route.StoreApi() { } } - } diff --git a/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/UserApi.kt b/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/UserApi.kt index 299f743e141d..c60ef4516d0d 100644 --- a/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/UserApi.kt +++ b/samples/server/petstore/kotlin-server-modelMutable/src/main/kotlin/org/openapitools/server/apis/UserApi.kt @@ -28,29 +28,23 @@ import org.openapitools.server.infrastructure.ApiPrincipal import org.openapitools.server.models.User fun Route.UserApi() { - val empty = mutableMapOf() - - post { + post { createUser -> call.respond(HttpStatusCode.NotImplemented) } - - post { + post { createUsersWithArrayInput -> call.respond(HttpStatusCode.NotImplemented) } - - post { + post { createUsersWithListInput -> call.respond(HttpStatusCode.NotImplemented) } - - delete { + delete { deleteUser -> call.respond(HttpStatusCode.NotImplemented) } - - get { + get { getUserByName -> val exampleContentType = "application/json" val exampleContentString = """{ "firstName" : "firstName", @@ -70,20 +64,16 @@ fun Route.UserApi() { } } - - get { + get { loginUser -> call.respond(HttpStatusCode.NotImplemented) } - - get { + get { logoutUser -> call.respond(HttpStatusCode.NotImplemented) } - - put { + put { updateUser -> call.respond(HttpStatusCode.NotImplemented) } - } diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/README.md b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/README.md new file mode 100644 index 000000000000..044b002ab899 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/README.md @@ -0,0 +1,39 @@ +# server + +This project was created using the [Ktor Project Generator](https://start.ktor.io). + +Here are some useful links to get you started: + +- [Ktor Documentation](https://ktor.io/docs/home.html) +- [Ktor GitHub page](https://github.com/ktorio/ktor) +- The [Ktor Slack chat](https://app.slack.com/client/T09229ZC6/C0A974TJ9). You'll need to [request an invite](https://surveys.jetbrains.com/s3/kotlin-slack-sign-up) to join. + +## Features + +Here's a list of features included in this project: + +| Name | Description | +| ------------------------------------------------------------------------|------------------------------------------------------------------------------------ | +| [Dependency Injection](https://start.ktor.io/p/ktor-di) | Enables dependency injection for your server | +| [Content Negotiation](https://start.ktor.io/p/content-negotiation) | Provides automatic content conversion according to Content-Type and Accept headers | +| [Routing](https://start.ktor.io/p/routing) | Provides a structured routing DSL | +| [kotlinx.serialization](https://start.ktor.io/p/kotlinx-serialization) | Handles JSON serialization using kotlinx.serialization library | +| [Resources](https://start.ktor.io/p/resources) | Provides type-safe routing | + +## Building & Running + +To build or run the project, use one of the following tasks: + +| Task | Description | +| -----------------------------------------------------------|------------------- | +| `mvn test` | Run the tests | +| `mvn package` | Build the project | +| `java -jar target/server-0.0.1-jar-with-dependencies.jar` | Run the server | + +If the server starts successfully, you'll see the following output: + +``` +2024-12-04 14:32:45.584 [main] INFO Application - Application started in 0.303 seconds. +2024-12-04 14:32:45.682 [main] INFO Application - Responding at http://0.0.0.0:8080 +``` + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator-ignore b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/FILES b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/FILES new file mode 100644 index 000000000000..9b0fc4342ba8 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/FILES @@ -0,0 +1,27 @@ +Dockerfile +README.md +build.gradle.kts +gradle.properties +gradle/wrapper/gradle-wrapper.properties +settings.gradle +src/main/kotlin/org/openapitools/server/AllApis.kt +src/main/kotlin/org/openapitools/server/Paths.kt +src/main/kotlin/org/openapitools/server/apis/PetApi.kt +src/main/kotlin/org/openapitools/server/apis/PetApiDelegate.kt +src/main/kotlin/org/openapitools/server/apis/StoreApi.kt +src/main/kotlin/org/openapitools/server/apis/StoreApiDelegate.kt +src/main/kotlin/org/openapitools/server/apis/UserApi.kt +src/main/kotlin/org/openapitools/server/apis/UserApiDelegate.kt +src/main/kotlin/org/openapitools/server/infrastructure/APINotImplementedException.kt +src/main/kotlin/org/openapitools/server/infrastructure/ApiKeyAuth.kt +src/main/kotlin/org/openapitools/server/infrastructure/AppDelegates.kt +src/main/kotlin/org/openapitools/server/infrastructure/BadParameterException.kt +src/main/kotlin/org/openapitools/server/infrastructure/Delegates.kt +src/main/kotlin/org/openapitools/server/models/Category.kt +src/main/kotlin/org/openapitools/server/models/ModelApiResponse.kt +src/main/kotlin/org/openapitools/server/models/Order.kt +src/main/kotlin/org/openapitools/server/models/Pet.kt +src/main/kotlin/org/openapitools/server/models/Tag.kt +src/main/kotlin/org/openapitools/server/models/User.kt +src/main/resources/application.conf +src/main/resources/logback.xml diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/VERSION b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/VERSION new file mode 100644 index 000000000000..0610c66bc14f --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.21.0-SNAPSHOT diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/Dockerfile b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/Dockerfile new file mode 100644 index 000000000000..0c1f942d32ae --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:8-jre-alpine + +COPY ./build/libs/kotlin-server.jar /root/kotlin-server.jar + +WORKDIR /root + +CMD ["java", "-server", "-Xms4g", "-Xmx4g", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "kotlin-server.jar"] \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/README.md b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/README.md new file mode 100644 index 000000000000..93746904eef9 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/README.md @@ -0,0 +1,287 @@ +# org.openapitools.server - Kotlin Server library for OpenAPI Petstore + +This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. + +Generated by OpenAPI Generator 7.21.0-SNAPSHOT. + +## Requires + +* Kotlin 2.0.20 +* Gradle 8.10.2 + +## Build + +First, create the gradle wrapper script: + +``` +gradle wrapper +``` + +Then, run: + +``` +./gradlew check assemble +``` + +This runs all tests and packages the library. + +## Running + +The server builds as a fat jar with a main entrypoint. To start the service, run `java -jar ./build/libs/kotlin-server.jar`. + +You may also run in docker: + +``` +docker build -t kotlin-server . +docker run -p 8080:8080 kotlin-server +``` + +## Features/Implementation Notes + +* Supports JSON inputs/outputs, File inputs, and Form inputs (see ktor documentation for more info). +* ~Supports collection formats for query parameters: csv, tsv, ssv, pipes.~ +* Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in OpenAPI definitions. + +## Using the delegate pattern (ktor) + +When generating a server with the `delegatePattern` enabled, the router delegates request handling to small, focused interfaces you implement. This is ideal for API‑first development: you wire your business logic without touching generated routing code. + + +### What gets generated +- For each API: an interface `org.openapitools.server.apis.Delegate` with one `suspend` function per operation. Each function receives parsed parameters plus a `io.ktor.server.routing.RoutingCall` named `call`. +- In `org.openapitools.server.infrastructure`: + - `AppDelegates` – a data holder aggregating all optional delegates. + - `Delegates` – helpers to register and inject delegates (`Application.delegates(AppDelegates(...))`, `RoutingCall.delegates(AppDelegates(...))`. + - `APINotImplementedException` – thrown when a delegate is missing. + – `BadParameterException` – used for parameter validation errors (400). + +- Generated interface: `org.openapitools.server.apis.PetApiDelegate` +- Generated interface: `org.openapitools.server.apis.StoreApiDelegate` +- Generated interface: `org.openapitools.server.apis.UserApiDelegate` + + +### Implement a delegate +Implement the generated interface(s) in your codebase. Example (using the first generated API): + +```kotlin +package com.example.impl + +import org.openapitools.server.apis.PetApiDelegate +import io.ktor.server.routing.RoutingCall +import + +class PetApiDelegateImpl : PetApiDelegate { + + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + // Access authentication, request, etc. via the call if needed + // val principal = call.principal() + // Return the payload expected by the spec (or Unit if no body) + TODO("Provide implementation") + } + +} +``` + + + +Notes: +- Do not call `call.respond(...)` inside the delegate. Return the value; the generated route will serialize and respond for you. For operations without a response body, return `kotlin.Unit` and the route responds with 200 by default. + + +### Register your delegates +Register your implementations once during application startup so routes can resolve them: +```kotlin +import org.openapitools.server.infrastructure.AppDelegates +import org.openapitools.server.infrastructure.delegates +import org.openapitools.server.AllApis +import io.ktor.server.application.Application +import io.ktor.server.routing.routing + +fun Application.module() { + delegates( + AppDelegates( + // Property names are lowerCamelCase of the API class + "Delegate" + // Provide only what you implement; others can be left null + // petApiDelegate = PetApiDelegateImpl(), + // storeApiDelegate = StoreApiDelegateImpl(), + // userApiDelegate = UserApiDelegateImpl(), + + ) + ) + + // ... install features, etc. + routing { + AllApis() + } +} +``` + +If you prefer to register APIs individually: +```kotlin +import org.openapitools.server.apis.PetApi +import org.openapitools.server.apis.StoreApi +import org.openapitools.server.apis.UserApi + +import io.ktor.server.application.Application +import io.ktor.server.routing.routing + +fun Application.module() { + // ... + routing { + PetApi() + StoreApi() + UserApi() + + } +} +``` +If you prefer per-request scoping, you may also attach delegates to a specific `RoutingCall`: +```kotlin +call.delegates(AppDelegates(petApiDelegate = PetApiDelegateImpl())) +``` + +### Runtime behavior +- If a route is invoked and its corresponding delegate is not provided, the server throws `APINotImplementedException` and responds with 501 Not Implemented. +- If a required parameter is missing or invalid, `BadParameterException` is thrown and the server responds with 400 Bad Request. +- You can access authenticated principals within delegates via `call.principal()` when auth is enabled for the operation. + +### Handle delegate exceptions with StatusPages +To return consistent responses for delegate errors, install Ktor `StatusPages` and map both exceptions: + +```kotlin +import io.ktor.http.HttpStatusCode +import io.ktor.server.plugins.statuspages.StatusPages +import io.ktor.server.plugins.statuspages.exception +import io.ktor.server.response.respondText +import org.openapitools.server.infrastructure.APINotImplementedException +import org.openapitools.server.infrastructure.BadParameterException + +install(StatusPages) { + exception { call, cause -> + when (cause) { + is BadParameterException -> { + call.respondText( + status = cause.statusCode, + text = mapOf( + "message" to (cause.message ?: "Invalid parameter"), + "parameter" to cause.parameterName, + "validation" to cause.validationType, + "expected" to cause.expectedValue, + "actual" to cause.actualValue + ).filterValues { it != null }.mapValues { it.toString() }.toString() + ) + } + + is APINotImplementedException -> { + call.respondText( + status = HttpStatusCode.NotImplemented, + text = cause.message ?: "API not implemented" + ) + } + } + } +} +``` + +### Handle file uploads in delegates +For multipart endpoints, generated delegates receive an `MultiPartReceiver` (operation-specific name) so you can parse form fields and process file parts inside your implementation. + +Example (`uploadFile`): +```kotlin +import io.ktor.server.routing.RoutingCall +import io.ktor.utils.io.toByteArray +import org.openapitools.server.apis.PetApiDelegate +import org.openapitools.server.models.ModelApiResponse + +class PetApiDelegateImpl : PetApiDelegate { + override suspend fun uploadFile( + petId: Long, + uploadFileMultipartReceiver: PetApiDelegate.UploadFileMultiPartReceiver, + call: RoutingCall + ): ModelApiResponse { + var fileName: String? = null + var fileContent: String? = null + + val multipart = uploadFileMultipartReceiver.receiveMultipart { + onReceiveFile { + fileName = originalFileName + fileContent = provider().toByteArray().decodeToString() + } + } + + val additionalMetadata = multipart.additionalMetadata + + return ModelApiResponse( + code = 200, + message = "uploaded file=$fileName metadata=$additionalMetadata content=$fileContent" + ) + } +} +``` + +Notes: +- `receiveMultipart { ... }` iterates incoming parts and maps known fields into the generated multipart model. +- Use `onReceiveFile { ... }` to handle each uploaded file stream (`PartData.FileItem`) without calling `call.receiveMultipart()` directly in your delegate. +- Scalar form values (for example `additionalMetadata`) are available on the returned multipart object. + +## Documentation for API Endpoints + +All URIs are relative to *http://petstore.swagger.io/v2* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +*PetApi* | [**addPet**](docs/PetApi.md#addpet) | **POST** /pet | Add a new pet to the store +*PetApi* | [**deletePet**](docs/PetApi.md#deletepet) | **DELETE** /pet/{petId} | Deletes a pet +*PetApi* | [**findPetsByStatus**](docs/PetApi.md#findpetsbystatus) | **GET** /pet/findByStatus | Finds Pets by status +*PetApi* | [**findPetsByTags**](docs/PetApi.md#findpetsbytags) | **GET** /pet/findByTags | Finds Pets by tags +*PetApi* | [**getPetById**](docs/PetApi.md#getpetbyid) | **GET** /pet/{petId} | Find pet by ID +*PetApi* | [**updatePet**](docs/PetApi.md#updatepet) | **PUT** /pet | Update an existing pet +*PetApi* | [**updatePetWithForm**](docs/PetApi.md#updatepetwithform) | **POST** /pet/{petId} | Updates a pet in the store with form data +*PetApi* | [**uploadFile**](docs/PetApi.md#uploadfile) | **POST** /pet/{petId}/uploadImage | uploads an image +*StoreApi* | [**deleteOrder**](docs/StoreApi.md#deleteorder) | **DELETE** /store/order/{orderId} | Delete purchase order by ID +*StoreApi* | [**getInventory**](docs/StoreApi.md#getinventory) | **GET** /store/inventory | Returns pet inventories by status +*StoreApi* | [**getOrderById**](docs/StoreApi.md#getorderbyid) | **GET** /store/order/{orderId} | Find purchase order by ID +*StoreApi* | [**placeOrder**](docs/StoreApi.md#placeorder) | **POST** /store/order | Place an order for a pet +*UserApi* | [**createUser**](docs/UserApi.md#createuser) | **POST** /user | Create user +*UserApi* | [**createUsersWithArrayInput**](docs/UserApi.md#createuserswitharrayinput) | **POST** /user/createWithArray | Creates list of users with given input array +*UserApi* | [**createUsersWithListInput**](docs/UserApi.md#createuserswithlistinput) | **POST** /user/createWithList | Creates list of users with given input array +*UserApi* | [**deleteUser**](docs/UserApi.md#deleteuser) | **DELETE** /user/{username} | Delete user +*UserApi* | [**getUserByName**](docs/UserApi.md#getuserbyname) | **GET** /user/{username} | Get user by user name +*UserApi* | [**loginUser**](docs/UserApi.md#loginuser) | **GET** /user/login | Logs user into the system +*UserApi* | [**logoutUser**](docs/UserApi.md#logoutuser) | **GET** /user/logout | Logs out current logged in user session +*UserApi* | [**updateUser**](docs/UserApi.md#updateuser) | **PUT** /user/{username} | Updated user + + + +## Documentation for Models + + - [org.openapitools.server.models.Category](docs/Category.md) + - [org.openapitools.server.models.ModelApiResponse](docs/ModelApiResponse.md) + - [org.openapitools.server.models.Order](docs/Order.md) + - [org.openapitools.server.models.Pet](docs/Pet.md) + - [org.openapitools.server.models.Tag](docs/Tag.md) + - [org.openapitools.server.models.User](docs/User.md) + + + +## Documentation for Authorization + + +Authentication schemes defined for the API: + +### petstore_auth + +- **Type**: OAuth +- **Flow**: implicit +- **Authorization URL**: http://petstore.swagger.io/api/oauth/dialog +- **Scopes**: + - write:pets: modify pets in your account + - read:pets: read your pets + + +### api_key + +- **Type**: API key +- **API key parameter name**: api_key +- **Location**: HTTP header + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/build.gradle.kts b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/build.gradle.kts new file mode 100644 index 000000000000..1e91d49feaf6 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/build.gradle.kts @@ -0,0 +1,43 @@ + +val kotlin_version: String by project +val logback_version: String by project + +group = "org.openapitools" +version = "1.0.0" + +plugins { + kotlin("jvm") version "2.0.20" + application + kotlin("plugin.serialization") version "2.0.20" +} + +application { + mainClass.set("io.ktor.server.netty.EngineMain") + + val isDevelopment: Boolean = project.ext.has("development") + applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(platform("io.ktor:ktor-bom:3.0.2")) + implementation("ch.qos.logback:logback-classic:$logback_version") + implementation("com.typesafe:config:1.4.1") + implementation("io.ktor:ktor-server-auth") + implementation("io.ktor:ktor-client-apache") + implementation("io.ktor:ktor-server-auto-head-response") + implementation("io.ktor:ktor-server-default-headers") + implementation("io.ktor:ktor-server-content-negotiation") + implementation("io.ktor:ktor-serialization-kotlinx-json") + implementation("io.ktor:ktor-server-resources") + implementation("io.ktor:ktor-server-hsts") + implementation("io.ktor:ktor-server-compression") + implementation("io.dropwizard.metrics:metrics-core:4.1.18") + implementation("io.ktor:ktor-server-metrics") + implementation("io.ktor:ktor-server-netty") + + testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle.properties b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle.properties new file mode 100644 index 000000000000..36dd5c928837 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle.properties @@ -0,0 +1,4 @@ +kotlin.code.style=official +ktor_version=3.0.2 +kotlin_version=2.0.20 +logback_version=1.5.19 diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle/wrapper/gradle-wrapper.properties b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..18330fcba804 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/settings.gradle b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/settings.gradle new file mode 100644 index 000000000000..a09a58efab11 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kotlin-server' \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AllApis.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AllApis.kt new file mode 100644 index 000000000000..402a6beadeb9 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AllApis.kt @@ -0,0 +1,14 @@ +package org.openapitools.server + +import io.ktor.server.routing.* +import org.openapitools.server.apis.PetApi +import org.openapitools.server.apis.StoreApi +import org.openapitools.server.apis.UserApi + + + +fun Route.AllApis() { + PetApi() + StoreApi() + UserApi() +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Paths.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Paths.kt new file mode 100644 index 000000000000..bfef4fe21eaa --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Paths.kt @@ -0,0 +1,164 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server + +import io.ktor.resources.* +import kotlinx.serialization.* +import org.openapitools.server.models.* + +object Paths { + /** + * Add a new pet to the store + * + * @param pet Pet object that needs to be added to the store + */ + @Resource("/pet") class addPet() + + /** + * Deletes a pet + * + * @param petId Pet id to delete + * @param apiKey (optional) + */ + @Resource("/pet/{petId}") class deletePet(val petId: kotlin.Long) + + /** + * Finds Pets by status + * Multiple status values can be provided with comma separated strings + * @param status Status values that need to be considered for filter + */ + @Resource("/pet/findByStatus") class findPetsByStatus(val status: kotlin.collections.List) + + /** + * Finds Pets by tags + * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + * @param tags Tags to filter by + */ + @Resource("/pet/findByTags") class findPetsByTags(val tags: kotlin.collections.List) + + /** + * Find pet by ID + * Returns a single pet + * @param petId ID of pet to return + */ + @Resource("/pet/{petId}") class getPetById(val petId: kotlin.Long) + + /** + * Update an existing pet + * + * @param pet Pet object that needs to be added to the store + */ + @Resource("/pet") class updatePet() + + /** + * Updates a pet in the store with form data + * + * @param petId ID of pet that needs to be updated + * @param name Updated name of the pet (optional) + * @param status Updated status of the pet (optional) + */ + @Resource("/pet/{petId}") class updatePetWithForm(val petId: kotlin.Long) + + /** + * uploads an image + * + * @param petId ID of pet to update + * @param additionalMetadata Additional data to pass to server (optional) + * @param file file to upload (optional) + */ + @Resource("/pet/{petId}/uploadImage") class uploadFile(val petId: kotlin.Long) + + /** + * Delete purchase order by ID + * For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + * @param orderId ID of the order that needs to be deleted + */ + @Resource("/store/order/{orderId}") class deleteOrder(val orderId: kotlin.String) + + /** + * Returns pet inventories by status + * Returns a map of status codes to quantities + */ + @Resource("/store/inventory") class getInventory + + /** + * Find purchase order by ID + * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions + * @param orderId ID of pet that needs to be fetched + */ + @Resource("/store/order/{orderId}") class getOrderById(val orderId: kotlin.Long) + + /** + * Place an order for a pet + * + * @param order order placed for purchasing the pet + */ + @Resource("/store/order") class placeOrder() + + /** + * Create user + * This can only be done by the logged in user. + * @param user Created user object + */ + @Resource("/user") class createUser() + + /** + * Creates list of users with given input array + * + * @param user List of user object + */ + @Resource("/user/createWithArray") class createUsersWithArrayInput() + + /** + * Creates list of users with given input array + * + * @param user List of user object + */ + @Resource("/user/createWithList") class createUsersWithListInput() + + /** + * Delete user + * This can only be done by the logged in user. + * @param username The name that needs to be deleted + */ + @Resource("/user/{username}") class deleteUser(val username: kotlin.String) + + /** + * Get user by user name + * + * @param username The name that needs to be fetched. Use user1 for testing. + */ + @Resource("/user/{username}") class getUserByName(val username: kotlin.String) + + /** + * Logs user into the system + * + * @param username The user name for login + * @param password The password for login in clear text + */ + @Resource("/user/login") class loginUser(val username: kotlin.String, val password: kotlin.String) + + /** + * Logs out current logged in user session + * + */ + @Resource("/user/logout") class logoutUser + + /** + * Updated user + * This can only be done by the logged in user. + * @param username name that need to be deleted + * @param user Updated user object + */ + @Resource("/user/{username}") class updateUser(val username: kotlin.String) + +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/PetApi.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/PetApi.kt new file mode 100644 index 000000000000..1800567f1e5c --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/PetApi.kt @@ -0,0 +1,177 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.apis + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.response.* +import org.openapitools.server.Paths +import io.ktor.server.resources.options +import io.ktor.server.resources.get +import io.ktor.server.resources.post +import io.ktor.server.resources.put +import io.ktor.server.resources.delete +import io.ktor.server.resources.head +import io.ktor.server.resources.patch +import io.ktor.server.routing.* +import io.ktor.server.request.* +import io.ktor.http.content.* +import org.openapitools.server.infrastructure.delegates +import org.openapitools.server.infrastructure.BadParameterException +import org.openapitools.server.infrastructure.APINotImplementedException +import org.openapitools.server.infrastructure.ApiPrincipal +import org.openapitools.server.models.ModelApiResponse +import org.openapitools.server.models.Pet + +fun Route.PetApi() { + authenticate("petstore_auth") { + post { addPet -> + val petApiDelegate: PetApiDelegate? by call.delegates + val pet = call.receive() + + + pet.validate() + + + val delegate = petApiDelegate ?: throw APINotImplementedException("API operation addPet has not been implemented yet.") + val result = delegate.addPet(pet, call) + call.respond(result) + + + } + } + authenticate("petstore_auth") { + delete { deletePet -> + val petApiDelegate: PetApiDelegate? by call.delegates + val petId = deletePet.petId + val apiKey = call.request.headers["api_key"] + + + + val delegate = petApiDelegate ?: throw APINotImplementedException("API operation deletePet has not been implemented yet.") + val result = delegate.deletePet(petId, apiKey, call) + call.respond(result) + + + } + } + authenticate("petstore_auth") { + get { findPetsByStatus -> + val petApiDelegate: PetApiDelegate? by call.delegates + val status = findPetsByStatus.status + + + + val delegate = petApiDelegate ?: throw APINotImplementedException("API operation findPetsByStatus has not been implemented yet.") + val result = delegate.findPetsByStatus(status, call) + call.respond(result) + + + } + } + authenticate("petstore_auth") { + get { findPetsByTags -> + val petApiDelegate: PetApiDelegate? by call.delegates + val tags = findPetsByTags.tags + + + + val delegate = petApiDelegate ?: throw APINotImplementedException("API operation findPetsByTags has not been implemented yet.") + val result = delegate.findPetsByTags(tags, call) + call.respond(result) + + + } + } + authenticate("api_key") { + get { getPetById -> + val petApiDelegate: PetApiDelegate? by call.delegates + val petId = getPetById.petId + + + + val delegate = petApiDelegate ?: throw APINotImplementedException("API operation getPetById has not been implemented yet.") + val result = delegate.getPetById(petId, call) + call.respond(result) + + + } + } + authenticate("petstore_auth") { + put { updatePet -> + val petApiDelegate: PetApiDelegate? by call.delegates + val pet = call.receive() + + + pet.validate() + + + val delegate = petApiDelegate ?: throw APINotImplementedException("API operation updatePet has not been implemented yet.") + val result = delegate.updatePet(pet, call) + call.respond(result) + + + } + } + authenticate("petstore_auth") { + post { updatePetWithForm -> + val petApiDelegate: PetApiDelegate? by call.delegates + val formParameters = call.receiveParameters() + val petId = updatePetWithForm.petId + val name = formParameters["name"] + val status = formParameters["status"] + + + + val delegate = petApiDelegate ?: throw APINotImplementedException("API operation updatePetWithForm has not been implemented yet.") + val result = delegate.updatePetWithForm(petId, name, status, call) + call.respond(result) + + + } + } + authenticate("petstore_auth") { + post { uploadFile -> + val petApiDelegate: PetApiDelegate? by call.delegates + val petId = uploadFile.petId + + val uploadFileMultipartReceiver = PetApiDelegate.UploadFileMultiPartReceiver { block -> + val builder = PetApiDelegate.UploadFileMultiPartFileBuilder() + with(builder) { block() } + call.receiveMultipart().forEachPart { part -> + when (part.name) { + "additionalMetadata" -> { + if(part is PartData.FormItem) { + builder.additionalMetadata(part.value) + } + } + "file" -> { + if(part is PartData.FileItem) { + builder.onReceiveFile(part) + } + } + } + part.dispose() + } + builder.build() + } + + + val delegate = petApiDelegate ?: throw APINotImplementedException("API operation uploadFile has not been implemented yet.") + val result = delegate.uploadFile(petId, uploadFileMultipartReceiver, call) + call.respond(result) + + + } + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/PetApiDelegate.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/PetApiDelegate.kt new file mode 100644 index 000000000000..195b6f1ccb78 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/PetApiDelegate.kt @@ -0,0 +1,114 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.apis + +import io.ktor.http.content.PartData +import io.ktor.server.routing.RoutingCall +import org.openapitools.server.infrastructure.ApiPrincipal +import org.openapitools.server.infrastructure.APINotImplementedException +import org.openapitools.server.infrastructure.BadParameterException +import org.openapitools.server.models.ModelApiResponse +import org.openapitools.server.models.Pet + +interface PetApiDelegate { + + /** + * Add a new pet to the store + * + * @param pet Pet object that needs to be added to the store + * @param call The Ktor [RoutingCall] + * @return Pet + */ + suspend fun addPet( pet: Pet, call:RoutingCall) : Pet = throw APINotImplementedException("API operation addPet has not been implemented yet.") + /** + * Deletes a pet + * + * @param petId Pet id to delete + * @param apiKey + * @param call The Ktor [RoutingCall] + * @return Unit + */ + suspend fun deletePet( petId: kotlin.Long, apiKey: kotlin.String?, call:RoutingCall) : Unit = throw APINotImplementedException("API operation deletePet has not been implemented yet.") + /** + * Finds Pets by status + * Multiple status values can be provided with comma separated strings + * @param status Status values that need to be considered for filter + * @param call The Ktor [RoutingCall] + * @return List + */ + suspend fun findPetsByStatus( status: kotlin.collections.List, call:RoutingCall) : List = throw APINotImplementedException("API operation findPetsByStatus has not been implemented yet.") + /** + * Finds Pets by tags + * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + * @param tags Tags to filter by + * @param call The Ktor [RoutingCall] + * @return List + */ + suspend fun findPetsByTags( tags: kotlin.collections.List, call:RoutingCall) : List = throw APINotImplementedException("API operation findPetsByTags has not been implemented yet.") + /** + * Find pet by ID + * Returns a single pet + * @param petId ID of pet to return + * @param call The Ktor [RoutingCall] + * @return Pet + */ + suspend fun getPetById( petId: kotlin.Long, call:RoutingCall) : Pet = throw APINotImplementedException("API operation getPetById has not been implemented yet.") + /** + * Update an existing pet + * + * @param pet Pet object that needs to be added to the store + * @param call The Ktor [RoutingCall] + * @return Pet + */ + suspend fun updatePet( pet: Pet, call:RoutingCall) : Pet = throw APINotImplementedException("API operation updatePet has not been implemented yet.") + /** + * Updates a pet in the store with form data + * + * @param petId ID of pet that needs to be updated + * @param name Updated name of the pet + * @param status Updated status of the pet + * @param call The Ktor [RoutingCall] + * @return Unit + */ + suspend fun updatePetWithForm( petId: kotlin.Long, name: kotlin.String?, status: kotlin.String?, call:RoutingCall) : Unit = throw APINotImplementedException("API operation updatePetWithForm has not been implemented yet.") + data class UploadFileMultiPart(val additionalMetadata: kotlin.String?, ) + class UploadFileMultiPartFileBuilder{ + + private var additionalMetadata: kotlin.String? = null + + private var _onReceiveFile: suspend PartData.FileItem.() -> Unit = {} + val onReceiveFile: suspend PartData.FileItem.() -> Unit + get() = _onReceiveFile + + + fun additionalMetadata(additionalMetadata: kotlin.String?) { this.additionalMetadata = additionalMetadata } + + fun onReceiveFile(block: suspend PartData.FileItem.() -> Unit = {}) { _onReceiveFile = block } + + fun build(): UploadFileMultiPart { + + return UploadFileMultiPart(additionalMetadata, ) + } + } + fun interface UploadFileMultiPartReceiver{ + suspend fun receiveMultipart(block: UploadFileMultiPartFileBuilder.() -> Unit): UploadFileMultiPart + } + /** + * uploads an image + * + * @param petId ID of pet to update + * @param uploadFileMultipartReceiver interface to receive multipart data + * @param call The Ktor [RoutingCall] + * @return ModelApiResponse + */ + suspend fun uploadFile( petId: kotlin.Long, uploadFileMultipartReceiver: UploadFileMultiPartReceiver, call:RoutingCall) : ModelApiResponse = throw APINotImplementedException("API operation uploadFile has not been implemented yet.") +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt new file mode 100644 index 000000000000..277e7a96d28f --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt @@ -0,0 +1,106 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.apis + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.response.* +import org.openapitools.server.Paths +import io.ktor.server.resources.options +import io.ktor.server.resources.get +import io.ktor.server.resources.post +import io.ktor.server.resources.put +import io.ktor.server.resources.delete +import io.ktor.server.resources.head +import io.ktor.server.resources.patch +import io.ktor.server.routing.* +import io.ktor.server.request.* +import io.ktor.http.content.* +import org.openapitools.server.infrastructure.delegates +import org.openapitools.server.infrastructure.BadParameterException +import org.openapitools.server.infrastructure.APINotImplementedException +import org.openapitools.server.infrastructure.ApiPrincipal +import org.openapitools.server.models.Order + +fun Route.StoreApi() { + delete { deleteOrder -> + val storeApiDelegate: StoreApiDelegate? by call.delegates + val orderId = deleteOrder.orderId + + + + val delegate = storeApiDelegate ?: throw APINotImplementedException("API operation deleteOrder has not been implemented yet.") + val result = delegate.deleteOrder(orderId, call) + call.respond(result) + + + } + authenticate("api_key") { + get { getInventory -> + val storeApiDelegate: StoreApiDelegate? by call.delegates + + + + val delegate = storeApiDelegate ?: throw APINotImplementedException("API operation getInventory has not been implemented yet.") + val result = delegate.getInventory(call) + call.respond(result) + + + } + } + get { getOrderById -> + val storeApiDelegate: StoreApiDelegate? by call.delegates + val orderId = getOrderById.orderId + + + if (orderId < 1) { + throw BadParameterException( + message = "Path parameter orderId should be >= 1", + parameterName = "orderId", + validationType = "minimum", + expectedValue = 1, + actualValue = orderId + ) + } + if (orderId > 5) { + throw BadParameterException( + message = "Path parameter orderId should be <= 5", + parameterName = "orderId", + validationType = "maximum", + expectedValue = 5, + actualValue = orderId + ) + } + + + val delegate = storeApiDelegate ?: throw APINotImplementedException("API operation getOrderById has not been implemented yet.") + val result = delegate.getOrderById(orderId, call) + call.respond(result) + + + } + post { placeOrder -> + val storeApiDelegate: StoreApiDelegate? by call.delegates + val order = call.receive() + + + order.validate() + + + val delegate = storeApiDelegate ?: throw APINotImplementedException("API operation placeOrder has not been implemented yet.") + val result = delegate.placeOrder(order, call) + call.respond(result) + + + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/StoreApiDelegate.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/StoreApiDelegate.kt new file mode 100644 index 000000000000..d2ef7c0ca81e --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/StoreApiDelegate.kt @@ -0,0 +1,54 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.apis + +import io.ktor.http.content.PartData +import io.ktor.server.routing.RoutingCall +import org.openapitools.server.infrastructure.ApiPrincipal +import org.openapitools.server.infrastructure.APINotImplementedException +import org.openapitools.server.infrastructure.BadParameterException +import org.openapitools.server.models.Order + +interface StoreApiDelegate { + + /** + * Delete purchase order by ID + * For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors + * @param orderId ID of the order that needs to be deleted + * @param call The Ktor [RoutingCall] + * @return Unit + */ + suspend fun deleteOrder( orderId: kotlin.String, call:RoutingCall) : Unit = throw APINotImplementedException("API operation deleteOrder has not been implemented yet.") + /** + * Returns pet inventories by status + * Returns a map of status codes to quantities + * @param call The Ktor [RoutingCall] + * @return Map + */ + suspend fun getInventory(call:RoutingCall) : Map = throw APINotImplementedException("API operation getInventory has not been implemented yet.") + /** + * Find purchase order by ID + * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions + * @param orderId ID of pet that needs to be fetched + * @param call The Ktor [RoutingCall] + * @return Order + */ + suspend fun getOrderById( orderId: kotlin.Long, call:RoutingCall) : Order = throw APINotImplementedException("API operation getOrderById has not been implemented yet.") + /** + * Place an order for a pet + * + * @param order order placed for purchasing the pet + * @param call The Ktor [RoutingCall] + * @return Order + */ + suspend fun placeOrder( order: Order, call:RoutingCall) : Order = throw APINotImplementedException("API operation placeOrder has not been implemented yet.") +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/UserApi.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/UserApi.kt new file mode 100644 index 000000000000..ccb27329fb63 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/UserApi.kt @@ -0,0 +1,159 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.apis + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.response.* +import org.openapitools.server.Paths +import io.ktor.server.resources.options +import io.ktor.server.resources.get +import io.ktor.server.resources.post +import io.ktor.server.resources.put +import io.ktor.server.resources.delete +import io.ktor.server.resources.head +import io.ktor.server.resources.patch +import io.ktor.server.routing.* +import io.ktor.server.request.* +import io.ktor.http.content.* +import org.openapitools.server.infrastructure.delegates +import org.openapitools.server.infrastructure.BadParameterException +import org.openapitools.server.infrastructure.APINotImplementedException +import org.openapitools.server.infrastructure.ApiPrincipal +import org.openapitools.server.models.User + +fun Route.UserApi() { + authenticate("api_key") { + post { createUser -> + val userApiDelegate: UserApiDelegate? by call.delegates + val user = call.receive() + + + user.validate() + + + val delegate = userApiDelegate ?: throw APINotImplementedException("API operation createUser has not been implemented yet.") + val result = delegate.createUser(user, call) + call.respond(result) + + + } + } + authenticate("api_key") { + post { createUsersWithArrayInput -> + val userApiDelegate: UserApiDelegate? by call.delegates + val user = call.receive>() + + + + val delegate = userApiDelegate ?: throw APINotImplementedException("API operation createUsersWithArrayInput has not been implemented yet.") + val result = delegate.createUsersWithArrayInput(user, call) + call.respond(result) + + + } + } + authenticate("api_key") { + post { createUsersWithListInput -> + val userApiDelegate: UserApiDelegate? by call.delegates + val user = call.receive>() + + + + val delegate = userApiDelegate ?: throw APINotImplementedException("API operation createUsersWithListInput has not been implemented yet.") + val result = delegate.createUsersWithListInput(user, call) + call.respond(result) + + + } + } + authenticate("api_key") { + delete { deleteUser -> + val userApiDelegate: UserApiDelegate? by call.delegates + val username = deleteUser.username + + + + val delegate = userApiDelegate ?: throw APINotImplementedException("API operation deleteUser has not been implemented yet.") + val result = delegate.deleteUser(username, call) + call.respond(result) + + + } + } + get { getUserByName -> + val userApiDelegate: UserApiDelegate? by call.delegates + val username = getUserByName.username + + + + val delegate = userApiDelegate ?: throw APINotImplementedException("API operation getUserByName has not been implemented yet.") + val result = delegate.getUserByName(username, call) + call.respond(result) + + + } + get { loginUser -> + val userApiDelegate: UserApiDelegate? by call.delegates + val username = loginUser.username + val password = loginUser.password + + + if (!"^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$".toRegex().matches(username.toString())) { + throw BadParameterException( + message = "Query parameter username does not match pattern ^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$", + parameterName = "username", + validationType = "pattern", + expectedValue = "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$", + actualValue = username + ) + } + + + val delegate = userApiDelegate ?: throw APINotImplementedException("API operation loginUser has not been implemented yet.") + val result = delegate.loginUser(username, password, call) + call.respond(result) + + + } + authenticate("api_key") { + get { logoutUser -> + val userApiDelegate: UserApiDelegate? by call.delegates + + + + val delegate = userApiDelegate ?: throw APINotImplementedException("API operation logoutUser has not been implemented yet.") + val result = delegate.logoutUser(call) + call.respond(result) + + + } + } + authenticate("api_key") { + put { updateUser -> + val userApiDelegate: UserApiDelegate? by call.delegates + val username = updateUser.username + val user = call.receive() + + + user.validate() + + + val delegate = userApiDelegate ?: throw APINotImplementedException("API operation updateUser has not been implemented yet.") + val result = delegate.updateUser(username, user, call) + call.respond(result) + + + } + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/UserApiDelegate.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/UserApiDelegate.kt new file mode 100644 index 000000000000..69fa2cc4c8be --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/apis/UserApiDelegate.kt @@ -0,0 +1,88 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.apis + +import io.ktor.http.content.PartData +import io.ktor.server.routing.RoutingCall +import org.openapitools.server.infrastructure.ApiPrincipal +import org.openapitools.server.infrastructure.APINotImplementedException +import org.openapitools.server.infrastructure.BadParameterException +import org.openapitools.server.models.User + +interface UserApiDelegate { + + /** + * Create user + * This can only be done by the logged in user. + * @param user Created user object + * @param call The Ktor [RoutingCall] + * @return Unit + */ + suspend fun createUser( user: User, call:RoutingCall) : Unit = throw APINotImplementedException("API operation createUser has not been implemented yet.") + /** + * Creates list of users with given input array + * + * @param user List of user object + * @param call The Ktor [RoutingCall] + * @return Unit + */ + suspend fun createUsersWithArrayInput( user: kotlin.collections.List, call:RoutingCall) : Unit = throw APINotImplementedException("API operation createUsersWithArrayInput has not been implemented yet.") + /** + * Creates list of users with given input array + * + * @param user List of user object + * @param call The Ktor [RoutingCall] + * @return Unit + */ + suspend fun createUsersWithListInput( user: kotlin.collections.List, call:RoutingCall) : Unit = throw APINotImplementedException("API operation createUsersWithListInput has not been implemented yet.") + /** + * Delete user + * This can only be done by the logged in user. + * @param username The name that needs to be deleted + * @param call The Ktor [RoutingCall] + * @return Unit + */ + suspend fun deleteUser( username: kotlin.String, call:RoutingCall) : Unit = throw APINotImplementedException("API operation deleteUser has not been implemented yet.") + /** + * Get user by user name + * + * @param username The name that needs to be fetched. Use user1 for testing. + * @param call The Ktor [RoutingCall] + * @return User + */ + suspend fun getUserByName( username: kotlin.String, call:RoutingCall) : User = throw APINotImplementedException("API operation getUserByName has not been implemented yet.") + /** + * Logs user into the system + * + * @param username The user name for login + * @param password The password for login in clear text + * @param call The Ktor [RoutingCall] + * @return kotlin.String + */ + suspend fun loginUser( username: kotlin.String, password: kotlin.String, call:RoutingCall) : kotlin.String = throw APINotImplementedException("API operation loginUser has not been implemented yet.") + /** + * Logs out current logged in user session + * + * @param call The Ktor [RoutingCall] + * @return Unit + */ + suspend fun logoutUser(call:RoutingCall) : Unit = throw APINotImplementedException("API operation logoutUser has not been implemented yet.") + /** + * Updated user + * This can only be done by the logged in user. + * @param username name that need to be deleted + * @param user Updated user object + * @param call The Ktor [RoutingCall] + * @return Unit + */ + suspend fun updateUser( username: kotlin.String, user: User, call:RoutingCall) : Unit = throw APINotImplementedException("API operation updateUser has not been implemented yet.") +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/APINotImplementedException.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/APINotImplementedException.kt new file mode 100644 index 000000000000..d799c83192ae --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/APINotImplementedException.kt @@ -0,0 +1,25 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.infrastructure + +import io.ktor.http.HttpStatusCode + +/** + * Exception thrown when an API operation has not been implemented yet. + * + * @param message The error message detailing the missing implementation. + * @param statusCode The HTTP status code to respond with, defaults to [HttpStatusCode.NotImplemented]. + */ +class APINotImplementedException( + message: String = "This API operation has not been implemented yet.", + val statusCode: HttpStatusCode = HttpStatusCode.NotImplemented +) : Exception(message) diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/ApiKeyAuth.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/ApiKeyAuth.kt new file mode 100644 index 000000000000..c2cad7fe7a7f --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/ApiKeyAuth.kt @@ -0,0 +1,102 @@ +package org.openapitools.server.infrastructure + +import io.ktor.http.auth.* +import io.ktor.server.application.* +import io.ktor.server.auth.* +import io.ktor.server.request.* +import io.ktor.server.response.* + +enum class ApiKeyLocation(val location: String) { + QUERY("query"), + HEADER("header") +} + +data class ApiKeyCredential(val value: String) : Credential +data class ApiPrincipal(val apiKeyCredential: ApiKeyCredential?) : Principal + +/** +* Represents an Api Key authentication provider +*/ +class ApiKeyAuthenticationProvider(configuration: Configuration) : AuthenticationProvider(configuration) { + + private val authenticationFunction = configuration.authenticationFunction + + private val apiKeyName: String = configuration.apiKeyName + + private val apiKeyLocation: ApiKeyLocation = configuration.apiKeyLocation + + override suspend fun onAuthenticate(context: AuthenticationContext) { + val call = context.call + val credentials = call.request.apiKeyAuthenticationCredentials(apiKeyName, apiKeyLocation) + val principal = credentials?.let { authenticationFunction.invoke(call, it) } + + val cause = when { + credentials == null -> AuthenticationFailedCause.NoCredentials + principal == null -> AuthenticationFailedCause.InvalidCredentials + else -> null + } + + if (cause != null) { + context.challenge(apiKeyName, cause) { challenge, call -> + call.respond( + UnauthorizedResponse( + HttpAuthHeader.Parameterized( + "API_KEY", + mapOf("key" to apiKeyName), + HeaderValueEncoding.QUOTED_ALWAYS + ) + ) + ) + challenge.complete() + } + } + + if (principal != null) { + context.principal(principal) + } + } + + class Configuration internal constructor(name: String?) : Config(name) { + + internal var authenticationFunction: suspend ApplicationCall.(ApiKeyCredential) -> Principal? = { + throw NotImplementedError( + "Api Key auth validate function is not specified. Use apiKeyAuth { validate { ... } } to fix." + ) + } + + var apiKeyName: String = "" + + var apiKeyLocation: ApiKeyLocation = ApiKeyLocation.QUERY + + /** + * Sets a validation function that will check given [ApiKeyCredential] instance and return [Principal], + * or null if credential does not correspond to an authenticated principal + */ + fun validate(body: suspend ApplicationCall.(ApiKeyCredential) -> Principal?) { + authenticationFunction = body + } + } +} + +fun AuthenticationConfig.apiKeyAuth( + name: String? = null, + configure: ApiKeyAuthenticationProvider.Configuration.() -> Unit +) { + val configuration = ApiKeyAuthenticationProvider.Configuration(name).apply(configure) + val provider = ApiKeyAuthenticationProvider(configuration) + register(provider) +} + +fun ApplicationRequest.apiKeyAuthenticationCredentials( + apiKeyName: String, + apiKeyLocation: ApiKeyLocation +): ApiKeyCredential? { + val value: String? = when (apiKeyLocation) { + ApiKeyLocation.QUERY -> this.queryParameters[apiKeyName] + ApiKeyLocation.HEADER -> this.headers[apiKeyName] + } + return when (value) { + null -> null + else -> ApiKeyCredential(value) + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/AppDelegates.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/AppDelegates.kt new file mode 100644 index 000000000000..ab329af843f4 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/AppDelegates.kt @@ -0,0 +1,32 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.infrastructure + +import io.ktor.util.* +import org.openapitools.server.apis.PetApiDelegate +import org.openapitools.server.apis.StoreApiDelegate +import org.openapitools.server.apis.UserApiDelegate + + +/** + * Data class to hold all API delegates. + */ +data class AppDelegates( + val petApiDelegate: PetApiDelegate? = null, + val storeApiDelegate: StoreApiDelegate? = null, + val userApiDelegate: UserApiDelegate? = null + +) { + companion object { + val AttributeKey = AttributeKey("AppDelegates") + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/BadParameterException.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/BadParameterException.kt new file mode 100644 index 000000000000..d2df939d7ecf --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/BadParameterException.kt @@ -0,0 +1,33 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.infrastructure + +import io.ktor.http.HttpStatusCode + +/** + * Exception thrown when a request contains invalid parameters or fails validation. + * + * @param message The error message detailing the validation failure. + * @param statusCode The HTTP status code to respond with, defaults to [HttpStatusCode.BadRequest]. + * @param parameterName The name of the parameter that failed validation. + * @param validationType The type of validation that failed (e.g., "pattern", "minimum"). + * @param expectedValue The expected value or constraint that was not met. + * @param actualValue The actual value that failed validation. + */ +class BadParameterException( + message: String, + val statusCode: HttpStatusCode = HttpStatusCode.BadRequest, + val parameterName: String? = null, + val validationType: String? = null, + val expectedValue: Any? = null, + val actualValue: Any? = null +) : Exception(message) diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/BadRequestException.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/BadRequestException.kt new file mode 100644 index 000000000000..21ae833f8d19 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/BadRequestException.kt @@ -0,0 +1,33 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.infrastructure + +import io.ktor.http.HttpStatusCode + +/** + * Exception thrown when a request contains invalid parameters or fails validation. + * + * @param message The error message detailing the validation failure. + * @param statusCode The HTTP status code to respond with, defaults to [HttpStatusCode.BadRequest]. + * @param parameterName The name of the parameter that failed validation. + * @param validationType The type of validation that failed (e.g., "pattern", "minimum"). + * @param expectedValue The expected value or constraint that was not met. + * @param actualValue The actual value that failed validation. + */ +class BadRequestException( + message: String, + val statusCode: HttpStatusCode = HttpStatusCode.BadRequest, + val parameterName: String? = null, + val validationType: String? = null, + val expectedValue: Any? = null, + val actualValue: Any? = null +) : Exception(message) diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/Delegates.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/Delegates.kt new file mode 100644 index 000000000000..219180eeb752 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/Delegates.kt @@ -0,0 +1,57 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.infrastructure + +import io.ktor.server.application.* +import io.ktor.server.routing.* +import io.ktor.util.* +import kotlin.reflect.KProperty +import org.openapitools.server.apis.PetApiDelegate +import org.openapitools.server.apis.StoreApiDelegate +import org.openapitools.server.apis.UserApiDelegate + +/** + * Extension for [Application] to register delegates. + */ +fun Application.delegates(appDelegates: AppDelegates) { + attributes.put(AppDelegates.AttributeKey, appDelegates) +} + +/** + * Extension for [RoutingCall] to register delegates. + */ +fun RoutingCall.delegates(appDelegates: AppDelegates) { + attributes.put(AppDelegates.AttributeKey, appDelegates) +} + +/** + * Extension for [RoutingContext] to support delegate injection. + */ +inline val RoutingCall.delegates: DelegateProvider + get() = DelegateProvider(this) + +@JvmInline +value class DelegateProvider(val call: RoutingCall) { + @Suppress("UNCHECKED_CAST") + inline operator fun getValue(thisRef: Any?, property: KProperty<*>): T? { + val appDelegates = call.application.attributes.getOrNull(AppDelegates.AttributeKey) + ?: call.attributes.getOrNull(AppDelegates.AttributeKey) + + return when (T::class) { + PetApiDelegate::class -> appDelegates?.petApiDelegate as T + StoreApiDelegate::class -> appDelegates?.storeApiDelegate as T + UserApiDelegate::class -> appDelegates?.userApiDelegate as T + + else -> null as T + } + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Cat.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Cat.kt new file mode 100644 index 000000000000..06f120e30463 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Cat.kt @@ -0,0 +1,47 @@ +/** +* Polymorphism example with allOf and discriminator +* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) +* +* The version of the OpenAPI document: 1.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.models + +import org.openapitools.server.models.Pet + +import kotlinx.serialization.Serializable +import io.ktor.http.Url +import kotlin.uuid.Uuid +import org.openapitools.server.infrastructure.BadParameterException +/** + * A representation of a cat + * @param huntingSkill The measured skill for hunting + */ +@Serializable +data class Cat( + /* The measured skill for hunting */ + val huntingSkill: Cat.HuntingSkill, + val name: kotlin.String, + val petType: kotlin.String +) +{ + /** + * The measured skill for hunting + * Values: clueless,lazy,adventurous,aggressive + */ + enum class HuntingSkill(val value: kotlin.String){ + clueless("clueless"), + lazy("lazy"), + adventurous("adventurous"), + aggressive("aggressive"); + } + fun validate() { + + } +} + + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Category.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Category.kt new file mode 100644 index 000000000000..7faa41bbab38 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Category.kt @@ -0,0 +1,42 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.models + + +import kotlinx.serialization.Serializable +import org.openapitools.server.infrastructure.BadParameterException +/** + * A category for a pet + * @param id + * @param name + */ +@Serializable +data class Category( + val id: kotlin.Long? = null, + val name: kotlin.String? = null +) +{ + fun validate() { + + if (!"^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$".toRegex().matches(name.toString())) { + throw BadParameterException( + message = "Property name does not match pattern ^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$", + parameterName = "name", + validationType = "pattern", + expectedValue = "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$", + actualValue = name + ) + } + + } +} + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Dog.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Dog.kt new file mode 100644 index 000000000000..8c61f06b9fd1 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Dog.kt @@ -0,0 +1,46 @@ +/** +* Polymorphism example with allOf and discriminator +* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) +* +* The version of the OpenAPI document: 1.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.models + +import org.openapitools.server.models.Pet + +import kotlinx.serialization.Serializable +import io.ktor.http.Url +import kotlin.uuid.Uuid +import org.openapitools.server.infrastructure.BadParameterException +/** + * A representation of a dog + * @param packSize the size of the pack the dog is from + */ +@Serializable +data class Dog( + /* the size of the pack the dog is from */ + val packSize: kotlin.Int = 0, + val name: kotlin.String, + val petType: kotlin.String +) +{ + fun validate() { + if (packSize < 0) { + throw BadParameterException( + message = "Property packSize should be >= 0", + parameterName = "packSize", + validationType = "minimum", + expectedValue = 0, + actualValue = packSize + ) + } + + } +} + + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/ModelApiResponse.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/ModelApiResponse.kt new file mode 100644 index 000000000000..d1f0e8b4b276 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/ModelApiResponse.kt @@ -0,0 +1,36 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.models + + +import kotlinx.serialization.Serializable +import org.openapitools.server.infrastructure.BadParameterException +/** + * Describes the result of uploading an image resource + * @param code + * @param type + * @param message + */ +@Serializable +data class ModelApiResponse( + val code: kotlin.Int? = null, + val type: kotlin.String? = null, + val message: kotlin.String? = null +) +{ + fun validate() { + + + + } +} + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Order.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Order.kt new file mode 100644 index 000000000000..da42f1f45117 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Order.kt @@ -0,0 +1,55 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.models + + +import kotlinx.serialization.Serializable +import org.openapitools.server.infrastructure.BadParameterException +/** + * An order for a pets from the pet store + * @param id + * @param petId + * @param quantity + * @param shipDate + * @param status Order Status + * @param complete + */ +@Serializable +data class Order( + val id: kotlin.Long? = null, + val petId: kotlin.Long? = null, + val quantity: kotlin.Int? = null, + val shipDate: kotlin.String? = null, + /* Order Status */ + val status: Order.Status? = null, + val complete: kotlin.Boolean? = false +) +{ + /** + * Order Status + * Values: placed,approved,delivered + */ + enum class Status(val value: kotlin.String){ + placed("placed"), + approved("approved"), + delivered("delivered"); + } + fun validate() { + + + + + + + } +} + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Pet.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Pet.kt new file mode 100644 index 000000000000..d287576682c9 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Pet.kt @@ -0,0 +1,61 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.models + +import org.openapitools.server.models.Category +import org.openapitools.server.models.Tag + +import kotlinx.serialization.Serializable +import org.openapitools.server.infrastructure.BadParameterException +/** + * A pet for sale in the pet store + * @param name + * @param photoUrls + * @param id + * @param category + * @param tags + * @param status pet status in the store + */ +@Serializable +data class Pet( + val name: kotlin.String, + val photoUrls: kotlin.collections.List, + val id: kotlin.Long? = null, + val category: Category? = null, + val tags: kotlin.collections.List? = null, + /* pet status in the store */ + val status: Pet.Status? = null +) +{ + /** + * pet status in the store + * Values: available,pending,sold + */ + enum class Status(val value: kotlin.String){ + available("available"), + pending("pending"), + sold("sold"); + } + fun validate() { + + photoUrls.forEach { + } + + + category?.validate() + + tags?.forEach { it.validate() } + + + } +} + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Tag.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Tag.kt new file mode 100644 index 000000000000..fdd68ad0dc95 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/Tag.kt @@ -0,0 +1,33 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.models + + +import kotlinx.serialization.Serializable +import org.openapitools.server.infrastructure.BadParameterException +/** + * A tag for a pet + * @param id + * @param name + */ +@Serializable +data class Tag( + val id: kotlin.Long? = null, + val name: kotlin.String? = null +) +{ + fun validate() { + + + } +} + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/User.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/User.kt new file mode 100644 index 000000000000..f1f74870cbb2 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/models/User.kt @@ -0,0 +1,52 @@ +/** +* OpenAPI Petstore +* This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. +* +* The version of the OpenAPI document: 1.0.0 +* +* +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package org.openapitools.server.models + + +import kotlinx.serialization.Serializable +import org.openapitools.server.infrastructure.BadParameterException +/** + * A User who is purchasing from the pet store + * @param id + * @param username + * @param firstName + * @param lastName + * @param email + * @param password + * @param phone + * @param userStatus User Status + */ +@Serializable +data class User( + val id: kotlin.Long? = null, + val username: kotlin.String? = null, + val firstName: kotlin.String? = null, + val lastName: kotlin.String? = null, + val email: kotlin.String? = null, + val password: kotlin.String? = null, + val phone: kotlin.String? = null, + /* User Status */ + val userStatus: kotlin.Int? = null +) +{ + fun validate() { + + + + + + + + + } +} + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/application.conf b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/application.conf new file mode 100644 index 000000000000..d33fe93f9937 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/application.conf @@ -0,0 +1,23 @@ +ktor { + deployment { + environment = development + port = 8080 + autoreload = true + watch = [ org.openapitools.server ] + } + + application { + modules = [ org.openapitools.server.AppMainKt.main ] + } +} + +# Typesafe config allows multiple ways to provide configuration values without hard-coding them here. +# Please see https://github.com/lightbend/config for details. +auth { + oauth { + petstore_auth { + clientId = "" + clientSecret = "" + } + } +} \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/logback.xml b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/logback.xml new file mode 100644 index 000000000000..d0eaba8debd6 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/gradle/libs.versions.toml b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/gradle/libs.versions.toml new file mode 100644 index 000000000000..4f4783e1b339 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/gradle/libs.versions.toml @@ -0,0 +1,21 @@ +[versions] +kotlin = "2.3.0" +ktor = "3.4.1" +logback = "1.4.14" + +[libraries] +ktor-server-di = { module = "io.ktor:ktor-server-di", version.ref = "ktor" } +ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" } +ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-server-resources = { module = "io.ktor:ktor-server-resources", version.ref = "ktor" } +ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml", version.ref = "ktor" } +ktor-server-test-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor" } +kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +ktor = { id = "io.ktor.plugin", version.ref = "ktor" } +kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/pom.xml b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/pom.xml new file mode 100644 index 000000000000..b8a9efadcc06 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/pom.xml @@ -0,0 +1,206 @@ + + + 4.0.0 + org.openapitools + server-ktor-delegate-pattern + 0.0.1 + server + server + + official + 2.3.0 + 3.4.3 + 1.4.14 + 2.0.9 + UTF-8 + true + io.ktor.server.netty.EngineMain + + + + + + io.ktor + ktor-server-di-jvm + ${ktor_version} + + + io.ktor + ktor-server-content-negotiation-jvm + ${ktor_version} + + + io.ktor + ktor-server-core-jvm + ${ktor_version} + + + io.ktor + ktor-serialization-kotlinx-json-jvm + ${ktor_version} + + + io.ktor + ktor-server-resources-jvm + ${ktor_version} + + + io.ktor + ktor-server-status-pages-jvm + ${ktor_version} + + + io.ktor + ktor-server-netty-jvm + ${ktor_version} + + + ch.qos.logback + logback-classic + ${logback_version} + + + org.slf4j + slf4j-api + ${slf4j_version} + + + io.ktor + ktor-server-config-yaml-jvm + ${ktor_version} + + + io.ktor + ktor-server-auth-jvm + ${ktor_version} + + + io.ktor + ktor-server-test-host-jvm + ${ktor_version} + test + + + org.jetbrains.kotlin + kotlin-test-junit + ${kotlin_version} + test + + + org.jetbrains.kotlinx + kotlinx-coroutines-debug + 1.6.4 + test + + + io.ktor + ktor-client-content-negotiation-jvm + ${ktor_version} + test + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + ${project.basedir}/src/main/resources + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.2.0 + + + generate-sources + + add-source + + + + ${project.basedir}/generated/src/main/kotlin + + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + + + java + + + + + ${main.class} + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + jar-with-dependencies + + + + true + ${main.class} + + + + + + assemble-all + package + + single + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin_version} + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + kotlinx-serialization + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin_version} + + + + + + \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Application.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Application.kt new file mode 100644 index 000000000000..dc9b318fe1ac --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Application.kt @@ -0,0 +1,14 @@ +package org.openapitools.server + +import io.ktor.server.application.* +import io.ktor.server.netty.EngineMain + +fun main(args: Array) { + EngineMain.main(args) +} + +fun Application.module() { + configureFrameworks() + configureSerialization() + configureRouting() +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Frameworks.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Frameworks.kt new file mode 100644 index 000000000000..c416c88daea9 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Frameworks.kt @@ -0,0 +1,10 @@ +package org.openapitools.server + +import io.ktor.server.application.* +import io.ktor.server.plugins.di.* + +fun Application.configureFrameworks() { + dependencies { + provide { GreetingService { "Hello, World!" } } + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/GreetingService.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/GreetingService.kt new file mode 100644 index 000000000000..5fd54dd83399 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/GreetingService.kt @@ -0,0 +1,5 @@ +package org.openapitools.server + +fun interface GreetingService { + fun sayHello(): String +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Routing.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Routing.kt new file mode 100644 index 000000000000..dc3832cf38d3 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Routing.kt @@ -0,0 +1,25 @@ +package org.openapitools.server + +import io.ktor.resources.* +import io.ktor.server.application.* +import io.ktor.server.resources.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.server.routing.get +import kotlinx.serialization.Serializable + +fun Application.configureRouting() { + install(Resources) + routing { + get("/") { + call.respondText("Hello World!") + } + get { article -> + // Get all articles ... + call.respond("List of articles sorted starting from ${article.sort}") + } + } +} +@Serializable +@Resource("/articles") +class Articles(val sort: String? = "new") diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Serialization.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Serialization.kt new file mode 100644 index 000000000000..38fab527120c --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/kotlin/org/openapitools/server/Serialization.kt @@ -0,0 +1,18 @@ +package org.openapitools.server + +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun Application.configureSerialization() { + install(ContentNegotiation) { + json() + } + routing { + get("/json/kotlinx-serialization") { + call.respond(mapOf("hello" to "world")) + } + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/application.yaml b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/application.yaml new file mode 100644 index 000000000000..e107d183aff2 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/application.yaml @@ -0,0 +1,6 @@ +ktor: + application: + modules: + - org.openapitools.ApplicationKt.module + deployment: + port: 8080 diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/logback.xml b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/logback.xml new file mode 100644 index 000000000000..aadef5d5ba2a --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/ApplicationTest.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/ApplicationTest.kt new file mode 100644 index 000000000000..b5546830e818 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/ApplicationTest.kt @@ -0,0 +1,21 @@ +package org.openapitools.server + +import io.ktor.client.request.get +import io.ktor.http.HttpStatusCode +import io.ktor.server.testing.testApplication +import kotlin.test.Test +import kotlin.test.assertEquals + +class ApplicationTest { + + @Test + fun testRoot() = testApplication { + application { + module() + } + client.get("/").apply { + assertEquals(HttpStatusCode.Companion.OK, status) + } + } + +} \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/PetApiTest.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/PetApiTest.kt new file mode 100644 index 000000000000..3fccb960af69 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/PetApiTest.kt @@ -0,0 +1,419 @@ +package org.openapitools.server + +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.request.forms.* +import io.ktor.http.* +import io.ktor.server.plugins.di.* +import io.ktor.server.routing.* +import io.ktor.utils.io.asByteWriteChannel +import io.ktor.utils.io.copyAndClose +import io.ktor.utils.io.copyTo +import io.ktor.utils.io.toByteArray +import kotlinx.io.Buffer +import kotlinx.io.readString +import org.openapitools.server.apis.PetApiDelegate +import org.openapitools.server.apis.PetApiDelegate.UploadFileMultiPartReceiver +import org.openapitools.server.models.Category +import org.openapitools.server.models.ModelApiResponse +import org.openapitools.server.models.Pet +import kotlin.test.Test +import kotlin.test.assertNull +import kotlin.test.assertEquals + +class PetApiTest { + + @Test + fun testAddPetShouldAddPet() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + val pet = Pet(name = "test", photoUrls = listOf("test.png"), category = Category(id = 1, name = "test")) + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody(pet) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(pet, receivedPet) + assertEquals(pet, response.body()) + } + + @Test + fun testAddPetShouldAddPetWithStatus201() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + call.response.status(HttpStatusCode.Created) + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + val pet = Pet(name = "test", photoUrls = listOf("test.png"), category = Category(id = 1, name = "test")) + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody(pet) + } + assertEquals(HttpStatusCode.Created, response.status) + assertEquals(pet, receivedPet) + assertEquals(pet, response.body()) + } + + @Test + fun testAddPetShouldValidateCategoryName() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + + val pet = Pet(name = "test", photoUrls = listOf("test.png"), category = Category(id = 1, name = "+test")) + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody(pet) + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedPet) + } + + @Test + fun testAddPetShouldRejectEmptyBody() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody("""{}""") + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedPet) + } + + @Test + fun testAddPetShouldRejectMissingName() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody("""{"photoUrls":["test.png"]}""") + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedPet) + } + + @Test + fun testAddPetShouldRejectMissingPhotoUrls() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody("""{"name":"test"}""") + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedPet) + } + + @Test + fun testAddPetShouldRejectInvalidStatus() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody("""{"name":"test", "photoUrls":["test.png"], "status":"invalid"}""") + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedPet) + } + + @Test + fun testAddPetShouldRejectInvalidPhotoUrls() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody("""{"name":"test", "photoUrls":"invalid"}""") + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedPet) + } + + @Test + fun testAddPetShouldRejectEmptyCategoryName() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody("""{"name":"test", "photoUrls":["test.png"], "category":{"id":1, "name":""}}""") + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedPet) + } + + @Test + fun testAddPetShouldRejectInvalidIdFormat() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody("""{"name":"test", "photoUrls":["test.png"], "id":"not-a-long"}""") + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedPet) + } + + @Test + fun testAddPetShouldRejectInvalidCategoryIdFormat() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun addPet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody("""{"name":"test", "photoUrls":["test.png"], "category":{"id":"not-a-long", "name":"test"}}""") + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedPet) + } + + @Test + fun testAddPetShouldReturnRightStatusCodeWhenNotImplemented() = petstoreTestApplication { + val pet = Pet(name = "test", photoUrls = listOf("test.png"), category = Category(id = 1, name = "test")) + val response = client.post("/pet") { + contentType(ContentType.Application.Json) + setBody(pet) + } + assertEquals(HttpStatusCode.NotImplemented, response.status) + } + + @Test + fun testUpdatePetShouldUpdatePet() = petstoreTestApplication { + var receivedPet: Pet? = null + class PetApiImpl: PetApiDelegate { + override suspend fun updatePet(pet: Pet, call: RoutingCall): Pet { + receivedPet = pet + return pet.copy() + } + } + application.dependencies.provide { PetApiImpl() } + val pet = Pet(name = "test", photoUrls = listOf("test.png"), category = Category(id = 1, name = "test")) + val response = client.put("/pet") { + contentType(ContentType.Application.Json) + setBody(pet) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(pet, receivedPet) + assertEquals(pet, response.body()) + } + + @Test + fun testFindPetsByStatusShouldReturnPets() = petstoreTestApplication { + val pets = listOf(Pet(name = "test", photoUrls = listOf("test.png"))) + var receivedStatuses: List? = null + class PetApiImpl: PetApiDelegate { + override suspend fun findPetsByStatus(status: List, call: RoutingCall): List { + receivedStatuses = status + return pets + } + } + application.dependencies.provide { PetApiImpl() } + val response = client.get("/pet/findByStatus?status=available") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(listOf("available"), receivedStatuses) + assertEquals(pets, response.body>()) + } + + @Test + fun testFindPetsByStatusShouldRejectMissingStatus() = petstoreTestApplication { + val response = client.get("/pet/findByStatus") + assertEquals(HttpStatusCode.BadRequest, response.status) + } + + @Test + fun testFindPetsByStatusShouldRejectInvalidStatus() = petstoreTestApplication { + val response = client.get("/pet/findByStatus?status=invalid") + assertEquals(HttpStatusCode.NotImplemented, response.status) + } + + @Test + fun testFindPetsByTagsShouldReturnPets() = petstoreTestApplication { + val pets = listOf(Pet(name = "test", photoUrls = listOf("test.png"))) + var receivedTags: List?=null + class PetApiImpl: PetApiDelegate { + override suspend fun findPetsByTags(tags: List, call: RoutingCall): List { + receivedTags = tags + return pets + } + } + application.dependencies.provide { PetApiImpl() } + val response = client.get("/pet/findByTags?tags=tag1") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(pets, response.body>()) + assertEquals(listOf("tag1"), receivedTags) + } + + @Test + fun testGetPetByIdShouldReturnPet() = petstoreTestApplication { + val pet = Pet(name = "test", photoUrls = listOf("test.png")) + var receivedPetId: Long? = null + class PetApiImpl: PetApiDelegate { + override suspend fun getPetById(petId: Long, call: RoutingCall): Pet { + receivedPetId = petId + return pet + } + } + application.dependencies.provide { PetApiImpl() } + val response = client.get("/pet/1") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(pet, response.body()) + assertEquals(1L, receivedPetId) + } + + @Test + fun testGetPetByIdShouldRejectInvalidId() = petstoreTestApplication { + val response = client.get("/pet/invalid") + assertEquals(HttpStatusCode.BadRequest, response.status) + } + + @Test + fun testUpdatePetWithFormShouldUpdatePet() = petstoreTestApplication { + var receivedPetId: Long? = null + var receivedName: String? = null + var receivedStatus: String? = null + class PetApiImpl: PetApiDelegate { + override suspend fun updatePetWithForm(petId: Long, name: String?, status: String?, call: RoutingCall) { + receivedPetId = petId + receivedName = name + receivedStatus = status + } + } + application.dependencies.provide { PetApiImpl() } + val response = client.post("/pet/1") { + setBody(FormDataContent(Parameters.build { + append("name", "new name") + append("status", "sold") + })) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(1L, receivedPetId) + assertEquals("new name", receivedName) + assertEquals("sold", receivedStatus) + } + + @Test + fun testDeletePetShouldDeletePet() = petstoreTestApplication { + var receivedPetId: Long? = null + class PetApiImpl: PetApiDelegate { + override suspend fun deletePet(petId: Long, apiKey: String?, call: RoutingCall) { + receivedPetId = petId + } + } + application.dependencies.provide { PetApiImpl() } + val response = client.delete("/pet/1") { + header("api_key", "test-key") + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(1L, receivedPetId) + } + + @Test + fun testUploadFileShouldUploadFile() = petstoreTestApplication { + var receivedPetId: Long? = null + var receivedAdditionalMetadata: String? = null + var receivedFileName: String? = null + var receivedFileContent:String? = null + class PetApiImpl: PetApiDelegate { + override suspend fun uploadFile(petId: kotlin.Long, uploadFileMultipartReceiver: UploadFileMultiPartReceiver, call:RoutingCall): ModelApiResponse { + receivedPetId = petId + val receivedMultipart = uploadFileMultipartReceiver.receiveMultipart{ + onReceiveFile { + receivedFileName = originalFileName + receivedFileContent = provider().toByteArray().decodeToString() + } + } + receivedAdditionalMetadata = receivedMultipart.additionalMetadata + + + return ModelApiResponse(code = 200, message = "ok") + } + } + application.dependencies.provide { PetApiImpl() } + val response = client.submitFormWithBinaryData( + url = "/pet/1/uploadImage", + formData = formData { + append("additionalMetadata", "test metadata") + append("file", "test content".toByteArray(), Headers.build { + append(HttpHeaders.ContentType, "image/png") + append(HttpHeaders.ContentDisposition, "filename=\"test.png\"") + }) + } + ) + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(1L, receivedPetId) + assertEquals("test metadata", receivedAdditionalMetadata) + assertEquals("test.png", receivedFileName) + assertEquals("test content", receivedFileContent) + } +} + + diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/StoreApiTest.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/StoreApiTest.kt new file mode 100644 index 000000000000..6c77146de34d --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/StoreApiTest.kt @@ -0,0 +1,138 @@ +package org.openapitools.server + +import io.ktor.client.call.body +import io.ktor.client.request.* +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.contentType +import io.ktor.server.routing.RoutingCall +import io.ktor.server.plugins.di.dependencies +import org.openapitools.server.apis.StoreApiDelegate +import org.openapitools.server.models.Order +import kotlin.test.Test +import kotlin.test.assertNull +import kotlin.test.assertEquals + +class StoreApiTest { + + @Test + fun testPlaceOrderShouldAddOrder() = petstoreTestApplication { + var receivedOrder: Order? = null + class StoreApiImpl: StoreApiDelegate { + override suspend fun placeOrder(order: Order, call: RoutingCall): Order { + receivedOrder = order + return order.copy() + } + } + application.dependencies.provide { StoreApiImpl() } + val order = Order(id = 1, petId = 1, quantity = 1, status = Order.Status.placed) + val response = client.post("/store/order") { + contentType(ContentType.Application.Json) + setBody(order) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(order, receivedOrder) + assertEquals(order, response.body()) + } + + @Test + fun testPlaceOrderShouldRejectInvalidStatus() = petstoreTestApplication { + var receivedOrder: Order? = null + class StoreApiImpl: StoreApiDelegate { + override suspend fun placeOrder(order: Order, call: RoutingCall): Order { + receivedOrder = order + return order.copy() + } + } + application.dependencies.provide { StoreApiImpl() } + + val response = client.post("/store/order") { + contentType(ContentType.Application.Json) + setBody("""{"id":1, "petId":1, "quantity":1, "status":"invalid"}""") + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedOrder) + } + + @Test + fun testPlaceOrderShouldRejectInvalidQuantityFormat() = petstoreTestApplication { + var receivedOrder: Order? = null + class StoreApiImpl: StoreApiDelegate { + override suspend fun placeOrder(order: Order, call: RoutingCall): Order { + receivedOrder = order + return order.copy() + } + } + application.dependencies.provide { StoreApiImpl() } + + val response = client.post("/store/order") { + contentType(ContentType.Application.Json) + setBody("""{"id":1, "petId":1, "quantity":"not-an-int", "status":"placed"}""") + } + assertEquals(HttpStatusCode.BadRequest, response.status) + assertNull(receivedOrder) + } + + @Test + fun testGetOrderByIdShouldValidateMin() = petstoreTestApplication { + class StoreApiImpl: StoreApiDelegate + application.dependencies.provide { StoreApiImpl() } + + val response = client.get("/store/order/0") + assertEquals(HttpStatusCode.BadRequest, response.status) + } + + @Test + fun testGetOrderByIdShouldValidateMax() = petstoreTestApplication { + class StoreApiImpl: StoreApiDelegate + application.dependencies.provide { StoreApiImpl() } + + val response = client.get("/store/order/6") + assertEquals(HttpStatusCode.BadRequest, response.status) + } + + @Test + fun testGetOrderByIdShouldValidateSuccess() = petstoreTestApplication { + val order = Order(id = 3, petId = 1, quantity = 1, status = Order.Status.placed) + class StoreApiImpl: StoreApiDelegate { + override suspend fun getOrderById(orderId: Long, call: RoutingCall): Order { + return order + } + } + application.dependencies.provide { StoreApiImpl() } + + val response = client.get("/store/order/3") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(order, response.body()) + } + + @Test + fun testGetInventoryShouldReturnInventory() = petstoreTestApplication { + val inventory = mapOf("available" to 10) + class StoreApiImpl: StoreApiDelegate { + override suspend fun getInventory(call: RoutingCall): Map { + return inventory + } + } + application.dependencies.provide { StoreApiImpl() } + + val response = client.get("/store/inventory") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(inventory, response.body>()) + } + + @Test + fun testDeleteOrderShouldDeleteOrder() = petstoreTestApplication { + var receivedOrderId: String? = null + class StoreApiImpl: StoreApiDelegate { + override suspend fun deleteOrder(orderId: String, call: RoutingCall) { + receivedOrderId = orderId + } + } + application.dependencies.provide { StoreApiImpl() } + + val response = client.delete("/store/order/1") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("1", receivedOrderId) + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/TestUtil.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/TestUtil.kt new file mode 100644 index 000000000000..7d5423c6572b --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/TestUtil.kt @@ -0,0 +1,84 @@ +package org.openapitools.server + +import io.ktor.http.HttpStatusCode +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.auth.Authentication +import io.ktor.server.auth.AuthenticationContext +import io.ktor.server.auth.AuthenticationProvider +import io.ktor.server.plugins.BadRequestException +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.plugins.di.dependencies +import io.ktor.server.plugins.statuspages.* +import io.ktor.server.resources.Resources +import io.ktor.server.response.respondText +import io.ktor.server.routing.routing +import io.ktor.server.testing.ApplicationTestBuilder +import io.ktor.server.testing.testApplication +import org.openapitools.server.AllApis +import org.openapitools.server.infrastructure.APINotImplementedException +import org.openapitools.server.infrastructure.AppDelegates +import org.openapitools.server.infrastructure.BadParameterException +import org.openapitools.server.infrastructure.delegates + + +class TestConfig(name: String): AuthenticationProvider.Config(name) + +class TestAuthenticationProvider(testConfig: TestConfig) : AuthenticationProvider(testConfig) { + override suspend fun onAuthenticate(context: AuthenticationContext) { + context.principal(principal = "test") + } +} + +fun petstoreTestApplication(block: suspend ApplicationTestBuilder.() -> kotlin.Unit) { + testApplication { + install(Authentication){ + register(TestAuthenticationProvider(TestConfig(name = "petstore_auth"))) + register(TestAuthenticationProvider(TestConfig(name = "api_key"))) + } + install(ContentNegotiation) { + json() + } + install(Resources) + install(StatusPages) { + exception { call, cause -> + when (cause) { + is BadParameterException -> { + call.respondText( + status = cause.statusCode, text = mapOf( + "message" to (cause.message ?: "Invalid parameter"), + "parameter" to cause.parameterName, + "validation" to cause.validationType, + "expected" to cause.expectedValue, + "actual" to cause.actualValue + ).filterValues { it != null }.mapValues { it.toString() }.toString() + ) + } + + is APINotImplementedException -> { + call.respondText(status = HttpStatusCode.NotImplemented, text = cause.message ?: "API not implemented") + } + + } + } + } + client = createClient { + install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) { + json() + } + } + + application { + dependencies { + provide(AppDelegates::class) + } + + val delegatesImpl: AppDelegates by dependencies + delegates(delegatesImpl) + routing { + AllApis() + } + + } + block() + } +} diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/UserApiTest.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/UserApiTest.kt new file mode 100644 index 000000000000..8c1af2987aa4 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/UserApiTest.kt @@ -0,0 +1,156 @@ +package org.openapitools.server + +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.server.plugins.di.* +import io.ktor.server.routing.* +import org.openapitools.server.apis.UserApiDelegate +import org.openapitools.server.models.User +import kotlin.test.Test +import kotlin.test.assertEquals + +class UserApiTest { + + @Test + fun testCreateUserShouldCreateUser() = petstoreTestApplication { + var receivedUser: User? = null + class UserApiImpl: UserApiDelegate { + override suspend fun createUser(user: User, call: RoutingCall) { + receivedUser = user + } + } + application.dependencies.provide { UserApiImpl() } + val user = User(id = 1, username = "testuser") + val response = client.post("/user") { + contentType(ContentType.Application.Json) + setBody(user) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(user, receivedUser) + } + + @Test + fun testCreateUsersWithArrayInputShouldCreateUsers() = petstoreTestApplication { + var receivedUsers: List? = null + class UserApiImpl: UserApiDelegate { + override suspend fun createUsersWithArrayInput(user: List, call: RoutingCall) { + receivedUsers = user + } + } + application.dependencies.provide { UserApiImpl() } + val users = listOf(User(id = 1, username = "testuser")) + val response = client.post("/user/createWithArray") { + contentType(ContentType.Application.Json) + setBody(users) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(users, receivedUsers) + } + + @Test + fun testCreateUsersWithListInputShouldCreateUsers() = petstoreTestApplication { + var receivedUsers: List? = null + class UserApiImpl: UserApiDelegate { + override suspend fun createUsersWithListInput(user: List, call: RoutingCall) { + receivedUsers = user + } + } + application.dependencies.provide { UserApiImpl() } + val users = listOf(User(id = 1, username = "testuser")) + val response = client.post("/user/createWithList") { + contentType(ContentType.Application.Json) + setBody(users) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(users, receivedUsers) + } + + @Test + fun testLoginUserShouldReturnToken() = petstoreTestApplication { + class UserApiImpl: UserApiDelegate { + override suspend fun loginUser(username: String, password: String, call: RoutingCall): String { + return "test-token" + } + } + application.dependencies.provide { UserApiImpl() } + val response = client.get("/user/login?username=testuser&password=password") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("test-token", response.body()) + } + + @Test + fun testLogoutUserShouldSucceed() = petstoreTestApplication { + var logoutCalled = false + class UserApiImpl: UserApiDelegate { + override suspend fun logoutUser(call: RoutingCall) { + logoutCalled = true + } + } + application.dependencies.provide { UserApiImpl() } + val response = client.get("/user/logout") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(true, logoutCalled) + } + + @Test + fun testGetUserByNameShouldReturnUser() = petstoreTestApplication { + val user = User(id = 1, username = "testuser") + class UserApiImpl: UserApiDelegate { + override suspend fun getUserByName(username: String, call: RoutingCall): User { + return user + } + } + application.dependencies.provide { UserApiImpl() } + val response = client.get("/user/testuser") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(user, response.body()) + } + + @Test + fun testUpdateUserShouldUpdateUser() = petstoreTestApplication { + var receivedUsername: String? = null + var receivedUser: User? = null + class UserApiImpl: UserApiDelegate { + override suspend fun updateUser(username: String, user: User, call: RoutingCall) { + receivedUsername = username + receivedUser = user + } + } + application.dependencies.provide { UserApiImpl() } + val user = User(id = 1, username = "testuser") + val response = client.put("/user/testuser") { + contentType(ContentType.Application.Json) + setBody(user) + } + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("testuser", receivedUsername) + assertEquals(user, receivedUser) + } + + @Test + fun testDeleteUserShouldDeleteUser() = petstoreTestApplication { + var receivedUsername: String? = null + class UserApiImpl: UserApiDelegate { + override suspend fun deleteUser(username: String, call: RoutingCall) { + receivedUsername = username + } + } + application.dependencies.provide { UserApiImpl() } + val response = client.delete("/user/testuser") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("testuser", receivedUsername) + } + + @Test + fun testLoginUserShouldRejectMissingParameters() = petstoreTestApplication { + val response = client.get("/user/login") + assertEquals(HttpStatusCode.NotImplemented, response.status) + } + + @Test + fun testLoginUserShouldRejectInvalidUsername() = petstoreTestApplication { + val response = client.get("/user/login?username=a&password=p") + assertEquals(HttpStatusCode.BadRequest, response.status) + } +} diff --git a/samples/server/petstore/kotlin-server/ktor/.openapi-generator/FILES b/samples/server/petstore/kotlin-server/ktor/.openapi-generator/FILES index 52c6d592e8f2..786a5876752f 100644 --- a/samples/server/petstore/kotlin-server/ktor/.openapi-generator/FILES +++ b/samples/server/petstore/kotlin-server/ktor/.openapi-generator/FILES @@ -4,6 +4,7 @@ build.gradle.kts gradle.properties gradle/wrapper/gradle-wrapper.properties settings.gradle +src/main/kotlin/org/openapitools/server/AllApis.kt src/main/kotlin/org/openapitools/server/AppMain.kt src/main/kotlin/org/openapitools/server/Configuration.kt src/main/kotlin/org/openapitools/server/Paths.kt diff --git a/samples/server/petstore/kotlin-server/ktor/README.md b/samples/server/petstore/kotlin-server/ktor/README.md index 92500a09feda..b371e659e03e 100644 --- a/samples/server/petstore/kotlin-server/ktor/README.md +++ b/samples/server/petstore/kotlin-server/ktor/README.md @@ -42,6 +42,7 @@ docker run -p 8080:8080 kotlin-server * ~Supports collection formats for query parameters: csv, tsv, ssv, pipes.~ * Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in OpenAPI definitions. + ## Documentation for API Endpoints diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/AllApis.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/AllApis.kt new file mode 100644 index 000000000000..402a6beadeb9 --- /dev/null +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/AllApis.kt @@ -0,0 +1,14 @@ +package org.openapitools.server + +import io.ktor.server.routing.* +import org.openapitools.server.apis.PetApi +import org.openapitools.server.apis.StoreApi +import org.openapitools.server.apis.UserApi + + + +fun Route.AllApis() { + PetApi() + StoreApi() + UserApi() +} diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/PetApi.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/PetApi.kt index b72ec0d72cec..e854c91df315 100644 --- a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/PetApi.kt +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/PetApi.kt @@ -29,10 +29,8 @@ import org.openapitools.server.models.ModelApiResponse import org.openapitools.server.models.Pet fun Route.PetApi() { - val empty = mutableMapOf() - authenticate("petstore_auth") { - post { + post { addPet -> val principal = call.authentication.principal() @@ -41,9 +39,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - delete { + delete { deletePet -> val principal = call.authentication.principal() @@ -52,9 +49,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - get { + get { findPetsByStatus -> val principal = call.authentication.principal() @@ -102,9 +98,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - get { + get { findPetsByTags -> val principal = call.authentication.principal() @@ -152,9 +147,8 @@ fun Route.PetApi() { } } - authenticate("api_key") { - get { + get { getPetById -> val principal = call.authentication.principal() @@ -186,9 +180,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - put { + put { updatePet -> val principal = call.authentication.principal() @@ -197,9 +190,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - post { + post { updatePetWithForm -> val principal = call.authentication.principal() @@ -208,9 +200,8 @@ fun Route.PetApi() { } } - authenticate("petstore_auth") { - post { + post { uploadFile -> val principal = call.authentication.principal() @@ -230,5 +221,4 @@ fun Route.PetApi() { } } - } diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt index 4a4395778401..62a7b778a36c 100644 --- a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt @@ -28,15 +28,12 @@ import org.openapitools.server.infrastructure.ApiPrincipal import org.openapitools.server.models.Order fun Route.StoreApi() { - val empty = mutableMapOf() - - delete { + delete { deleteOrder -> call.respond(HttpStatusCode.NotImplemented) } - authenticate("api_key") { - get { + get { getInventory -> val principal = call.authentication.principal() @@ -45,8 +42,7 @@ fun Route.StoreApi() { } } - - get { + get { getOrderById -> val exampleContentType = "application/json" val exampleContentString = """{ "petId" : 6, @@ -64,8 +60,7 @@ fun Route.StoreApi() { } } - - post { + post { placeOrder -> val exampleContentType = "application/json" val exampleContentString = """{ "petId" : 6, @@ -83,5 +78,4 @@ fun Route.StoreApi() { } } - } diff --git a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/UserApi.kt b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/UserApi.kt index 299f743e141d..c60ef4516d0d 100644 --- a/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/UserApi.kt +++ b/samples/server/petstore/kotlin-server/ktor/src/main/kotlin/org/openapitools/server/apis/UserApi.kt @@ -28,29 +28,23 @@ import org.openapitools.server.infrastructure.ApiPrincipal import org.openapitools.server.models.User fun Route.UserApi() { - val empty = mutableMapOf() - - post { + post { createUser -> call.respond(HttpStatusCode.NotImplemented) } - - post { + post { createUsersWithArrayInput -> call.respond(HttpStatusCode.NotImplemented) } - - post { + post { createUsersWithListInput -> call.respond(HttpStatusCode.NotImplemented) } - - delete { + delete { deleteUser -> call.respond(HttpStatusCode.NotImplemented) } - - get { + get { getUserByName -> val exampleContentType = "application/json" val exampleContentString = """{ "firstName" : "firstName", @@ -70,20 +64,16 @@ fun Route.UserApi() { } } - - get { + get { loginUser -> call.respond(HttpStatusCode.NotImplemented) } - - get { + get { logoutUser -> call.respond(HttpStatusCode.NotImplemented) } - - put { + put { updateUser -> call.respond(HttpStatusCode.NotImplemented) } - } From 77ed7df8c0942d84bf048a8b8f81ec25ada9779b Mon Sep 17 00:00:00 2001 From: Alain-Michel Chomnoue Nghemning Date: Mon, 11 May 2026 11:44:43 +0200 Subject: [PATCH 05/10] Generate updated doc for delegate pattern option --- docs/generators/kotlin-server.md | 1 + samples/server/petstore/kotlin-server-modelMutable/README.md | 1 - samples/server/petstore/kotlin-server/ktor/README.md | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/generators/kotlin-server.md b/docs/generators/kotlin-server.md index 86859ea27401..4ad6fecf4e31 100644 --- a/docs/generators/kotlin-server.md +++ b/docs/generators/kotlin-server.md @@ -22,6 +22,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |apiSuffix|suffix for api classes| |Api| |artifactId|Generated artifact id (name of jar).| |kotlin-server| |artifactVersion|Generated artifact's package version.| |1.0.0| +|delegatePattern|Whether to generate the server files using the delegate pattern. This option is currently supported only when using ktor library.| |true| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |original| |featureAutoHead|Automatically provide responses to HEAD requests for existing routes that have the GET verb defined.| |true| |featureCORS|Ktor by default provides an interceptor for implementing proper support for Cross-Origin Resource Sharing (CORS). See enable-cors.org.| |false| diff --git a/samples/server/petstore/kotlin-server-modelMutable/README.md b/samples/server/petstore/kotlin-server-modelMutable/README.md index b371e659e03e..92500a09feda 100644 --- a/samples/server/petstore/kotlin-server-modelMutable/README.md +++ b/samples/server/petstore/kotlin-server-modelMutable/README.md @@ -42,7 +42,6 @@ docker run -p 8080:8080 kotlin-server * ~Supports collection formats for query parameters: csv, tsv, ssv, pipes.~ * Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in OpenAPI definitions. - ## Documentation for API Endpoints diff --git a/samples/server/petstore/kotlin-server/ktor/README.md b/samples/server/petstore/kotlin-server/ktor/README.md index b371e659e03e..92500a09feda 100644 --- a/samples/server/petstore/kotlin-server/ktor/README.md +++ b/samples/server/petstore/kotlin-server/ktor/README.md @@ -42,7 +42,6 @@ docker run -p 8080:8080 kotlin-server * ~Supports collection formats for query parameters: csv, tsv, ssv, pipes.~ * Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in OpenAPI definitions. - ## Documentation for API Endpoints From 4b7e2411f86b9e206c662946b0c1e49d53f3bbca Mon Sep 17 00:00:00 2001 From: Alain-Michel Chomnoue Nghemning Date: Mon, 11 May 2026 12:12:23 +0200 Subject: [PATCH 06/10] Fix tests after conflicts resolved --- docs/generators/kotlin-server.md | 1 + .../codegen/kotlin/KotlinServerCodegenTest.java | 2 ++ .../generated/.openapi-generator/VERSION | 2 +- .../ktor-delegate-pattern/generated/README.md | 2 +- .../ktor-delegate-pattern/generated/build.gradle.kts | 9 ++++----- .../ktor-delegate-pattern/generated/gradle.properties | 4 ++-- .../generated/src/main/resources/logback.xml | 2 +- 7 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/generators/kotlin-server.md b/docs/generators/kotlin-server.md index da36df26ba13..6ff7e2c748fc 100644 --- a/docs/generators/kotlin-server.md +++ b/docs/generators/kotlin-server.md @@ -22,6 +22,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |apiSuffix|suffix for api classes| |Api| |artifactId|Generated artifact id (name of jar).| |kotlin-server| |artifactVersion|Generated artifact's package version.| |1.0.0| +|delegatePattern|Whether to generate the server files using the delegate pattern. This option is currently supported only when using ktor library.| |true| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| |featureAutoHead|Automatically provide responses to HEAD requests for existing routes that have the GET verb defined.| |true| |featureCORS|Ktor by default provides an interceptor for implementing proper support for Cross-Origin Resource Sharing (CORS). See enable-cors.org.| |false| diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java index 565cf941e5cc..0abd3ad73473 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java @@ -40,6 +40,8 @@ import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.RETURN_RESPONSE; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.USE_TAGS; import static org.openapitools.codegen.languages.features.BeanValidationFeatures.USE_BEANVALIDATION; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.DELEGATE_PATTERN; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.KTOR; public class KotlinServerCodegenTest { diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/VERSION b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/VERSION index 0610c66bc14f..ca7bf6e46889 100644 --- a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/VERSION +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/.openapi-generator/VERSION @@ -1 +1 @@ -7.21.0-SNAPSHOT +7.23.0-SNAPSHOT diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/README.md b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/README.md index 93746904eef9..1594bcd95afd 100644 --- a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/README.md +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/README.md @@ -2,7 +2,7 @@ This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. -Generated by OpenAPI Generator 7.21.0-SNAPSHOT. +Generated by OpenAPI Generator 7.23.0-SNAPSHOT. ## Requires diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/build.gradle.kts b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/build.gradle.kts index 1e91d49feaf6..2cd09d7a899b 100644 --- a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/build.gradle.kts +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/build.gradle.kts @@ -6,13 +6,13 @@ group = "org.openapitools" version = "1.0.0" plugins { - kotlin("jvm") version "2.0.20" - application - kotlin("plugin.serialization") version "2.0.20" + kotlin("jvm") version "2.3.0" + id("io.ktor.plugin") version "3.4.0" + kotlin("plugin.serialization") version "2.3.0" } application { - mainClass.set("io.ktor.server.netty.EngineMain") + mainClass = "io.ktor.server.netty.EngineMain" val isDevelopment: Boolean = project.ext.has("development") applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") @@ -23,7 +23,6 @@ repositories { } dependencies { - implementation(platform("io.ktor:ktor-bom:3.0.2")) implementation("ch.qos.logback:logback-classic:$logback_version") implementation("com.typesafe:config:1.4.1") implementation("io.ktor:ktor-server-auth") diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle.properties b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle.properties index 36dd5c928837..46c974d67a63 100644 --- a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle.properties +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/gradle.properties @@ -1,4 +1,4 @@ kotlin.code.style=official -ktor_version=3.0.2 -kotlin_version=2.0.20 +ktor_version=3.4.0 +kotlin_version=2.3.0 logback_version=1.5.19 diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/logback.xml b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/logback.xml index d0eaba8debd6..b4671538e9e7 100644 --- a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/logback.xml +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/resources/logback.xml @@ -1,7 +1,7 @@ - %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n From 6a1b64ae01f2097d9878b825c1eef4d2410252de Mon Sep 17 00:00:00 2001 From: Alain-Michel Chomnoue Nghemning Date: Mon, 11 May 2026 13:18:28 +0200 Subject: [PATCH 07/10] Fix delegates unsafe casting by using as? --- .gitignore | 3 +++ .../kotlin-server/libraries/ktor/Delegates.kt.mustache | 4 ++-- .../org/openapitools/server/infrastructure/Delegates.kt | 8 ++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 58c0341b3f7d..a3d23cf207fb 100644 --- a/.gitignore +++ b/.gitignore @@ -312,3 +312,6 @@ samples/client/jetbrains/adyen/checkout71/http/client/Apis/http-client.private.e # Generated by the run-in-docker.sh \?/ + +# AI agent caches +.junie \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Delegates.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Delegates.kt.mustache index 166c528e43bc..4a8919c8b7f4 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Delegates.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Delegates.kt.mustache @@ -41,9 +41,9 @@ value class DelegateProvider(val call: RoutingCall) { ?: call.attributes.getOrNull(AppDelegates.AttributeKey) return when (T::class) { - {{#apiInfo}}{{#apis}}{{classname}}Delegate::class -> appDelegates?.{{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Delegate as T + {{#apiInfo}}{{#apis}}{{classname}}Delegate::class -> appDelegates?.{{#lambda.camelcase}}{{classname}}{{/lambda.camelcase}}Delegate as? T {{/apis}}{{/apiInfo}} - else -> null as T + else -> null } } } diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/Delegates.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/Delegates.kt index 219180eeb752..b87dc91b6653 100644 --- a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/Delegates.kt +++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/infrastructure/Delegates.kt @@ -47,11 +47,11 @@ value class DelegateProvider(val call: RoutingCall) { ?: call.attributes.getOrNull(AppDelegates.AttributeKey) return when (T::class) { - PetApiDelegate::class -> appDelegates?.petApiDelegate as T - StoreApiDelegate::class -> appDelegates?.storeApiDelegate as T - UserApiDelegate::class -> appDelegates?.userApiDelegate as T + PetApiDelegate::class -> appDelegates?.petApiDelegate as? T + StoreApiDelegate::class -> appDelegates?.storeApiDelegate as? T + UserApiDelegate::class -> appDelegates?.userApiDelegate as? T - else -> null as T + else -> null } } } From 5beb14684b409f174f40776c22f2d718ee75566f Mon Sep 17 00:00:00 2001 From: Alain-Michel Chomnoue Nghemning Date: Mon, 11 May 2026 16:55:56 +0200 Subject: [PATCH 08/10] Use fromValue() method for enum parsing --- docs/generators/kotlin-server.md | 2 +- .../languages/KotlinServerCodegen.java | 2 +- .../kotlin-server/enum_class.mustache | 12 ++++++ .../ktor/_extract_param_value.mustache | 2 +- .../libraries/ktor/apiDelegate.mustache | 4 +- .../kotlin/KotlinServerCodegenTest.java | 39 +++++++++++++++++++ .../test/resources/3_1/enum-wire-value.yaml | 19 +++++++++ 7 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_1/enum-wire-value.yaml diff --git a/docs/generators/kotlin-server.md b/docs/generators/kotlin-server.md index 6ff7e2c748fc..09964a2753a2 100644 --- a/docs/generators/kotlin-server.md +++ b/docs/generators/kotlin-server.md @@ -22,7 +22,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |apiSuffix|suffix for api classes| |Api| |artifactId|Generated artifact id (name of jar).| |kotlin-server| |artifactVersion|Generated artifact's package version.| |1.0.0| -|delegatePattern|Whether to generate the server files using the delegate pattern. This option is currently supported only when using ktor library.| |true| +|delegatePattern|Whether to generate the server files using the delegate pattern. This option is currently supported only when using ktor library.| |false| |enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', 'original', and 'bestEffortBacktick' (like 'original' but tries to wrap values in backticks before falling back to sanitizing, e.g. `name,asc` stays `name,asc` rather than becoming nameCommaAsc; useful for sort/order enums)| |original| |featureAutoHead|Automatically provide responses to HEAD requests for existing routes that have the GET verb defined.| |true| |featureCORS|Ktor by default provides an interceptor for implementing proper support for Cross-Origin Resource Sharing (CORS). See enable-cors.org.| |false| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java index 80c3d7f1eedb..6a10b4587d47 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java @@ -177,7 +177,7 @@ public KotlinServerCodegen() { addSwitch(Constants.OMIT_GRADLE_WRAPPER, Constants.OMIT_GRADLE_WRAPPER_DESC, omitGradleWrapper); addSwitch(USE_JAKARTA_EE, Constants.USE_JAKARTA_EE_DESC, useJakartaEe); addSwitch(Constants.FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE, Constants.FIX_JACKSON_JSON_TYPE_INFO_INHERITANCE_DESC, fixJacksonJsonTypeInfoInheritance); - addSwitch(Constants.DELEGATE_PATTERN, Constants.DELEGATE_PATTERN_DESC, getAutoHeadFeatureEnabled()); + addSwitch(Constants.DELEGATE_PATTERN, Constants.DELEGATE_PATTERN_DESC, getDelegatePatternEnabled()); } @Override diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/enum_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/enum_class.mustache index 6203806394da..1a2a72e9f1fe 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/enum_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/enum_class.mustache @@ -11,4 +11,16 @@ enum class {{classname}}(val value: {{dataType}}) { {{/enumDescription}} {{&name}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} {{/enumVars}}{{/allowableValues}} +{{#delegatePattern}} + + companion object { + /** + * Returns the enum individual associated with the [value] passed as parameter. + * + * @throws IllegalArgumentException if no enum individual is associated with the [value] passed as parameter. + */ + fun fromValue(value: {{{dataType}}}): {{classname}} = + values().firstOrNull { it.value == value } ?: throw IllegalArgumentException("No enum constant {{classname}}.$value") + } +{{/delegatePattern}} } diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache index 9aa223725a28..23104329da42 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache @@ -1 +1 @@ -{{^required}}{{#isString}}{{/isString}}{{#isEnum}}?.let { runCatching { enumValueOf<{{{dataType}}}>(it) }.getOrElse { throw BadRequestException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnum}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadRequestException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnum}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadRequestException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnum}}{{/isString}}{{/required}}{{#required}}{{#defaultValue}}{{#isString}}{{/isString}}{{#isEnum}}?.let { runCatching { enumValueOf<{{{dataType}}}>(it) }.getOrElse { throw BadRequestException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnum}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadRequestException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnum}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadRequestException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnum}}{{/isString}} ?: {{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}.let { it{{#isString}}{{/isString}}{{#isEnum}}?.let { runCatching { enumValueOf<{{{dataType}}}>(it) }.getOrElse { throw BadRequestException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnum}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadRequestException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnum}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadRequestException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnum}}{{/isString}} ?: throw BadRequestException(message = "Missing {{#isQueryParam}}query{{/isQueryParam}}{{#isPathParam}}path{{/isPathParam}}{{#isHeaderParam}}header{{/isHeaderParam}}{{#isCookieParam}}cookie{{/isCookieParam}}{{#isFormParam}}form{{/isFormParam}} parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") }{{/defaultValue}}{{/required}} \ No newline at end of file +{{^required}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}}{{/required}}{{#required}}{{#defaultValue}}{{#isString}}{{/isString}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}} ?: {{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}.let { it{{#isString}}{{/isString}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}} ?: throw BadParameterException(message = "Missing {{#isQueryParam}}query{{/isQueryParam}}{{#isPathParam}}path{{/isPathParam}}{{#isHeaderParam}}header{{/isHeaderParam}}{{#isCookieParam}}cookie{{/isCookieParam}}{{#isFormParam}}form{{/isFormParam}} parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") }{{/defaultValue}}{{/required}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache index ba8d2cc38e94..f061768868c5 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache @@ -36,14 +36,14 @@ interface {{classname}}Delegate { fun build(): {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPart { {{#allParams}}{{#isFormParam}}{{^isFile}}{{#required}} if ({{paramName}} == null) { - throw BadRequestException(message = "Missing form parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") + throw BadParameterException(message = "Missing form parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") } {{/required}}{{^required}}{{#defaultValue}} if ({{paramName}} == null) { {{paramName}} = {{{defaultValue}}} } {{/defaultValue}}{{/required}}{{/isFile}}{{/isFormParam}}{{/allParams}} - return {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPart({{#allParams}}{{#isFormParam}}{{^isFile}}{{paramName}}, {{/isFile}}{{/isFormParam}}{{/allParams}}) + return {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPart({{#allParams}}{{#isFormParam}}{{^isFile}}{{paramName}}{{^isNullable}}{{#defaultValue}}!!{{/defaultValue}}{{^defaultValue}}{{#required}}!!{{/required}}{{/defaultValue}}{{/isNullable}}, {{/isFile}}{{/isFormParam}}{{/allParams}}) } } fun interface {{#lambda.titlecase}}{{operationId}}{{/lambda.titlecase}}MultiPartReceiver{ diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java index 0abd3ad73473..8099db66ebb7 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java @@ -42,6 +42,7 @@ import static org.openapitools.codegen.languages.features.BeanValidationFeatures.USE_BEANVALIDATION; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.DELEGATE_PATTERN; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.KTOR; +import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.RESOURCES; public class KotlinServerCodegenTest { @@ -669,4 +670,42 @@ public void delegatePattern_canBeEnabled() throws IOException { Assert.assertTrue(Files.exists(Paths.get(infraPath + "/BadParameterException.kt"))); Assert.assertTrue(Files.exists(Paths.get(infraPath + "/APINotImplementedException.kt"))); } + + @Test + public void delegatePattern_enumWireValue() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + var codegen = new KotlinServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(LIBRARY, KTOR); + codegen.additionalProperties().put(DELEGATE_PATTERN, true); + codegen.additionalProperties().put(RESOURCES, false); + codegen.inlineSchemaOption().put("RESOLVE_INLINE_ENUMS","true"); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_1/enum-wire-value.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; + + + Path statusFile = Paths.get(outputPath + "/models/TestEnumStatusParameter.kt"); + + assertFileContains( + statusFile, + "companion object {", + "fun fromValue(value: kotlin.String): TestEnumStatusParameter =", + "values().firstOrNull { it.value == value } ?: throw IllegalArgumentException(\"No enum constant TestEnumStatusParameter.$value\")" + ); + + // Check parameter extraction + // Check Enum definition in the API file (inline enum) + Path apiPath = Paths.get(outputPath + "/apis/DefaultApi.kt"); + assertFileContains( + apiPath, + "val status = call.request.queryParameters[\"status\"]?.let { runCatching { TestEnumStatusParameter.fromValue(it) }.getOrElse { throw BadParameterException(message = \"Invalid enum value for parameter status: $it\", parameterName = \"status\") } }" + ); + } } diff --git a/modules/openapi-generator/src/test/resources/3_1/enum-wire-value.yaml b/modules/openapi-generator/src/test/resources/3_1/enum-wire-value.yaml new file mode 100644 index 000000000000..94d2d526b225 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/enum-wire-value.yaml @@ -0,0 +1,19 @@ +openapi: 3.1.0 +info: + title: Enum Test + version: 1.0.0 +paths: + /test: + get: + operationId: testEnum + parameters: + - name: status + in: query + schema: + type: string + enum: + - in-progress + - completed + responses: + '200': + description: OK From 123ff397912a54ee909263b2e64051ceb13797a1 Mon Sep 17 00:00:00 2001 From: Alain-Michel Chomnoue Nghemning Date: Tue, 12 May 2026 13:54:47 +0200 Subject: [PATCH 09/10] Use to() to extract numerical values --- .../libraries/ktor/_extract_param_value.mustache | 2 +- .../src/main/resources/logback.xml | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/logback.xml diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache index 23104329da42..540d0e4168cb 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache @@ -1 +1 @@ -{{^required}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}}{{/required}}{{#required}}{{#defaultValue}}{{#isString}}{{/isString}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}} ?: {{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}.let { it{{#isString}}{{/isString}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { {{{dataType}}}(it) }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}} ?: throw BadParameterException(message = "Missing {{#isQueryParam}}query{{/isQueryParam}}{{#isPathParam}}path{{/isPathParam}}{{#isHeaderParam}}header{{/isHeaderParam}}{{#isCookieParam}}cookie{{/isCookieParam}}{{#isFormParam}}form{{/isFormParam}} parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") }{{/defaultValue}}{{/required}} \ No newline at end of file +{{^required}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}}{{/required}}{{#required}}{{#defaultValue}}{{#isString}}{{/isString}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}} ?: {{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}.let { it{{#isString}}{{/isString}}{{#isEnumOrRef}}?.let { runCatching { {{{dataType}}}.fromValue(it) }.getOrElse { throw BadParameterException(message = "Invalid enum value for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isEnumOrRef}}{{#isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid number format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{^isString}}{{^isEnumOrRef}}{{^isNumber}}?.let { runCatching { it.to{{{dataType}}}() }.getOrElse { throw BadParameterException(message = "Invalid format for parameter {{baseName}}: $it", parameterName = "{{baseName}}") } }{{/isNumber}}{{/isEnumOrRef}}{{/isString}} ?: throw BadParameterException(message = "Missing {{#isQueryParam}}query{{/isQueryParam}}{{#isPathParam}}path{{/isPathParam}}{{#isHeaderParam}}header{{/isHeaderParam}}{{#isCookieParam}}cookie{{/isCookieParam}}{{#isFormParam}}form{{/isFormParam}} parameter: {{baseName}}", parameterName = "{{baseName}}", validationType = "required") }{{/defaultValue}}{{/required}} \ No newline at end of file diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/logback.xml b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/logback.xml deleted file mode 100644 index aadef5d5ba2a..000000000000 --- a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/logback.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - - \ No newline at end of file From 3de9af88955a30b8c5a1c63e4501b6da1bde038f Mon Sep 17 00:00:00 2001 From: Alain-Michel Chomnoue Nghemning Date: Tue, 12 May 2026 18:36:49 +0200 Subject: [PATCH 10/10] Fix multipleOf validation to support floating-point --- .../kotlin-server/_common_validation.mustache | 24 +++++++++ .../kotlin/KotlinServerCodegenTest.java | 43 ++++++++++++++++ .../3_1/kotlin/multiple-of-validation.yaml | 50 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 modules/openapi-generator/src/test/resources/3_1/kotlin/multiple-of-validation.yaml diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache index 2fc1309ac76a..4378942e0dac 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache @@ -54,7 +54,15 @@ if ({{{name}}}{{{paramName}}} {{#exclusiveMaximum}}>={{/exclusiveMaximum}}{{^exc } {{/maximum}} {{#multipleOf}} +{{#isFloat}} +if (kotlin.math.abs(({{{name}}}{{{paramName}}}.toDouble() / {{{multipleOf}}}) - kotlin.math.round({{{name}}}{{{paramName}}}.toDouble() / {{{multipleOf}}})) > 1.0e-6) { +{{/isFloat}} +{{#isDouble}} +if (kotlin.math.abs(({{{name}}}{{{paramName}}}.toDouble() / {{{multipleOf}}}) - kotlin.math.round({{{name}}}{{{paramName}}}.toDouble() / {{{multipleOf}}})) > 1.0e-10) { +{{/isDouble}} +{{^isFloat}}{{^isDouble}} if ({{{name}}}{{{paramName}}} % {{{multipleOf}}} != 0) { +{{/isDouble}}{{/isFloat}} throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} should be a multiple of {{{multipleOf}}}", parameterName = "{{{name}}}{{{paramName}}}", @@ -195,7 +203,15 @@ try { } {{/maximum}} {{#multipleOf}} + {{#isFloat}} + if (kotlin.math.abs((it.toString().toDouble() / {{{multipleOf}}}) - kotlin.math.round(it.toString().toDouble() / {{{multipleOf}}})) > 1.0e-6) { + {{/isFloat}} + {{#isDouble}} + if (kotlin.math.abs((it.toString().toDouble() / {{{multipleOf}}}) - kotlin.math.round(it.toString().toDouble() / {{{multipleOf}}})) > 1.0e-10) { + {{/isDouble}} + {{^isFloat}}{{^isDouble}} if (it.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() % {{{multipleOf}}} != 0{{#isFloat}}.0{{/isFloat}}{{#isDouble}}.0{{/isDouble}}) { + {{/isDouble}}{{/isFloat}} throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} item $it should be a multiple of {{{multipleOf}}}", parameterName = "{{{name}}}{{{paramName}}}", @@ -306,7 +322,15 @@ try { } {{/maximum}} {{#multipleOf}} + {{#isFloat}} + if (kotlin.math.abs((value.toString().toDouble() / {{{multipleOf}}}) - kotlin.math.round(value.toString().toDouble() / {{{multipleOf}}})) > 1.0e-6) { + {{/isFloat}} + {{#isDouble}} + if (kotlin.math.abs((value.toString().toDouble() / {{{multipleOf}}}) - kotlin.math.round(value.toString().toDouble() / {{{multipleOf}}})) > 1.0e-10) { + {{/isDouble}} + {{^isFloat}}{{^isDouble}} if (value.toString().to{{#isFloat}}Float{{/isFloat}}{{#isDouble}}Double{{/isDouble}}{{#isLong}}Long{{/isLong}}{{#isInteger}}Int{{/isInteger}}{{^isFloat}}{{^isDouble}}{{^isLong}}{{^isInteger}}Double{{/isInteger}}{{/isLong}}{{/isDouble}}{{/isFloat}}() % {{{multipleOf}}} != 0{{#isFloat}}.0{{/isFloat}}{{#isDouble}}.0{{/isDouble}}) { + {{/isDouble}}{{/isFloat}} throw BadParameterException( message = "{{#isPathParam}}Path parameter{{/isPathParam}}{{#isQueryParam}}Query parameter{{/isQueryParam}}{{#isHeaderParam}}Header parameter{{/isHeaderParam}}{{#isCookieParam}}Cookie parameter{{/isCookieParam}}{{#isBodyParam}}Body parameter{{/isBodyParam}}{{#isFormParam}}Form parameter{{/isFormParam}}{{^isPathParam}}{{^isQueryParam}}{{^isHeaderParam}}{{^isCookieParam}}{{^isBodyParam}}{{^isFormParam}}Property{{/isFormParam}}{{/isBodyParam}}{{/isCookieParam}}{{/isHeaderParam}}{{/isQueryParam}}{{/isPathParam}} {{{name}}}{{{paramName}}} value $value for key $key should be a multiple of {{{multipleOf}}}", parameterName = "{{{name}}}{{{paramName}}}", diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java index 8099db66ebb7..abe3fcc3da50 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java @@ -708,4 +708,47 @@ public void delegatePattern_enumWireValue() throws IOException { "val status = call.request.queryParameters[\"status\"]?.let { runCatching { TestEnumStatusParameter.fromValue(it) }.getOrElse { throw BadParameterException(message = \"Invalid enum value for parameter status: $it\", parameterName = \"status\") } }" ); } + + + @Test + public void testFloatingPointMultipleOfValidationUsesTolerance() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinServerCodegen codegen = new KotlinServerCodegen(); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(LIBRARY, KTOR); + codegen.additionalProperties().put(DELEGATE_PATTERN, true); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_1/kotlin/multiple-of-validation.yaml")) + .config(codegen)) + .generate(); + + String modelPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server/models/MultipleOfModel.kt"; + Path multipleOfModel = Paths.get(modelPath); + + Assert.assertTrue(Files.exists(multipleOfModel)); + + // Floating-point multipleOf validation must tolerate JVM representation error. + assertFileContains( + multipleOfModel, + "if (kotlin.math.abs((floatVal.toDouble() / 0.1) - kotlin.math.round(floatVal.toDouble() / 0.1)) > 1.0e-6) {", + "if (kotlin.math.abs((doubleVal.toDouble() / 0.1) - kotlin.math.round(doubleVal.toDouble() / 0.1)) > 1.0e-10) {", + "if (kotlin.math.abs((it.toString().toDouble() / 0.1) - kotlin.math.round(it.toString().toDouble() / 0.1)) > 1.0e-6) {", + "if (kotlin.math.abs((value.toString().toDouble() / 0.01) - kotlin.math.round(value.toString().toDouble() / 0.01)) > 1.0e-10) {" + ); + + assertFileNotContains( + multipleOfModel, + "if (floatVal % 0.1 != 0) {", + "if (doubleVal % 0.1 != 0) {" + ); + + // Integral multipleOf validation remains exact. + assertFileContains( + multipleOfModel, + "if (intVal % 2 != 0) {" + ); + } } diff --git a/modules/openapi-generator/src/test/resources/3_1/kotlin/multiple-of-validation.yaml b/modules/openapi-generator/src/test/resources/3_1/kotlin/multiple-of-validation.yaml new file mode 100644 index 000000000000..4dea9399f952 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/kotlin/multiple-of-validation.yaml @@ -0,0 +1,50 @@ +openapi: 3.0.0 +info: + title: MultipleOf Validation + version: 1.0.0 +paths: + /test: + post: + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MultipleOfModel' + responses: + '200': + description: OK +components: + schemas: + MultipleOfModel: + type: object + required: + - int_val + - float_val + - double_val + - float_values + - double_map + properties: + int_val: + type: integer + multipleOf: 2 + float_val: + type: number + format: float + multipleOf: 0.1 + double_val: + type: number + format: double + multipleOf: 0.1 + float_values: + type: array + items: + type: number + format: float + multipleOf: 0.1 + double_map: + type: object + additionalProperties: + type: number + format: double + multipleOf: 0.01