From ede98e38b67cc49beb4d4eb6b22f01b967c8d765 Mon Sep 17 00:00:00 2001 From: altro3 Date: Wed, 20 Nov 2024 22:39:30 +0700 Subject: [PATCH 1/3] Fix discriminator override for kotlin Fixed #1881 --- .../AbstractMicronautKotlinCodegen.java | 2 +- .../io/micronaut/openapi/generator/Utils.java | 11 +- .../openapitools/codegen/DefaultCodegen.java | 117 ++++++++++++------ .../kotlin-micronaut/client/api.mustache | 2 +- .../client/auth/Authorization.mustache | 2 +- .../client/auth/AuthorizationBinder.mustache | 15 +-- .../client/auth/AuthorizationFilter.mustache | 12 +- .../client/auth/Authorizations.mustache | 2 +- .../ApiKeyAuthConfiguration.mustache | 29 ++--- .../HttpBasicAuthConfiguration.mustache | 9 +- .../client/params/bodyParams.mustache | 2 +- .../client/params/formParams.mustache | 2 +- .../common/model/oneof_interface.mustache | 2 +- .../common/model/typeInfoAnnotation.mustache | 4 +- .../common/operationAnnotations.mustache | 14 +-- .../server/controller-interface.mustache | 2 +- .../KotlinMicronautClientCodegenTest.java | 82 +++++++++--- .../KotlinMicronautServerCodegenTest.java | 30 ++--- 18 files changed, 214 insertions(+), 125 deletions(-) diff --git a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java index 604328bde9..9a55552180 100644 --- a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java +++ b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java @@ -2102,7 +2102,7 @@ private void processVar(CodegenModel model, List vars, List"; typeWithEnumWithGenericAnnotations = "Map"; } else if (containerType != null) { - var genericAnnotations = genericAnnotations(itemsProp, isGenerateHardNullable); + genericAnnotations = genericAnnotations(itemsProp, isGenerateHardNullable); processGenericAnnotations(itemsProp, useBeanValidation, isGenerateHardNullable, itemsProp.isNullable, itemsProp.required, itemsProp.isReadOnly, withNullablePostfix); typeWithGenericAnnotations = containerType + "<" + genericAnnotations + itemsProp.vendorExtensions.get("typeWithGenericAnnotations") + ">"; typeWithEnumWithGenericAnnotations = containerType + "<" + genericAnnotations + itemsProp.vendorExtensions.get("typeWithEnumWithGenericAnnotations") + ">"; } } - ext.put("typeWithGenericAnnotations", typeWithGenericAnnotations + (withNullablePostfix && (isNullable || isRequired && isReadonly) ? "?" : "")); - ext.put("typeWithEnumWithGenericAnnotations", typeWithEnumWithGenericAnnotations + (withNullablePostfix && (isNullable || isRequired && isReadonly) ? "?" : "")); + var isNullableType = withNullablePostfix && (isNullable || isRequired && isReadonly || (genericAnnotations != null && !genericAnnotations.contains("NotNull") && !isRequired)); + + ext.put("typeWithGenericAnnotations", typeWithGenericAnnotations + (isNullableType ? "?" : "")); + ext.put("typeWithEnumWithGenericAnnotations", typeWithEnumWithGenericAnnotations + (isNullableType ? "?" : "")); } private static String genericAnnotations(CodegenProperty prop, boolean isGenerateHardNullable) { diff --git a/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 13314bfe90..320aea0ca1 100644 --- a/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -116,6 +116,7 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; @@ -482,7 +483,7 @@ private void registerMustacheLambdas() { @Override @SuppressWarnings("static-method") public Map postProcessAllModels(Map objs) { - for (Entry entry : objs.entrySet()) { + for (Map.Entry entry : objs.entrySet()) { CodegenModel model = ModelUtils.getModelByName(entry.getKey(), objs); if (model == null) { @@ -547,7 +548,7 @@ public Map postProcessAllModels(Map objs) List> modelsImports = modelsAttrs.getImportsOrEmpty(); for (ModelMap mo : modelsAttrs.getModels()) { CodegenModel cm = mo.getModel(); - if (cm.oneOf.size() > 0) { + if (!cm.oneOf.isEmpty()) { cm.vendorExtensions.put("x-is-one-of-interface", true); for (String one : cm.oneOf) { if (!additionalDataMap.containsKey(one)) { @@ -562,7 +563,7 @@ public Map postProcessAllModels(Map objs) } // Add all the data from OneOfImplementorAdditionalData classes to the implementing models - for (Entry modelsEntry : objs.entrySet()) { + for (Map.Entry modelsEntry : objs.entrySet()) { ModelsMap modelsAttrs = modelsEntry.getValue(); List> imports = modelsAttrs.getImports(); for (ModelMap implmo : modelsAttrs.getModels()) { @@ -657,7 +658,7 @@ public Map updateAllModels(Map objs) { } // Let parent know about all its children - for (Entry allModelsEntry : allModels.entrySet()) { + for (Map.Entry allModelsEntry : allModels.entrySet()) { String name = allModelsEntry.getKey(); CodegenModel cm = allModelsEntry.getValue(); CodegenModel parent = allModels.get(cm.getParent()); @@ -1089,8 +1090,8 @@ public void preprocessOpenAPI(OpenAPI openAPI) { // we need to add all request and response bodies to processed schemas if (pathItems != null) { - for (Entry e : pathItems.entrySet()) { - for (Entry op : e.getValue().readOperationsMap().entrySet()) { + for (Map.Entry e : pathItems.entrySet()) { + for (Map.Entry op : e.getValue().readOperationsMap().entrySet()) { String opId = getOrGenerateOperationId(op.getValue(), e.getKey(), op.getKey().toString()); // process request body RequestBody b = ModelUtils.getReferencedRequestBody(openAPI, op.getValue().getRequestBody()); @@ -1103,7 +1104,7 @@ public void preprocessOpenAPI(OpenAPI openAPI) { } // process all response bodies if (op.getValue().getResponses() != null) { - for (Entry ar : op.getValue().getResponses().entrySet()) { + for (Map.Entry ar : op.getValue().getResponses().entrySet()) { ApiResponse a = ModelUtils.getReferencedApiResponse(openAPI, ar.getValue()); Schema responseSchema = unaliasSchema(ModelUtils.getSchemaFromResponse(openAPI, a)); if (responseSchema != null) { @@ -1117,20 +1118,20 @@ public void preprocessOpenAPI(OpenAPI openAPI) { // also add all properties of all schemas to be checked for oneOf Map propertySchemas = new HashMap<>(); - for (Entry e : schemas.entrySet()) { + for (Map.Entry e : schemas.entrySet()) { Schema s = e.getValue(); Map props = s.getProperties(); if (props == null) { props = new HashMap<>(); } - for (Entry p : props.entrySet()) { + for (Map.Entry p : props.entrySet()) { propertySchemas.put(e.getKey() + "/" + p.getKey(), p.getValue()); } } schemas.putAll(propertySchemas); // go through all gathered schemas and add them as interfaces to be created - for (Entry e : schemas.entrySet()) { + for (Map.Entry e : schemas.entrySet()) { String n = toModelName(e.getKey()); Schema s = e.getValue(); String nOneOf = toModelName(n + "OneOf"); @@ -1273,6 +1274,20 @@ public String escapeText(String input) { .replace("\"", "\\\"")); } + /** + * This method escapes text to be used in a single quoted string + * @param input the input string + * @return the escaped string + */ + public String escapeTextInSingleQuotes(String input) { + if (input == null) { + return null; + } + + return escapeText(input).replace("'", "\\'"); + } + + /** * Escape characters while allowing new lines * @@ -3310,7 +3325,7 @@ protected void setAddProps(Schema schema, IJsonSchemaValidationProperties proper additionalPropertiesIsAnyType = true; } } else { - // if additioanl properties is set (e.g. free form object, any type, string, etc) + // if additional properties is set (e.g. free form object, any type, string, etc) addPropProp = fromProperty(getAdditionalPropertiesName(), (Schema) schema.getAdditionalProperties(), false); additionalPropertiesIsAnyType = true; } @@ -4012,8 +4027,29 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo } Schema original = null; + // process the dereference schema if it's a ref to allOf with a single item + // and certain field(s) (e.g. description, readyOnly, etc) is set + if (p.get$ref() != null) { + Schema derefSchema = ModelUtils.getReferencedSchema(openAPI, p); + if (ModelUtils.isAllOfWithSingleItem(derefSchema) && ( + derefSchema.getReadOnly() != null || + derefSchema.getWriteOnly() != null || + derefSchema.getDeprecated() != null || + derefSchema.getDescription() != null || + derefSchema.getMaxLength() != null || + derefSchema.getMinLength() != null || + derefSchema.getMinimum() != null || + derefSchema.getMaximum() != null || + derefSchema.getMaximum() != null || + derefSchema.getMinItems() != null || + derefSchema.getTitle() != null + )) { + p = ModelUtils.getReferencedSchema(openAPI, p); + } + } + // check if it's allOf (only 1 sub schema) with or without default/nullable/etc set in the top level - if (ModelUtils.isAllOf(p) && p.getAllOf().size() == 1) { + if (ModelUtils.isAllOfWithSingleItem(p)) { if (p.getAllOf().get(0) instanceof Schema) { original = p; p = (Schema) p.getAllOf().get(0); @@ -4293,7 +4329,7 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo property.defaultValueWithParam = toDefaultValueWithParam(name, p); LOGGER.debug("debugging from property return: {}", property); - schemaCodegenPropertyCache.put(ns, property); +// schemaCodegenPropertyCache.put(ns, property); return property; } @@ -4482,7 +4518,7 @@ protected ApiResponse findMethodResponse(ApiResponses responses) { if (code == null) { return null; } - return responses.get(code); + return ModelUtils.getReferencedApiResponse(openAPI, responses.get(code)); } /** @@ -4514,7 +4550,8 @@ protected void handleMethodResponse(Operation operation, CodegenOperation op, ApiResponse methodResponse, Map schemaMappings) { - Schema responseSchema = unaliasSchema(ModelUtils.getSchemaFromResponse(openAPI, methodResponse)); + ApiResponse response = ModelUtils.getReferencedApiResponse(openAPI, methodResponse); + Schema responseSchema = unaliasSchema(ModelUtils.getSchemaFromResponse(openAPI, response)); if (responseSchema != null) { CodegenProperty cm = fromProperty("response", responseSchema, false); @@ -4567,7 +4604,7 @@ protected void handleMethodResponse(Operation operation, } op.returnProperty = cm; } - addHeaders(methodResponse, op.responseHeaders); + addHeaders(response, op.responseHeaders); } /** @@ -4631,9 +4668,9 @@ public CodegenOperation fromOperation(String path, if (operation.getResponses() != null && !operation.getResponses().isEmpty()) { ApiResponse methodResponse = findMethodResponse(operation.getResponses()); - for (Entry operationGetResponsesEntry : operation.getResponses().entrySet()) { + for (Map.Entry operationGetResponsesEntry : operation.getResponses().entrySet()) { String key = operationGetResponsesEntry.getKey(); - ApiResponse response = operationGetResponsesEntry.getValue(); + ApiResponse response = ModelUtils.getReferencedApiResponse(openAPI, operationGetResponsesEntry.getValue()); addProducesInfo(response, op); CodegenResponse r = fromResponse(key, response); Map headers = response.getHeaders(); @@ -4703,9 +4740,10 @@ public CodegenOperation fromOperation(String path, List> examples = new ArrayList<>(); for (String statusCode : operation.getResponses().keySet()) { - ApiResponse apiResponse = operation.getResponses().get(statusCode); + ApiResponse apiResponse = ModelUtils.getReferencedApiResponse(openAPI, operation.getResponses().get(statusCode)); Schema schema = unaliasSchema(ModelUtils.getSchemaFromResponse(openAPI, apiResponse)); if (schema == null) { + // void response continue; } @@ -5289,7 +5327,7 @@ public CodegenParameter fromParameter(Parameter parameter, Set imports) parameterModelName = getParameterDataType(parameter, parameterSchema); CodegenProperty prop; if (this instanceof RustServerCodegen) { - // for rust server, we need to do somethings special as it uses + // for rust server, we need to do something special as it uses // $ref (e.g. #components/schemas/Pet) to determine whether it's a model prop = fromProperty(parameter.getName(), parameterSchema, false); } else if (getUseInlineModelResolver()) { @@ -5303,7 +5341,7 @@ public CodegenParameter fromParameter(Parameter parameter, Set imports) if (content.size() > 1) { once(LOGGER).warn("Multiple schemas found in content, returning only the first one"); } - Entry entry = content.entrySet().iterator().next(); + Map.Entry entry = content.entrySet().iterator().next(); codegenParameter.contentType = entry.getKey(); parameterSchema = entry.getValue().getSchema(); parameterModelName = getParameterDataType(parameter, parameterSchema); @@ -5783,7 +5821,7 @@ protected List> toExamples(Map examples) { } final List> output = new ArrayList<>(examples.size()); - for (Entry entry : examples.entrySet()) { + for (Map.Entry entry : examples.entrySet()) { final Map kv = new HashMap<>(); kv.put("contentType", entry.getKey()); kv.put("example", entry.getValue()); @@ -5800,7 +5838,7 @@ protected List> toExamples(Map examples) { */ protected void addHeaders(ApiResponse response, List properties) { if (response.getHeaders() != null) { - for (Entry headerEntry : response.getHeaders().entrySet()) { + for (Map.Entry headerEntry : response.getHeaders().entrySet()) { String description = headerEntry.getValue().getDescription(); // follow the $ref Header header = ModelUtils.getReferencedHeader(this.openAPI, headerEntry.getValue()); @@ -5825,8 +5863,6 @@ protected void addHeaders(ApiResponse response, List properties } } - private final Map seenOperationIds = new HashMap(); - /** * Add operation to group * @@ -5847,18 +5883,13 @@ public void addOperationToGroup(String tag, String resourcePath, Operation opera } // check for operationId uniqueness String uniqueName = co.operationId; - int counter = seenOperationIds.getOrDefault(uniqueName, 0); - while (seenOperationIds.containsKey(uniqueName)) { - uniqueName = co.operationId + "_" + counter; - counter++; - } + int counter = 0; for (CodegenOperation op : opList) { if (uniqueName.equals(op.operationId)) { uniqueName = co.operationId + "_" + counter; counter++; } } - seenOperationIds.put(co.operationId, counter); if (!co.operationId.equals(uniqueName)) { LOGGER.warn("generated unique operationId `{}`", uniqueName); } @@ -6064,7 +6095,7 @@ protected void addVars(IJsonSchemaValidationProperties m, List } } - for (Entry entry : properties.entrySet()) { + for (Map.Entry entry : properties.entrySet()) { final String key = entry.getKey(); final Schema prop = entry.getValue(); if (prop == null) { @@ -6072,7 +6103,7 @@ protected void addVars(IJsonSchemaValidationProperties m, List } else { final CodegenProperty cp; - if (cm != null && cm.allVars == vars && varsMap.keySet().contains(key)) { + if (cm != null && cm.allVars == vars && varsMap.containsKey(key)) { // when updating allVars, reuse the codegen property from the child model if it's already present // the goal is to avoid issues when the property is defined in both child, parent but the // definition is not identical, e.g. required vs optional, integer vs string @@ -6171,7 +6202,7 @@ Map getAllAliases(Map schemas) { } Map aliases = new HashMap<>(); - for (Entry entry : schemas.entrySet()) { + for (Map.Entry entry : schemas.entrySet()) { Schema schema = entry.getValue(); if (isAliasOfSimpleTypes(schema)) { if (schema.getAllOf() != null && schema.getAllOf().size() == 1) { // allOf with a single item @@ -6798,7 +6829,7 @@ public void updateCodegenPropertyEnum(CodegenProperty var) { String varDataType = var.mostInnerItems != null ? var.mostInnerItems.dataType : var.dataType; Optional referencedSchema = ModelUtils.getSchemas(openAPI).entrySet().stream() .filter(entry -> Objects.equals(varDataType, toModelName(entry.getKey()))) - .map(Entry::getValue) + .map(Map.Entry::getValue) .findFirst(); String dataType = (referencedSchema.isPresent()) ? getTypeDeclaration(referencedSchema.get()) : varDataType; List> enumVars = buildEnumVars(values, dataType); @@ -7072,7 +7103,7 @@ private void setOauth2Info(CodegenSecurity codegenSecurity, OAuthFlow flow) { if (flow.getScopes() != null && !flow.getScopes().isEmpty()) { List> scopes = new ArrayList<>(); - for (Entry scopeEntry : flow.getScopes().entrySet()) { + for (Map.Entry scopeEntry : flow.getScopes().entrySet()) { Map scope = new HashMap<>(); scope.put("scope", scopeEntry.getKey()); scope.put("description", escapeText(scopeEntry.getValue())); @@ -7086,7 +7117,7 @@ private void setOauth2Info(CodegenSecurity codegenSecurity, OAuthFlow flow) { private void setOpenIdConnectInfo(CodegenSecurity codegenSecurity, OAuthFlow flow) { if (flow.getScopes() != null && !flow.getScopes().isEmpty()) { List> scopes = new ArrayList<>(); - for (Entry scopeEntry : flow.getScopes().entrySet()) { + for (Map.Entry scopeEntry : flow.getScopes().entrySet()) { Map scope = new HashMap<>(); scope.put("scope", scopeEntry.getKey()); scopes.add(scope); @@ -7290,7 +7321,7 @@ public List fromRequestBodyToFormParameters(RequestBody body, boolean isOneOfOrAnyOf = ModelUtils.isOneOf(schema) || ModelUtils.isAnyOf(schema); if (!properties.isEmpty()) { - for (Entry entry : properties.entrySet()) { + for (Map.Entry entry : properties.entrySet()) { CodegenParameter codegenParameter; // key => property name // value => property schema @@ -8602,6 +8633,7 @@ private List getComposedProperties(List xOfCollection, } List xOf = new ArrayList<>(); Set dataTypeSet = new HashSet<>(); // to keep track of dataType + Set dataTypeSetIgnoringErasure = new HashSet<>(); int i = 0; for (Schema xOfSchema : xOfCollection) { CodegenProperty cp = fromProperty(collectionName + "_" + i, xOfSchema, false); @@ -8610,7 +8642,7 @@ private List getComposedProperties(List xOfCollection, if (dataTypeSet.contains(cp.dataType) || (isTypeErasedGenerics() && dataTypeSet.contains(cp.baseType))) { - // add "x-duplicated-data-type" to indicate if the dataType already occurs before + // add "x-duplicated-data-type" to indicate if the (base) dataType already occurs before // in other sub-schemas of allOf/anyOf/oneOf cp.vendorExtensions.putIfAbsent("x-duplicated-data-type", true); } else { @@ -8620,6 +8652,13 @@ private List getComposedProperties(List xOfCollection, dataTypeSet.add(cp.dataType); } } + if (dataTypeSetIgnoringErasure.contains(cp.dataType)) { + // add "x-duplicated-data-type-ignoring-erasure" to indicate if the dataType already occurs before + // in other sub-schemas of allOf/anyOf/oneOf + cp.vendorExtensions.putIfAbsent("x-duplicated-data-type-ignoring-erasure", true); + } else { + dataTypeSetIgnoringErasure.add(cp.dataType); + } } return xOf; } diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/api.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/api.mustache index 31c7facca9..62d3e9fbd7 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/api.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/api.mustache @@ -66,7 +66,7 @@ interface {{classname}} { {{{.}}} {{/vendorExtensions.x-operation-extra-annotation}} fun {{nickname}}({{#allParams}} - {{#formatSingleLine}}{{>client/params/queryParams}}{{>client/params/pathParams}}{{>client/params/headerParams}}{{>client/params/bodyParams}}{{>client/params/formParams}}{{>client/params/cookieParams}}{{^-last}},{{/-last}}{{/formatSingleLine}} + {{#formatSingleLine}}{{>client/params/queryParams}}{{>client/params/pathParams}}{{>client/params/headerParams}}{{>client/params/bodyParams}}{{>client/params/formParams}}{{>client/params/cookieParams}},{{/formatSingleLine}} {{/allParams}}){{#returnType}}: {{{returnType}}}{{/returnType}} {{/formatNoEmptyLines}} {{/operation}} diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/Authorization.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/Authorization.mustache index 64f9b1766f..935c027726 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/Authorization.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/Authorization.mustache @@ -27,5 +27,5 @@ annotation class Authorization( /** * The scopes for the oauth authorization. */ - val scopes: Array = [] + val scopes: Array = [], ) diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/AuthorizationBinder.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/AuthorizationBinder.mustache index 70ffa0abdb..2b833a4b53 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/AuthorizationBinder.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/AuthorizationBinder.mustache @@ -24,19 +24,20 @@ open class AuthorizationBinder : AnnotatedClientRequestBinder { return Authorization::class.java } - override fun bind(@NonNull context: MethodInvocationContext, - @NonNull uriContext: ClientRequestUriContext, - @NonNull request: MutableHttpRequest<*> + override fun bind( + @NonNull context: MethodInvocationContext, + @NonNull uriContext: ClientRequestUriContext, + @NonNull request: MutableHttpRequest<*>, ) { val annotations: List> = context.annotationMetadata - .getAnnotationValuesByType(Authorization::class.java) + .getAnnotationValuesByType(Authorization::class.java) if (annotations.isNotEmpty()) { val authorizationNames = ArrayList() for (ann in annotations) { ann.stringValue("value") - .filter{ s -> !s.isNullOrEmpty() } - .ifPresent { v -> authorizationNames.add(configurationName(v)) } + .filter{ s -> !s.isNullOrEmpty() } + .ifPresent { v -> authorizationNames.add(configurationName(v)) } } request.setAttribute(AUTHORIZATION_NAMES, authorizationNames) } @@ -47,6 +48,6 @@ open class AuthorizationBinder : AnnotatedClientRequestBinder { } companion object { - var AUTHORIZATION_NAMES = "micronaut.security.AUTHORIZATION_NAMES" + const val AUTHORIZATION_NAMES = "micronaut.security.AUTHORIZATION_NAMES" } } diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/AuthorizationFilter.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/AuthorizationFilter.mustache index 97dea3c39e..bd989d1e65 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/AuthorizationFilter.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/AuthorizationFilter.mustache @@ -45,15 +45,15 @@ open class AuthorizationFilter( init { clientConfigurationByName = clientConfigurations - .filter { it.isEnabled } - .collect(Collectors.toMap({ it.name }, { it })) - tokenPropagatorByName = HashMap() - clientCredentialsClientByName = HashMap() + .filter { it.isEnabled } + .collect(Collectors.toMap({ it.name }, { it })) + tokenPropagatorByName = mutableMapOf() + clientCredentialsClientByName = mutableMapOf() authorizationsByName = configurableAuthorizations - .collect(Collectors.toMap({ it.name }, { it })) + .collect(Collectors.toMap({ it.name }, { it })) } - override fun doFilter(request: @NonNull MutableHttpRequest<*>, chain: @NonNull ClientFilterChain): Publisher?> { + override fun doFilter(@NonNull request: MutableHttpRequest<*>, @NonNull chain: ClientFilterChain): Publisher?> { val names = request.getAttribute(AuthorizationBinder.AUTHORIZATION_NAMES, MutableList::class.java).orElse(null) if (names.isNullOrEmpty()) { return chain.proceed(request) diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/Authorizations.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/Authorizations.mustache index e437d71dea..d7b2b6d187 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/Authorizations.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/Authorizations.mustache @@ -19,5 +19,5 @@ import {{javaxPackage}}.annotation.Generated @Bindable annotation class Authorizations( - val value: Array + val value: Array, ) diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/configuration/ApiKeyAuthConfiguration.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/configuration/ApiKeyAuthConfiguration.mustache index 0c0689dd64..93a26a7a87 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/configuration/ApiKeyAuthConfiguration.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/configuration/ApiKeyAuthConfiguration.mustache @@ -15,31 +15,28 @@ import {{javaxPackage}}.annotation.Generated {{>common/generatedAnnotation}} {{/generatedAnnotation}} @EachProperty("security.api-key-auth") -data class ApiKeyAuthConfiguration @ConfigurationInject -constructor( - @Parameter override val name: String, - @NonNull var location: AuthKeyLocation, - @NonNull var paramName: String, - @NonNull var apiKey: String +data class ApiKeyAuthConfiguration @ConfigurationInject constructor( + @Parameter + override val name: String, + @NonNull + var location: AuthKeyLocation, + @NonNull + var paramName: String, + @NonNull + var apiKey: String, ) : ConfigurableAuthorization { override fun applyAuthorization(@NonNull request: MutableHttpRequest<*>) { when (location) { - AuthKeyLocation.HEADER -> { - request.header(paramName, apiKey) - } - AuthKeyLocation.QUERY -> { - request.parameters.add(paramName, apiKey) - } - AuthKeyLocation.COOKIE -> { - request.cookie(Cookie.of(paramName, apiKey)) - } + AuthKeyLocation.HEADER -> request.header(paramName, apiKey) + AuthKeyLocation.QUERY -> request.parameters.add(paramName, apiKey) + AuthKeyLocation.COOKIE -> request.cookie(Cookie.of(paramName, apiKey)) } } enum class AuthKeyLocation { HEADER, QUERY, - COOKIE + COOKIE, } } diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/configuration/HttpBasicAuthConfiguration.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/configuration/HttpBasicAuthConfiguration.mustache index 0ec240d6c2..18329664a9 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/configuration/HttpBasicAuthConfiguration.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/auth/configuration/HttpBasicAuthConfiguration.mustache @@ -15,9 +15,12 @@ import {{javaxPackage}}.annotation.Generated {{/generatedAnnotation}} @EachProperty("security.basic-auth") data class HttpBasicAuthConfiguration( - @Parameter override val name: String, - @NonNull var username: String, - @NonNull var password: String + @Parameter + override val name: String, + @NonNull + var username: String, + @NonNull + var password: String, ) : ConfigurableAuthorization { override fun applyAuthorization(@NonNull request: MutableHttpRequest<*>) { diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/params/bodyParams.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/params/bodyParams.mustache index 13bc87d40d..ad2f98a77d 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/params/bodyParams.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/params/bodyParams.mustache @@ -1 +1 @@ -{{#isBodyParam}}@Body {{>common/params/validation}}{{>client/params/type}}{{{paramName}}}: {{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{/isBodyParam}} \ No newline at end of file +{{#isBodyParam}}@Body {{>common/params/validation}}{{>client/params/type}}{{{paramName}}}: {{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{#vendorExtensions.defaultValueInit}} = {{{.}}}{{/vendorExtensions.defaultValueInit}}{{^vendorExtensions.defaultValueInit}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{^required}} = null{{/required}}{{/isNullable}}{{/vendorExtensions.defaultValueInit}}{{/isBodyParam}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/params/formParams.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/params/formParams.mustache index 23289e839b..72a300aaf1 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/params/formParams.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/client/params/formParams.mustache @@ -1 +1 @@ -{{#isFormParam}}{{>common/params/validation}}{{>client/params/type}}{{{paramName}}}: {{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{/isFormParam}} \ No newline at end of file +{{#isFormParam}}{{>common/params/validation}}{{>client/params/type}}{{{paramName}}}: {{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{#vendorExtensions.defaultValueInit}} = {{{.}}}{{/vendorExtensions.defaultValueInit}}{{^vendorExtensions.defaultValueInit}}{{#isNullable}} = null{{/isNullable}}{{^isNullable}}{{^required}} = null{{/required}}{{/isNullable}}{{/vendorExtensions.defaultValueInit}}{{/isFormParam}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/oneof_interface.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/oneof_interface.mustache index 89fa1d42f4..a83701c3da 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/oneof_interface.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/oneof_interface.mustache @@ -6,7 +6,7 @@ {{>common/generatedAnnotation}} {{/generatedAnnotation}} {{>common/model/typeInfoAnnotation}} -interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} : {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}}{{/formatNoEmptyLines}}{{#discriminator}} { +{{/formatNoEmptyLines}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} : {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}}{{#discriminator}} { val {{propertyName}}: {{propertyType}} } diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/typeInfoAnnotation.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/typeInfoAnnotation.mustache index 28c0d42172..609b49fdb0 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/typeInfoAnnotation.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/typeInfoAnnotation.mustache @@ -3,7 +3,7 @@ {{#jackson}} @JsonIgnoreProperties( value = ["{{{discriminator.propertyBaseName}}}"], // ignore manually set {{{discriminator.propertyBaseName}}}, it will be automatically generated by Jackson during serialization - allowSetters = true // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization + allowSetters = true, // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization ) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "{{{discriminator.propertyBaseName}}}", visible = true) {{#vendorExtensions.hasMappedModels}} @@ -13,4 +13,4 @@ {{/vendorExtensions.hasMappedModels}} {{/jackson}} {{/discriminator.propertyBaseName}} -{{/discriminator}} +{{/discriminator}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/operationAnnotations.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/operationAnnotations.mustache index 33bcd0de96..0ff4796820 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/operationAnnotations.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/operationAnnotations.mustache @@ -53,20 +53,20 @@ {{#responses}} ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = [ {{#produces}} - Content(mediaType = "{{{mediaType}}}", {{#isArray}}array = ArraySchema({{/isArray}}schema = Schema(implementation = {{{baseType}}}::class){{#isArray}}){{/isArray}}){{^-last}},{{/-last}} + Content(mediaType = "{{{mediaType}}}", {{#isArray}}array = ArraySchema({{/isArray}}schema = Schema(implementation = {{{baseType}}}::class){{#isArray}}){{/isArray}}), {{/produces}} - ]{{/baseType}}){{^-last}},{{/-last}} + ]{{/baseType}}), {{/responses}} - ]{{#vendorExtensions.hasNotBodyParam}}, + ],{{#vendorExtensions.hasNotBodyParam}} parameters = [ {{#vendorExtensions.swaggerParams}} - Parameter(name = "{{baseName}}"{{#isDeprecated}}, deprecated = true{{/isDeprecated}}{{#description}}, description = "{{{description}}}"{{/description}}{{#required}}, required = true{{/required}}, `in` = ParameterIn.{{#isCookieParam}}COOKIE{{/isCookieParam}}{{#isHeaderParam}}HEADER{{/isHeaderParam}}{{#isQueryParam}}QUERY{{/isQueryParam}}{{#isPathParam}}PATH{{/isPathParam}}){{^-last}},{{/-last}} + Parameter(name = "{{baseName}}"{{#isDeprecated}}, deprecated = true{{/isDeprecated}}{{#description}}, description = "{{{description}}}"{{/description}}{{#required}}, required = true{{/required}}, `in` = ParameterIn.{{#isCookieParam}}COOKIE{{/isCookieParam}}{{#isHeaderParam}}HEADER{{/isHeaderParam}}{{#isQueryParam}}QUERY{{/isQueryParam}}{{#isPathParam}}PATH{{/isPathParam}}), {{/vendorExtensions.swaggerParams}} - ]{{/vendorExtensions.hasNotBodyParam}}{{#hasAuthMethods}}, + ],{{/vendorExtensions.hasNotBodyParam}}{{#hasAuthMethods}} security = [ {{#authMethods}} - SecurityRequirement(name = "{{name}}"{{#isOAuth}}{{#scopes.1}}, scopes = [{{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}}]{{/scopes.1}}{{/isOAuth}}){{^-last}},{{/-last}} + SecurityRequirement(name = "{{name}}"{{#isOAuth}}{{#scopes.1}}, scopes = [{{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}}]{{/scopes.1}}{{/isOAuth}}), {{/authMethods}} - ]{{/hasAuthMethods}} + ],{{/hasAuthMethods}} ) {{/generateSwagger2Annotations}} diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-interface.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-interface.mustache index be60ee8fb0..1c0cbb6f7a 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-interface.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-interface.mustache @@ -78,7 +78,7 @@ interface {{classname}} { {{#isDeprecated}} @java.lang.Deprecated {{/isDeprecated}} - {{{paramName}}}: {{#isEnum}}{{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.typeWithGenericAnnotations}}}{{/isEnum}}{{^-last}},{{/-last}}{{/formatSingleLine}} + {{{paramName}}}: {{#isEnum}}{{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.typeWithGenericAnnotations}}}{{/isEnum}},{{/formatSingleLine}} {{/allParams}}){{#returnType}}: {{{returnType}}}{{/returnType}} {{/formatNoEmptyLines}} diff --git a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java index 7a367f1011..bc13a5a9a4 100644 --- a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java +++ b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java @@ -397,9 +397,11 @@ void testDiscriminatorConstructorBug() { assertFileContains(apiPath + "BookInfo.kt", """ open class BookInfo( + @field:NotNull @field:JsonProperty(JSON_PROPERTY_NAME) open var name: String, + @field:NotNull @field:JsonProperty(JSON_PROPERTY_TYPE) open var type: BookInfoType? = null, @@ -407,10 +409,12 @@ open class BookInfo( assertFileContains(apiPath + "BasicBookInfo.kt", """ open class BasicBookInfo( + @field:NotNull @field:Size(min = 3) @field:JsonProperty(JSON_PROPERTY_AUTHOR) open var author: String, + @field:NotNull @field:JsonProperty(JSON_PROPERTY_NAME) override var name: String, @@ -419,14 +423,17 @@ open class BasicBookInfo( assertFileContains(apiPath + "DetailedBookInfo.kt", """ data class DetailedBookInfo( + @field:NotNull @field:Pattern(regexp = "[0-9]{13}") @field:JsonProperty(JSON_PROPERTY_ISBN) var isbn: String, + @field:NotNull @field:Size(min = 3) @field:JsonProperty(JSON_PROPERTY_AUTHOR) override var author: String, + @field:NotNull @field:JsonProperty(JSON_PROPERTY_NAME) override var name: String, @@ -622,7 +629,7 @@ void testFileDownloadEndpoint() { assertFileContains(apiPath + "DefaultApi.kt", """ fun fetchData( - @PathVariable("id") @NotNull @Min(0L) id: Long + @PathVariable("id") @NotNull @Min(0L) id: Long, ): Mono>> """); } @@ -754,7 +761,7 @@ void testMultipartFormData() { fun profilePasswordPost( @Header("WCToken") @NotNull wcToken: String, @Header("WCTrustedToken") @NotNull wcTrustedToken: String, - @Body @Nullable multipartBody: MultipartBody? + @Body @Nullable multipartBody: MultipartBody? = null, ): Mono """); } @@ -771,7 +778,7 @@ void testGenerateByMultipleFiles() { @Post("/api/customer/{id}/files") fun uploadFile( @PathVariable("id") @NotNull id: UUID, - @Body @NotNull @Valid fileCreateDto: FileCreateDto + @Body @NotNull @Valid fileCreateDto: FileCreateDto, ): Mono> """); assertFileContains(path + "model/FileCreateDto.kt", @@ -804,22 +811,22 @@ void testMultipleContentTypesEndpoints() { @Post("/multiplecontentpath") @Produces("application/json", "application/xml") fun myOp( - @Body @Nullable @Valid coordinates: Coordinates? + @Body @Nullable @Valid coordinates: Coordinates? = null, ): Mono> """, """ @Post("/multiplecontentpath") @Produces("multipart/form-data") fun myOp_1( - @Nullable @Valid coordinates: Coordinates?, - @Nullable file: ByteArray? + @Nullable @Valid coordinates: Coordinates? = null, + @Nullable file: ByteArray? = null, ): Mono> """, """ @Post("/multiplecontentpath") @Produces("application/yaml", "text/json") fun myOp_2( - @Body @Nullable @Valid mySchema: MySchema? + @Body @Nullable @Valid mySchema: MySchema? = null, ): Mono> """); } @@ -1077,21 +1084,21 @@ void testDeprecated() { description = "A method to send primitives as request parameters", responses = [ ApiResponse(responseCode = "200", description = "Success", content = [ - Content(mediaType = "application/json", schema = Schema(implementation = SendPrimitivesResponse::class)) + Content(mediaType = "application/json", schema = Schema(implementation = SendPrimitivesResponse::class)), ]), - ApiResponse(responseCode = "default", description = "An unexpected error has occurred") + ApiResponse(responseCode = "default", description = "An unexpected error has occurred"), ], parameters = [ Parameter(name = "name", deprecated = true, required = true, `in` = ParameterIn.PATH), Parameter(name = "age", required = true, `in` = ParameterIn.QUERY), - Parameter(name = "height", deprecated = true, required = true, `in` = ParameterIn.HEADER) - ] + Parameter(name = "height", deprecated = true, required = true, `in` = ParameterIn.HEADER), + ], ) @Get("/sendPrimitives/{name}") fun sendPrimitives( @PathVariable("name") @NotNull @java.lang.Deprecated name: String, @QueryValue("age") @NotNull age: BigDecimal, - @Header("height") @NotNull @java.lang.Deprecated height: Float + @Header("height") @NotNull @java.lang.Deprecated height: Float, ): Mono """); @@ -1319,25 +1326,64 @@ void testSwaggerAnnotations() { responses = [ ApiResponse(responseCode = "200", description = "successful operation", content = [ Content(mediaType = "application/json", array = ArraySchema(schema = Schema(implementation = Pet::class))), - Content(mediaType = "application/xml", array = ArraySchema(schema = Schema(implementation = Pet::class))) + Content(mediaType = "application/xml", array = ArraySchema(schema = Schema(implementation = Pet::class))), ]), - ApiResponse(responseCode = "400", description = "Invalid status value") + ApiResponse(responseCode = "400", description = "Invalid status value"), ], parameters = [ - Parameter(name = "status", description = "Status values that need to be considered for filter", `in` = ParameterIn.QUERY) + Parameter(name = "status", description = "Status values that need to be considered for filter", `in` = ParameterIn.QUERY), ], security = [ - SecurityRequirement(name = "petstore_auth", scopes = ["write:pets", "read:pets"]) - ] + SecurityRequirement(name = "petstore_auth", scopes = ["write:pets", "read:pets"]), + ], ) @Get("/pet/findByStatus") @Consumes("application/json", "application/xml") fun findPetsByStatus( - @QueryValue("status") @Nullable status: List<@NotNull String>? = null + @QueryValue("status") @Nullable status: List<@NotNull String>? = null, ): Mono> """); } + @Test + void testDiscriminatorOverride() { + + var codegen = new KotlinMicronautClientCodegen(); + codegen.setGenerateSwaggerAnnotations(true); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/test-override-discriminator.yml", CodegenConstants.APIS, CodegenConstants.MODELS); + String path = outputPath + "src/main/kotlin/org/openapitools/"; + + assertFileContains(path + "model/AnimalRequest.kt", + """ + @field:Nullable + @field:Schema(name = "name", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @field:JsonProperty(JSON_PROPERTY_NAME) + @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) + open var name: String? = null, + + @field:Nullable + @field:Schema(name = "valueType", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @field:JsonProperty(JSON_PROPERTY_VALUE_TYPE) + @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) + open var valueType: String? = null, + """); + + assertFileContains(path + "model/AnimalResponse.kt", + """ + @field:Nullable + @field:Schema(name = "name", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @field:JsonProperty(JSON_PROPERTY_NAME) + @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) + open var name: String? = null, + + @field:Nullable + @field:Schema(name = "valueType", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @field:JsonProperty(JSON_PROPERTY_VALUE_TYPE) + @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) + open var valueType: String? = null, + """); + } + @Test void testEquals() { diff --git a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java index 946f690845..ea39e2d0da 100644 --- a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java +++ b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java @@ -608,7 +608,7 @@ fun profilePasswordPost( @Header("WCToken") @NotNull wcToken: String, @Header("WCTrustedToken") @NotNull wcTrustedToken: String, @Part("name") @Nullable name: String?, - @Part("file") @Nullable file: CompletedFileUpload? + @Part("file") @Nullable file: CompletedFileUpload?, ): Mono """); } @@ -625,7 +625,7 @@ void testMultipleContentTypesEndpoints() { @Consumes("application/json", "application/xml") @Secured(SecurityRule.IS_ANONYMOUS) fun myOp( - @Body @Nullable @Valid coordinates: Coordinates? + @Body @Nullable @Valid coordinates: Coordinates?, ): Mono> """, """ @@ -634,7 +634,7 @@ fun myOp( @Secured(SecurityRule.IS_ANONYMOUS) fun myOp_1( @Nullable @Valid coordinates: Coordinates?, - @Nullable file: CompletedFileUpload? + @Nullable file: CompletedFileUpload?, ): Mono> """, """ @@ -642,7 +642,7 @@ fun myOp_1( @Consumes("application/yaml", "text/json") @Secured(SecurityRule.IS_ANONYMOUS) fun myOp_2( - @Body @Nullable @Valid mySchema: MySchema? + @Body @Nullable @Valid mySchema: MySchema?, ): Mono> """); } @@ -694,22 +694,22 @@ void testDeprecated() { description = "A method to send primitives as request parameters", responses = [ ApiResponse(responseCode = "200", description = "Success", content = [ - Content(mediaType = "application/json", schema = Schema(implementation = SendPrimitivesResponse::class)) + Content(mediaType = "application/json", schema = Schema(implementation = SendPrimitivesResponse::class)), ]), - ApiResponse(responseCode = "default", description = "An unexpected error has occurred") + ApiResponse(responseCode = "default", description = "An unexpected error has occurred"), ], parameters = [ Parameter(name = "name", deprecated = true, required = true, `in` = ParameterIn.PATH), Parameter(name = "age", required = true, `in` = ParameterIn.QUERY), - Parameter(name = "height", deprecated = true, required = true, `in` = ParameterIn.HEADER) - ] + Parameter(name = "height", deprecated = true, required = true, `in` = ParameterIn.HEADER), + ], ) @Get("/sendPrimitives/{name}") @Secured(SecurityRule.IS_ANONYMOUS) fun sendPrimitives( @PathVariable("name") @NotNull @java.lang.Deprecated name: String, @QueryValue("age") @NotNull age: BigDecimal, - @Header("height") @NotNull @java.lang.Deprecated height: Float + @Header("height") @NotNull @java.lang.Deprecated height: Float, ): Mono """); } @@ -774,22 +774,22 @@ void testSwaggerAnnotations() { responses = [ ApiResponse(responseCode = "200", description = "successful operation", content = [ Content(mediaType = "application/json", array = ArraySchema(schema = Schema(implementation = Pet::class))), - Content(mediaType = "application/xml", array = ArraySchema(schema = Schema(implementation = Pet::class))) + Content(mediaType = "application/xml", array = ArraySchema(schema = Schema(implementation = Pet::class))), ]), - ApiResponse(responseCode = "400", description = "Invalid status value") + ApiResponse(responseCode = "400", description = "Invalid status value"), ], parameters = [ - Parameter(name = "status", description = "Status values that need to be considered for filter", `in` = ParameterIn.QUERY) + Parameter(name = "status", description = "Status values that need to be considered for filter", `in` = ParameterIn.QUERY), ], security = [ - SecurityRequirement(name = "petstore_auth", scopes = ["write:pets", "read:pets"]) - ] + SecurityRequirement(name = "petstore_auth", scopes = ["write:pets", "read:pets"]), + ], ) @Get("/pet/findByStatus") @Produces("application/json", "application/xml") @Secured("write:pets", "read:pets") fun findPetsByStatus( - @QueryValue("status") @Nullable status: List<@NotNull String>? + @QueryValue("status") @Nullable status: List<@NotNull String>?, ): Mono> """); } From d60192d29eba74e028dd3696fac183fe6ec45b00 Mon Sep 17 00:00:00 2001 From: altro3 Date: Sat, 30 Nov 2024 18:17:23 +0700 Subject: [PATCH 2/3] Refactor generating kotlin POJO's --- .../AbstractMicronautKotlinCodegen.java | 171 +++++++++++++++--- .../io/micronaut/openapi/generator/Utils.java | 3 + .../common/model/field_annotations.mustache | 2 +- .../common/model/pojo.mustache | 31 +--- .../JavaMicronautClientCodegenTest.java | 4 +- .../KotlinMicronautClientCodegenTest.java | 140 ++++++++------ .../KotlinMicronautServerCodegenTest.java | 148 +++++++++++---- ...{discirminator2.yml => discriminator2.yml} | 0 8 files changed, 361 insertions(+), 138 deletions(-) rename openapi-generator/src/test/resources/3_0/{discirminator2.yml => discriminator2.yml} (100%) diff --git a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java index 9a55552180..1fe4e3d76f 100644 --- a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java +++ b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java @@ -814,6 +814,11 @@ protected CodegenDiscriminator createDiscriminator(String schemaName, Schema sch break; } } + var type = discriminator.getPropertyType(); + if (!type.endsWith("?")) { + type = type + "?"; + } + discriminator.setPropertyType(type); return discriminator; } @@ -1504,6 +1509,13 @@ public CodegenProperty fromProperty(String name, Schema schema, boolean required defaultValueInit = property.dataType + "." + enumVarName; } } + if (defaultValueInit == null && property.isDiscriminator + || property.isNullable + || property.required && property.isReadOnly + || !property.required + ) { + defaultValueInit = "null"; + } if (defaultValueInit != null) { property.vendorExtensions.put("defaultValueInit", defaultValueInit); } @@ -1804,7 +1816,7 @@ public Map postProcessAllModels(Map objs) var requiredParentVarsWithoutDiscriminator = new ArrayList(); var allVars = new ArrayList(); - processParentModel(model, requiredVarsWithoutDiscriminator, requiredParentVarsWithoutDiscriminator, allVars, false); + processParentModel(model, requiredVarsWithoutDiscriminator, requiredParentVarsWithoutDiscriminator, allVars, false, objs); model.allVars = allVars; var withInheritance = model.hasChildren || model.parent != null; @@ -1815,7 +1827,7 @@ public Map postProcessAllModels(Map objs) for (var v : model.vars) { v.vendorExtensions.put("hasChildren", model.hasChildren); if (!notContainsProp(v, requiredVarsWithoutDiscriminator) - || (!model.hasChildren || (v.required && !v.isDiscriminator))) { + || (!model.hasChildren || v.required)) { if (notContainsProp(v, requiredVars)) { requiredVars.add(v); } @@ -1831,6 +1843,7 @@ public Map postProcessAllModels(Map objs) if (!requiredParentVarsWithoutDiscriminator.isEmpty()) { model.vendorExtensions.put("requiredParentVarsWithoutDiscriminator", requiredParentVarsWithoutDiscriminator); } + model.vendorExtensions.put("requiredVarsWithoutDiscriminator", requiredVarsWithoutDiscriminator); model.vendorExtensions.put("requiredVars", requiredVars); model.vendorExtensions.put("withRequiredOrOptionalVars", !requiredVarsWithoutDiscriminator.isEmpty() || !optionalVars.isEmpty()); @@ -1851,6 +1864,18 @@ public Map postProcessAllModels(Map objs) if (model.parentVars != null) { for (var property : model.parentVars) { processProperty(property, isServer, model, objs); + removePropIfContains(property, optionalVars); + } + } + + for (var parentVar : requiredParentVarsWithoutDiscriminator) { + var childVar = findProp(parentVar, requiredVarsWithoutDiscriminator); + if (childVar != null) { + var isParentVar = !isParentOneOfInterface(model); + childVar.vendorExtensions.put("isParentVar", isParentVar); + if (isParentVar) { + childVar.vendorExtensions.put("fieldAnnPrefix", ""); + } } } @@ -1865,6 +1890,7 @@ public Map postProcessAllModels(Map objs) model.vendorExtensions.put("withRequiredVars", false); model.vars = Collections.emptyList(); } + model.vendorExtensions.put("dataClass", !model.hasChildren && model.hasVars && (!withInheritance || model.parentVars.isEmpty()) ? "data " : ""); addStrValueToEnum(model); } @@ -1874,6 +1900,29 @@ public Map postProcessAllModels(Map objs) processOneOfModels(model, objs.values()); } + for (ModelsMap models : objs.values()) { + CodegenModel model = models.getModels().get(0).getModel(); + var requiredVarsWithoutDiscriminator = (List) model.vendorExtensions.get("requiredVarsWithoutDiscriminator"); + var requiredParentVarsWithoutDiscriminator = (List) model.vendorExtensions.get("requiredParentVarsWithoutDiscriminator"); + var optionalVars = (List) model.vendorExtensions.get("optionalVars"); + var withInheritance = (Boolean) model.vendorExtensions.get("withInheritance"); + + if (withInheritance) { + for (var optionalVar : optionalVars) { + var found = false; + for (var reqVar : requiredVarsWithoutDiscriminator) { + if (optionalVar.name.equals(reqVar.name)) { + found = true; + break; + } + } + if (!found) { + requiredVarsWithoutDiscriminator.add(optionalVar); + } + } + } + } + return objs; } @@ -1940,6 +1989,20 @@ protected void updateEnumVarsWithExtensions(List> enumVars, } } + private boolean isParentOneOfInterface(CodegenModel model) { + var parentModel = model.parentModel; + var parentIsOneOfInterface = parentModel != null && Boolean.TRUE.equals(parentModel.getVendorExtensions().get("x-is-one-of-interface")); + if (!parentIsOneOfInterface && model.interfaceModels != null) { + for (var interfaceModel : model.interfaceModels) { + if (Boolean.TRUE.equals(interfaceModel.getVendorExtensions().get("x-is-one-of-interface"))) { + parentIsOneOfInterface = true; + break; + } + } + } + return parentIsOneOfInterface; + } + private void processOneOfModels(CodegenModel model, Collection models) { if (!model.vendorExtensions.containsKey("x-is-one-of-interface") @@ -1970,7 +2033,7 @@ private void processOneOfModels(CodegenModel model, Collection models if (prop.name.equals(discriminator.getPropertyName())) { prop.isDiscriminator = true; prop.isOverridden = true; - prop.isNullable = false; + prop.isNullable = true; prop.isOptional = false; prop.required = true; prop.isReadOnly = false; @@ -1985,7 +2048,7 @@ private void processOneOfModels(CodegenModel model, Collection models if (prop.name.equals(discriminator.getPropertyName())) { prop.isDiscriminator = true; prop.isOverridden = true; - prop.isNullable = false; + prop.isNullable = true; prop.isOptional = false; prop.required = true; prop.isReadOnly = false; @@ -2005,7 +2068,9 @@ private void processProperty(CodegenProperty property, boolean isServer, Codegen property.vendorExtensions.put("inRequiredArgsConstructor", !property.isReadOnly || isServer); property.vendorExtensions.put("isServer", isServer); property.vendorExtensions.put("defaultValueIsNotNull", property.defaultValue != null && !property.defaultValue.equals("null")); - property.vendorExtensions.put("fieldAnnPrefix", ksp ? "" : "field:"); + + var isParentVar = (Boolean) property.vendorExtensions.get("isParentVar"); + property.vendorExtensions.put("fieldAnnPrefix", isParentVar != null && isParentVar ? "" : "field:"); property.vendorExtensions.put("x-implements", model.vendorExtensions.get("x-implements")); if ("null".equals(property.example)) { property.example = null; @@ -2023,6 +2088,10 @@ private void processProperty(CodegenProperty property, boolean isServer, Codegen property.pattern = null; } + if (isDiscriminator(property, model)) { + property.isDiscriminator = true; + } + processGenericAnnotations(property, useBeanValidation, false, property.isNullable || property.isDiscriminator, property.required, property.isReadOnly, true); @@ -2033,7 +2102,8 @@ private void processProperty(CodegenProperty property, boolean isServer, Codegen private void processParentModel(CodegenModel model, List requiredVarsWithoutDiscriminator, List requiredParentVarsWithoutDiscriminator, List allVars, - boolean processParentModel) { + boolean processParentModel, + Map objs) { var parent = model.parentModel; var hasParent = parent != null; @@ -2046,14 +2116,14 @@ private void processParentModel(CodegenModel model, List requir var parentIsOneOfInterface = hasParent && Boolean.TRUE.equals(parent.getVendorExtensions().get("x-is-one-of-interface")); if (!processParentModel) { - processVar(model, model.vars, requiredVarsWithoutDiscriminator, requiredParentVarsWithoutDiscriminator, processParentModel); + processVar(model, model.vars, requiredVarsWithoutDiscriminator, requiredParentVarsWithoutDiscriminator, processParentModel, objs); } - processVar(model, model.requiredVars, requiredVarsWithoutDiscriminator, requiredParentVarsWithoutDiscriminator, processParentModel); + processVar(model, model.vars, requiredVarsWithoutDiscriminator, requiredParentVarsWithoutDiscriminator, processParentModel, objs); requiredParentVarsWithoutDiscriminator(model, requiredParentVarsWithoutDiscriminator); if (hasParent) { model.parentVars = parent.allVars; - processParentModel(parent, requiredVarsWithoutDiscriminator, requiredParentVarsWithoutDiscriminator, allVars, true); + processParentModel(parent, requiredVarsWithoutDiscriminator, requiredParentVarsWithoutDiscriminator, allVars, true, objs); } if (parentIsOneOfInterface) { @@ -2077,22 +2147,40 @@ private void processParentModel(CodegenModel model, List requir } } - private void processVar(CodegenModel model, List vars, List requiredVarsWithoutDiscriminator, List requiredParentVarsWithoutDiscriminator, boolean processParentModel) { + private void processVar(CodegenModel model, List vars, List requiredVarsWithoutDiscriminator, + List requiredParentVarsWithoutDiscriminator, + boolean processParentModel, + Map objs) { var parent = model.parentModel; var hasParent = parent != null; var parentRequiredVars = hasParent ? (List) parent.vendorExtensions.get("requiredVarsWithoutDiscriminator") : Collections.emptyList(); - var parentIsOneOfInterface = hasParent && Boolean.TRUE.equals(parent.getVendorExtensions().get("x-is-one-of-interface")); + var parentIsOneOfInterface = isParentOneOfInterface(model); + var isParentVar = !parentIsOneOfInterface; for (var v : vars) { - if (notContainsProp(v, requiredVarsWithoutDiscriminator) && (!isDiscriminator(v, model) || parentIsOneOfInterface)) { + processProperty(v, isServer(), model, objs); + + if (notContainsProp(v, requiredVarsWithoutDiscriminator)) { var copyVar = v; + var isDiscriminator = isDiscriminator(v, model); + if (isDiscriminator) { + copyVar.isDiscriminator = true; + } if ((hasParent && !useOneOfInterfaces && !notContainsProp(v, parent.allVars)) || (v.isOverridden != null && !v.isOverridden && !v.vendorExtensions.containsKey("overridden") && notContainsProp(v, vars))) { copyVar = v.clone(); + copyVar.vendorExtensions.put("isParentVar", isParentVar); + if (isParentVar) { + copyVar.vendorExtensions.put("fieldAnnPrefix", ""); + } + if (isDiscriminator && !parentIsOneOfInterface) { + copyVar.isNullable = true; + copyVar.vendorExtensions.put("defaultValueInit", "null"); + } copyVar.isOverridden = true; copyVar.vendorExtensions.put("overridden", true); } @@ -2101,14 +2189,22 @@ private void processVar(CodegenModel model, List vars, List props) { + if (props == null || props.isEmpty()) { + return; + } + props.removeIf(p -> prop.name.equals(p.name)); + } + + private CodegenProperty findProp(CodegenProperty prop, List props) { + if (props == null || props.isEmpty()) { + return null; + } + for (var p : props) { + if (prop.name.equals(p.name)) { + return p; } } + return null; } private boolean notContainsProp(CodegenProperty prop, List props) { diff --git a/openapi-generator/src/main/java/io/micronaut/openapi/generator/Utils.java b/openapi-generator/src/main/java/io/micronaut/openapi/generator/Utils.java index abcce760ef..ff5ce2d082 100644 --- a/openapi-generator/src/main/java/io/micronaut/openapi/generator/Utils.java +++ b/openapi-generator/src/main/java/io/micronaut/openapi/generator/Utils.java @@ -441,6 +441,9 @@ private static List normalizeExtraAnnotations(String prefix, Collection< } private static String normalizeExtraAnnotation(String prefix, String annotationStr) { + if (annotationStr.startsWith(prefix)) { + return annotationStr; + } return prefix + (annotationStr.startsWith("@") ? annotationStr.substring(1) : annotationStr); } diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/field_annotations.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/field_annotations.mustache index a3611d5a2c..86b03672ac 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/field_annotations.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/field_annotations.mustache @@ -13,7 +13,7 @@ {{#isContainer}} // Is a container wrapped = {{isXmlWrapped}} {{#items}} - // items.name = {{name}} items.baseName = {{baseName}} items.xmlName = {{xmlName}} items.xmlNamespace = {{xmlNamespace}} + // items.name = {{{name}}} items.baseName = {{baseName}} items.xmlName = {{xmlName}} items.xmlNamespace = {{xmlNamespace}} // items.example = {{example}} items.type = {{dataType}} @{{{vendorExtensions.fieldAnnPrefix}}}XmlElement({{#xmlNamespace}}namespace = "{{xmlNamespace}}", {{/xmlNamespace}}name = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") {{/items}} diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/pojo.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/pojo.mustache index 9253c624ee..0446ec97b1 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/pojo.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/common/model/pojo.mustache @@ -45,7 +45,7 @@ {{/vendorExtensions.x-class-extra-annotation}} {{/formatNoEmptyLines}} {{!Declare the class with extends and implements}} -{{#nonPublicApi}}internal {{/nonPublicApi}}{{#hasChildren}}open {{/hasChildren}}{{^hasChildren}}{{#hasVars}}data {{/hasVars}}{{/hasChildren}}class {{classname}}{{#hasVars}}({{/hasVars}} +{{#nonPublicApi}}internal {{/nonPublicApi}}{{#hasChildren}}open {{/hasChildren}}{{vendorExtensions.dataClass}}class {{classname}}{{#hasVars}}({{/hasVars}} {{#vendorExtensions.requiredVarsWithoutDiscriminator}} {{#formatNoEmptyLines}} @@ -66,34 +66,9 @@ {{/deprecated}} {{/description}} {{>common/model/field_annotations}} - {{#vendorExtensions.overridden}}override {{/vendorExtensions.overridden}}{{^vendorExtensions.overridden}}{{#hasChildren}}open {{/hasChildren}}{{/vendorExtensions.overridden}}var {{{name}}}: {{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{#vendorExtensions.defaultValueInit}} = {{{.}}}{{/vendorExtensions.defaultValueInit}}{{^vendorExtensions.defaultValueInit}}{{#isNullable}} = null{{/isNullable}}{{/vendorExtensions.defaultValueInit}}, + {{^vendorExtensions.isParentVar}}{{#vendorExtensions.overridden}}override {{/vendorExtensions.overridden}}{{^vendorExtensions.overridden}}{{#hasChildren}}open {{/hasChildren}}{{/vendorExtensions.overridden}}var {{/vendorExtensions.isParentVar}}{{{name}}}: {{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{#vendorExtensions.defaultValueInit}} = {{{.}}}{{/vendorExtensions.defaultValueInit}}{{^vendorExtensions.defaultValueInit}}{{#isNullable}} = null{{/isNullable}}{{/vendorExtensions.defaultValueInit}}, {{/formatNoEmptyLines}} {{/vendorExtensions.requiredVarsWithoutDiscriminator}} -{{#vendorExtensions.withInheritance}} - {{#vendorExtensions.optionalVars}} - - {{#formatNoEmptyLines}} - {{#description}} - /** - * {{description}} - {{#deprecated}} - * - * @deprecated{{#vendorExtensions.x-deprecated-message}} {{{.}}}{{/vendorExtensions.x-deprecated-message}} - {{/deprecated}} - */ - {{/description}} - {{^description}} - {{#deprecated}} - /** - * @deprecated{{#vendorExtensions.x-deprecated-message}} {{{.}}}{{/vendorExtensions.x-deprecated-message}} - */ - {{/deprecated}} - {{/description}} - {{>common/model/field_annotations}} - {{#vendorExtensions.overridden}}override {{/vendorExtensions.overridden}}{{^vendorExtensions.overridden}}{{#vendorExtensions.hasChildren}}open {{/vendorExtensions.hasChildren}}{{/vendorExtensions.overridden}}var {{{name}}}: {{{vendorExtensions.typeWithEnumWithGenericAnnotations}}} = {{#vendorExtensions.defaultValueInit}}{{{.}}}{{/vendorExtensions.defaultValueInit}}{{^vendorExtensions.defaultValueInit}}null{{/vendorExtensions.defaultValueInit}}, - {{/formatNoEmptyLines}} - {{/vendorExtensions.optionalVars}} -{{/vendorExtensions.withInheritance}} {{#hasVars}}){{/hasVars}}{{#parent}} : {{{parent}}}({{#vendorExtensions.requiredParentVarsWithoutDiscriminator}}{{{name}}}{{^-last}}, {{/-last}}{{/vendorExtensions.requiredParentVarsWithoutDiscriminator}}){{/parent}}{{#vendorExtensions.x-implements}}{{#parent}}, {{/parent}}{{^parent}}: {{/parent}}{{^-first}}, {{/-first}}{{{.}}}{{/vendorExtensions.x-implements}} {{openbrace}} {{#vendorExtensions.withInheritance}} @@ -117,7 +92,7 @@ } override fun hashCode(): Int = - Objects.hash({{#vars}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#vendorExtensions.hasOwnVars}}, {{/vendorExtensions.hasOwnVars}}super.hashCode(){{/parent}}) + Objects.hash({{#vars}}{{^isByteArray}}{{{name}}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{{name}}}){{/isByteArray}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#vendorExtensions.hasOwnVars}}, {{/vendorExtensions.hasOwnVars}}super.hashCode(){{/parent}}) override fun toString(): String = {{#hasVars}} diff --git a/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenTest.java b/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenTest.java index e1774f2059..a97e93219c 100644 --- a/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenTest.java +++ b/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenTest.java @@ -647,7 +647,7 @@ void testDiscriminatorWithoutUseOneOfInterfaces() { var codegen = new JavaMicronautClientCodegen(); codegen.setUseOneOfInterfaces(false); - String outputPath = generateFiles(codegen, "src/test/resources/3_0/discirminator2.yml", CodegenConstants.MODELS); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/discriminator2.yml", CodegenConstants.MODELS); String path = outputPath + "src/main/java/org/openapitools/"; assertFileContains(path + "model/JsonOp.java", @@ -677,7 +677,7 @@ void testDiscriminatorWithUseOneOfInterfaces() { var codegen = new JavaMicronautClientCodegen(); codegen.setUseOneOfInterfaces(true); - String outputPath = generateFiles(codegen, "src/test/resources/3_0/discirminator2.yml", CodegenConstants.MODELS); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/discriminator2.yml", CodegenConstants.MODELS); String path = outputPath + "src/main/java/org/openapitools/"; assertFileContains(path + "model/JsonOp.java", diff --git a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java index bc13a5a9a4..9369d19ff8 100644 --- a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java +++ b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java @@ -313,22 +313,27 @@ void testReadOnlyConstructorBug() { assertFileContains(apiPath + "BookInfo.kt", """ open class BookInfo( + @field:NotNull @field:JsonProperty(JSON_PROPERTY_NAME) open var name: String, + @field:Nullable @field:JsonProperty(JSON_PROPERTY_REQUIRED_READ_ONLY) @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) - open var requiredReadOnly: String?, + open var requiredReadOnly: String? = null, + @field:Nullable @field:Size(min = 3) @field:JsonProperty(JSON_PROPERTY_AUTHOR) @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) open var author: String? = null, + @field:Nullable @field:JsonProperty(JSON_PROPERTY_OPTIONAL_READ_ONLY) @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) open var optionalReadOnly: String? = null, + @field:Nullable @field:JsonProperty(JSON_PROPERTY_TYPE) @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) @@ -337,19 +342,38 @@ open class BookInfo( """); assertFileContains(apiPath + "ExtendedBookInfo.kt", """ - data class ExtendedBookInfo( + class ExtendedBookInfo( + @field:NotNull @field:Pattern(regexp = "[0-9]{13}") @field:JsonProperty(JSON_PROPERTY_ISBN) var isbn: String, - @field:NotNull - @field:JsonProperty(JSON_PROPERTY_NAME) - override var name: String, - @field:Nullable - @field:JsonProperty(JSON_PROPERTY_REQUIRED_READ_ONLY) - @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) - override var requiredReadOnly: String?, - ) : BookInfo(name, requiredReadOnly) { + + @NotNull + @JsonProperty(JSON_PROPERTY_NAME) + name: String, + + @Nullable + @JsonProperty(JSON_PROPERTY_REQUIRED_READ_ONLY) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + requiredReadOnly: String? = null, + + @Nullable + @Size(min = 3) + @JsonProperty(JSON_PROPERTY_AUTHOR) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + author: String? = null, + + @Nullable + @JsonProperty(JSON_PROPERTY_OPTIONAL_READ_ONLY) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + optionalReadOnly: String? = null, + + @Nullable + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + type: BookInfoType? = null, + ) : BookInfo(name, requiredReadOnly, author, optionalReadOnly, type) { """); } @@ -397,47 +421,56 @@ void testDiscriminatorConstructorBug() { assertFileContains(apiPath + "BookInfo.kt", """ open class BookInfo( - + @field:NotNull @field:JsonProperty(JSON_PROPERTY_NAME) open var name: String, - + @field:NotNull @field:JsonProperty(JSON_PROPERTY_TYPE) open var type: BookInfoType? = null, - ) {"""); + ) { + """); assertFileContains(apiPath + "BasicBookInfo.kt", """ open class BasicBookInfo( - + @field:NotNull @field:Size(min = 3) @field:JsonProperty(JSON_PROPERTY_AUTHOR) open var author: String, - - @field:NotNull - @field:JsonProperty(JSON_PROPERTY_NAME) - override var name: String, - ) : BookInfo(name) { + + @NotNull + @JsonProperty(JSON_PROPERTY_NAME) + name: String, + + @Nullable + @JsonProperty(JSON_PROPERTY_TYPE) + type: BookInfoType? = null, + ) : BookInfo(name, type) { """); assertFileContains(apiPath + "DetailedBookInfo.kt", """ - data class DetailedBookInfo( - + class DetailedBookInfo( + @field:NotNull @field:Pattern(regexp = "[0-9]{13}") @field:JsonProperty(JSON_PROPERTY_ISBN) var isbn: String, - - @field:NotNull - @field:Size(min = 3) - @field:JsonProperty(JSON_PROPERTY_AUTHOR) - override var author: String, - - @field:NotNull - @field:JsonProperty(JSON_PROPERTY_NAME) - override var name: String, - ) : BasicBookInfo(author, name) + + @NotNull + @Size(min = 3) + @JsonProperty(JSON_PROPERTY_AUTHOR) + author: String, + + @NotNull + @JsonProperty(JSON_PROPERTY_NAME) + name: String, + + @Nullable + @JsonProperty(JSON_PROPERTY_TYPE) + type: BookInfoType? = null, + ) : BasicBookInfo(author, name, type) { """); } @@ -562,8 +595,8 @@ void testOneOf() { String outputPath = generateFiles(codegen, "src/test/resources/3_0/oneof-with-discriminator.yml", CodegenConstants.APIS, CodegenConstants.MODELS); String path = outputPath + "src/main/kotlin/org/openapitools/"; - assertFileContains(path + "model/Subject.kt", "val typeCode: String"); - assertFileContains(path + "model/Person.kt", "override var typeCode: String = \"PERS\","); + assertFileContains(path + "model/Subject.kt", "val typeCode: String?"); + assertFileContains(path + "model/Person.kt", "override var typeCode: String? = \"PERS\","); } @Test @@ -586,11 +619,11 @@ void testDiscriminatorCustomType() { String path = outputPath + "src/main/kotlin/org/openapitools/"; assertFileContains(path + "model/CancellationReasonTypesV2.kt", """ - @field:NotNull + @field:Nullable @field:JsonProperty(JSON_PROPERTY_VERSION) - override var version: Int, + override var version: Int? = null, """); - assertFileContains(path + "model/CancellationReasonTypesDTO.kt", "val version: Int"); + assertFileContains(path + "model/CancellationReasonTypesDTO.kt", "val version: Int?"); } @Test @@ -675,15 +708,17 @@ void testDiscriminatorWithoutUseOneOfInterfaces() { var codegen = new KotlinMicronautClientCodegen(); codegen.setUseOneOfInterfaces(false); - String outputPath = generateFiles(codegen, "src/test/resources/3_0/discirminator2.yml", CodegenConstants.MODELS); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/discriminator2.yml", CodegenConstants.MODELS); String path = outputPath + "src/main/kotlin/org/openapitools/"; assertFileContains(path + "model/JsonOp.kt", """ open class JsonOp( + @field:NotNull @field:JsonProperty(JSON_PROPERTY_PATH) open var path: String, + @field:NotNull @field:JsonProperty(JSON_PROPERTY_OP) open var op: String, @@ -698,18 +733,21 @@ open class JsonOp( assertFileContains(path + "model/OpAdd.kt", """ - data class OpAdd( + class OpAdd( + @field:Nullable @field:JsonProperty(JSON_PROPERTY_VALUE) @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) var `value`: String? = null, - @field:NotNull - @field:JsonProperty(JSON_PROPERTY_PATH) - override var path: String, - @field:NotNull - @field:JsonProperty(JSON_PROPERTY_OP) - override var op: String, - ) : JsonOp(path, op) { + + @NotNull + @JsonProperty(JSON_PROPERTY_PATH) + path: String, + + @NotNull + @JsonProperty(JSON_PROPERTY_OP) + op: String, + ) : JsonOp(path, op) { """ ); } @@ -719,32 +757,32 @@ void testDiscriminatorWithUseOneOfInterfaces() { var codegen = new KotlinMicronautClientCodegen(); codegen.setUseOneOfInterfaces(true); - String outputPath = generateFiles(codegen, "src/test/resources/3_0/discirminator2.yml", CodegenConstants.MODELS); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/discriminator2.yml", CodegenConstants.MODELS); String path = outputPath + "src/main/kotlin/org/openapitools/"; assertFileContains(path + "model/JsonOp.kt", """ interface JsonOp { - val op: String + val op: String? } """ ); assertFileContains(path + "model/OpAdd.kt", """ - data class OpAdd( @field:Nullable @field:JsonProperty(JSON_PROPERTY_VALUE) @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) var `value`: String? = null, + @field:NotNull @field:JsonProperty(JSON_PROPERTY_PATH) var path: String, - @field:NotNull + + @field:Nullable @field:JsonProperty(JSON_PROPERTY_OP) - override var op: String, - ): JsonOp { + override var op: String? = null, """ ); } diff --git a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java index ea39e2d0da..236e3b3dd3 100644 --- a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java +++ b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java @@ -344,26 +344,31 @@ void testReadOnlyConstructorBug() { assertFileContains(apiPath + "BookInfo.kt", """ open class BookInfo( + @field:NotNull @field:Schema(name = "name", requiredMode = Schema.RequiredMode.REQUIRED) @field:JsonProperty(JSON_PROPERTY_NAME) open var name: String, + @field:Nullable @field:Schema(name = "requiredReadOnly", accessMode = Schema.AccessMode.READ_ONLY, requiredMode = Schema.RequiredMode.REQUIRED) @field:JsonProperty(JSON_PROPERTY_REQUIRED_READ_ONLY) @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) open var requiredReadOnly: String? = null, + @field:Nullable @field:Size(min = 3) @field:Schema(name = "author", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @field:JsonProperty(JSON_PROPERTY_AUTHOR) @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) open var author: String? = null, + @field:Nullable @field:Schema(name = "optionalReadOnly", accessMode = Schema.AccessMode.READ_ONLY, requiredMode = Schema.RequiredMode.NOT_REQUIRED) @field:JsonProperty(JSON_PROPERTY_OPTIONAL_READ_ONLY) @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) open var optionalReadOnly: String? = null, + @field:Nullable @field:Schema(name = "type", requiredMode = Schema.RequiredMode.NOT_REQUIRED) @field:JsonProperty(JSON_PROPERTY_TYPE) @@ -373,22 +378,44 @@ open class BookInfo( """); assertFileContains(apiPath + "ExtendedBookInfo.kt", """ - data class ExtendedBookInfo( + class ExtendedBookInfo( + @field:NotNull @field:Pattern(regexp = "[0-9]{13}") @field:Schema(name = "isbn", requiredMode = Schema.RequiredMode.REQUIRED) @field:JsonProperty(JSON_PROPERTY_ISBN) var isbn: String, - @field:NotNull - @field:Schema(name = "name", requiredMode = Schema.RequiredMode.REQUIRED) - @field:JsonProperty(JSON_PROPERTY_NAME) - override var name: String, - @field:Nullable - @field:Schema(name = "requiredReadOnly", accessMode = Schema.AccessMode.READ_ONLY, requiredMode = Schema.RequiredMode.REQUIRED) - @field:JsonProperty(JSON_PROPERTY_REQUIRED_READ_ONLY) - @field:JsonInclude(JsonInclude.Include.USE_DEFAULTS) - override var requiredReadOnly: String? = null, - ) : BookInfo(name, requiredReadOnly) { + + @NotNull + @Schema(name = "name", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty(JSON_PROPERTY_NAME) + name: String, + + @Nullable + @Schema(name = "requiredReadOnly", accessMode = Schema.AccessMode.READ_ONLY, requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty(JSON_PROPERTY_REQUIRED_READ_ONLY) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + requiredReadOnly: String? = null, + + @Nullable + @Size(min = 3) + @Schema(name = "author", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @JsonProperty(JSON_PROPERTY_AUTHOR) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + author: String? = null, + + @Nullable + @Schema(name = "optionalReadOnly", accessMode = Schema.AccessMode.READ_ONLY, requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @JsonProperty(JSON_PROPERTY_OPTIONAL_READ_ONLY) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + optionalReadOnly: String? = null, + + @Nullable + @Schema(name = "type", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + type: BookInfoType? = null, + ) : BookInfo(name, requiredReadOnly, author, optionalReadOnly, type) { """); } @@ -402,48 +429,67 @@ void testDiscriminatorConstructorBug() { assertFileContains(apiPath + "BookInfo.kt", """ open class BookInfo( + @field:NotNull @field:Schema(name = "name", requiredMode = Schema.RequiredMode.REQUIRED) @field:JsonProperty(JSON_PROPERTY_NAME) open var name: String, + @field:NotNull @field:Schema(name = "type", requiredMode = Schema.RequiredMode.REQUIRED) @field:JsonProperty(JSON_PROPERTY_TYPE) open var type: BookInfoType? = null, ) { """); + + assertFileContains(apiPath + "BasicBookInfo.kt", """ open class BasicBookInfo( + @field:NotNull @field:Size(min = 3) @field:Schema(name = "author", requiredMode = Schema.RequiredMode.REQUIRED) @field:JsonProperty(JSON_PROPERTY_AUTHOR) open var author: String, - @field:NotNull - @field:Schema(name = "name", requiredMode = Schema.RequiredMode.REQUIRED) - @field:JsonProperty(JSON_PROPERTY_NAME) - override var name: String, - ) : BookInfo(name) { + + @NotNull + @Schema(name = "name", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty(JSON_PROPERTY_NAME) + name: String, + + @Nullable + @Schema(name = "type", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty(JSON_PROPERTY_TYPE) + type: BookInfoType? = null, + ) : BookInfo(name, type) { """); assertFileContains(apiPath + "DetailedBookInfo.kt", """ - data class DetailedBookInfo( + class DetailedBookInfo( + @field:NotNull @field:Pattern(regexp = "[0-9]{13}") @field:Schema(name = "isbn", requiredMode = Schema.RequiredMode.REQUIRED) @field:JsonProperty(JSON_PROPERTY_ISBN) var isbn: String, - @field:NotNull - @field:Size(min = 3) - @field:Schema(name = "author", requiredMode = Schema.RequiredMode.REQUIRED) - @field:JsonProperty(JSON_PROPERTY_AUTHOR) - override var author: String, - @field:NotNull - @field:Schema(name = "name", requiredMode = Schema.RequiredMode.REQUIRED) - @field:JsonProperty(JSON_PROPERTY_NAME) - override var name: String, - ) : BasicBookInfo(author, name) { + + @NotNull + @Size(min = 3) + @Schema(name = "author", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty(JSON_PROPERTY_AUTHOR) + author: String, + + @NotNull + @Schema(name = "name", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty(JSON_PROPERTY_NAME) + name: String, + + @Nullable + @Schema(name = "type", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty(JSON_PROPERTY_TYPE) + type: BookInfoType? = null, + ) : BasicBookInfo(author, name, type) { """); } @@ -657,11 +703,11 @@ void testPolymorphism() { String path = outputPath + "src/main/kotlin/org/openapitools/"; assertFileContains(path + "model/CurrencyInvoiceCreateDto.kt", """ - @field:NotNull - @field:Size(max = 10) - @field:Schema(name = "sellerVatId", requiredMode = Schema.RequiredMode.REQUIRED) - @field:JsonProperty(JSON_PROPERTY_SELLER_VAT_ID) - override var sellerVatId: String, + @NotNull + @Size(max = 10) + @Schema(name = "sellerVatId", requiredMode = Schema.RequiredMode.REQUIRED) + @JsonProperty(JSON_PROPERTY_SELLER_VAT_ID) + sellerVatId: String, """ ); } @@ -793,4 +839,42 @@ fun findPetsByStatus( ): Mono> """); } + + @Test + void testKspMode() { + + var codegen = new KotlinMicronautServerCodegen(); + codegen.setKsp(true); + codegen.setGenerateSwaggerAnnotations(false); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/spec.yml", CodegenConstants.APIS, CodegenConstants.MODELS); + String path = outputPath + "src/main/kotlin/org/openapitools/"; + +// assertFileContains(path + "api/PetApi.kt", +// """ +// @Operation( +// operationId = "findPetsByStatus", +// summary = "Finds Pets by status", +// description = "Multiple status values can be provided with comma separated strings", +// responses = [ +// ApiResponse(responseCode = "200", description = "successful operation", content = [ +// Content(mediaType = "application/json", array = ArraySchema(schema = Schema(implementation = Pet::class))), +// Content(mediaType = "application/xml", array = ArraySchema(schema = Schema(implementation = Pet::class))), +// ]), +// ApiResponse(responseCode = "400", description = "Invalid status value"), +// ], +// parameters = [ +// Parameter(name = "status", description = "Status values that need to be considered for filter", `in` = ParameterIn.QUERY), +// ], +// security = [ +// SecurityRequirement(name = "petstore_auth", scopes = ["write:pets", "read:pets"]), +// ], +// ) +// @Get("/pet/findByStatus") +// @Produces("application/json", "application/xml") +// @Secured("write:pets", "read:pets") +// fun findPetsByStatus( +// @QueryValue("status") @Nullable status: List<@NotNull String>?, +// ): Mono> +// """); + } } diff --git a/openapi-generator/src/test/resources/3_0/discirminator2.yml b/openapi-generator/src/test/resources/3_0/discriminator2.yml similarity index 100% rename from openapi-generator/src/test/resources/3_0/discirminator2.yml rename to openapi-generator/src/test/resources/3_0/discriminator2.yml From 2758c5ea615932b3821194f6b20a400649b00dbe Mon Sep 17 00:00:00 2001 From: altro3 Date: Tue, 10 Dec 2024 00:49:18 +0700 Subject: [PATCH 3/3] Added default values for kotlin controller methods with optional parameters Fixed #1903 --- .../AbstractMicronautJavaCodegen.java | 5 ++ .../AbstractMicronautKotlinCodegen.java | 19 ++++++ .../server/controller-implementation.mustache | 11 +-- .../server/controller-interface.mustache | 2 +- .../AbstractMicronautCodegenTest.java | 3 +- .../JavaMicronautClientCodegenTest.java | 2 +- .../KotlinMicronautClientCodegenTest.java | 23 ++++++- .../KotlinMicronautServerCodegenTest.java | 67 ++++++++----------- .../3_0/optional-controller-values.yml | 38 +++++++++++ 9 files changed, 121 insertions(+), 49 deletions(-) create mode 100644 openapi-generator/src/test/resources/3_0/optional-controller-values.yml diff --git a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautJavaCodegen.java b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautJavaCodegen.java index c1af4f2173..127fd2615c 100644 --- a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautJavaCodegen.java +++ b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautJavaCodegen.java @@ -735,6 +735,11 @@ public String toModelTestFilename(String name) { @Override public CodegenParameter fromParameter(Parameter p, Set imports) { var parameter = super.fromParameter(p, imports); + + if (parameter.isPathParam && !parameter.required) { + parameter.required = true; + } + checkPrimitives(parameter, unaliasSchema(p.getSchema())); // if name is escaped var realName = parameter.paramName; diff --git a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java index 1fe4e3d76f..25ea98fc52 100644 --- a/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java +++ b/openapi-generator/src/main/java/io/micronaut/openapi/generator/AbstractMicronautKotlinCodegen.java @@ -29,6 +29,7 @@ import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.servers.Server; import org.apache.commons.lang3.StringUtils; @@ -1421,9 +1422,24 @@ public String toModelName(final String name) { return schemaKeyToModelNameCache.get(name); } + @Override + public CodegenParameter fromRequestBody(RequestBody body, Set imports, String bodyParameterName) { + var rqBody = super.fromRequestBody(body, imports, bodyParameterName); + if (!rqBody.required) { + rqBody.vendorExtensions.put("defaultValueInit", "null"); + } + + return rqBody; + } + @Override public CodegenParameter fromParameter(Parameter p, Set imports) { var parameter = super.fromParameter(p, imports); + + if (parameter.isPathParam && !parameter.required) { + parameter.required = true; + } + checkPrimitives(parameter, unaliasSchema(p.getSchema())); // if name is escaped var realName = parameter.paramName; @@ -1462,6 +1478,9 @@ public CodegenParameter fromParameter(Parameter p, Set imports) { defaultValueInit = parameter.dataType + "." + enumVarName; } } + if (defaultValueInit == null && !parameter.required) { + defaultValueInit = "null"; + } if (defaultValueInit != null) { parameter.vendorExtensions.put("defaultValueInit", defaultValueInit); } diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-implementation.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-implementation.mustache index cec290663f..b8448b3f4c 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-implementation.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-implementation.mustache @@ -18,12 +18,13 @@ import {{import}} {{/imports}} @Controller -class {{controllerClassname}}: {{classname}} { - +class {{controllerClassname}} : {{classname}} { {{#operations}} - {{#operation}} - {{!the method definition}} - override fun {{nickname}}({{#allParams}}{{{paramName}}}: {{{vendorExtensions.typeWithGenericAnnotations}}}{{^-last}}, {{/-last}}{{/allParams}}){{#returnType}}: {{{returnType}}}{{/returnType}} { + {{#operation}}{{#formatNoEmptyLines}} + override fun {{nickname}}({{#allParams}} + {{{paramName}}}: {{#isEnum}}{{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.typeWithGenericAnnotations}}}{{/isEnum}}{{#vendorExtensions.defaultValueInit}} = {{{.}}}{{/vendorExtensions.defaultValueInit}}, +{{/allParams}} + ){{#returnType}}: {{{returnType}}}{{/returnType}} {{openbrace}}{{/formatNoEmptyLines}} {{>server/controllerOperationBody}} } {{^-last}} diff --git a/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-interface.mustache b/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-interface.mustache index 1c0cbb6f7a..5fac6ab667 100644 --- a/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-interface.mustache +++ b/openapi-generator/src/main/resources/templates/kotlin-micronaut/server/controller-interface.mustache @@ -78,7 +78,7 @@ interface {{classname}} { {{#isDeprecated}} @java.lang.Deprecated {{/isDeprecated}} - {{{paramName}}}: {{#isEnum}}{{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.typeWithGenericAnnotations}}}{{/isEnum}},{{/formatSingleLine}} + {{{paramName}}}: {{#isEnum}}{{{vendorExtensions.typeWithEnumWithGenericAnnotations}}}{{/isEnum}}{{^isEnum}}{{{vendorExtensions.typeWithGenericAnnotations}}}{{/isEnum}}{{#vendorExtensions.defaultValueInit}} = {{{.}}}{{/vendorExtensions.defaultValueInit}},{{/formatSingleLine}} {{/allParams}}){{#returnType}}: {{{returnType}}}{{/returnType}} {{/formatNoEmptyLines}} diff --git a/openapi-generator/src/test/java/io/micronaut/openapi/generator/AbstractMicronautCodegenTest.java b/openapi-generator/src/test/java/io/micronaut/openapi/generator/AbstractMicronautCodegenTest.java index 7d764ed82a..a33d00a96a 100644 --- a/openapi-generator/src/test/java/io/micronaut/openapi/generator/AbstractMicronautCodegenTest.java +++ b/openapi-generator/src/test/java/io/micronaut/openapi/generator/AbstractMicronautCodegenTest.java @@ -46,8 +46,7 @@ protected String generateFiles(MicronautCodeGenerator codegen, String configP .withOutputDirectory(output) .withOutputs(Arrays.stream(filesToGenerate) .map(MicronautCodeGeneratorEntryPoint.OutputKind::of) - .toList() - .toArray(new MicronautCodeGeneratorEntryPoint.OutputKind[0]) + .toArray(MicronautCodeGeneratorEntryPoint.OutputKind[]::new) ) .build() .generate(); diff --git a/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenTest.java b/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenTest.java index a97e93219c..c3d765ccb5 100644 --- a/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenTest.java +++ b/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenTest.java @@ -557,7 +557,7 @@ void testParamsWithDefaultValue() { assertFileContains(path + "api/DefaultApi.java", "@QueryValue(\"ids\") @Nullable List<@NotNull Integer> ids", - "@PathVariable(name = \"apiVersion\", defaultValue = \"v5\") @Nullable BrowseSearchOrdersApiVersionParameter apiVersio", + "@PathVariable(name = \"apiVersion\", defaultValue = \"v5\") @NotNull BrowseSearchOrdersApiVersionParameter apiVersion", "@Header(name = \"Content-Type\", defaultValue = \"application/json\") @Nullable String contentType" ); } diff --git a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java index 9369d19ff8..6a64433345 100644 --- a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java +++ b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautClientCodegenTest.java @@ -647,7 +647,7 @@ void testParamsWithDefaultValue() { assertFileContains(path + "api/DefaultApi.kt", "@QueryValue(\"ids\") @Nullable ids: List<@NotNull Int>? = null,", "@Header(\"X-Favor-Token\") @Nullable xFavorToken: String? = null,", - "@PathVariable(name = \"apiVersion\", defaultValue = \"v5\") @Nullable apiVersion: BrowseSearchOrdersApiVersionParameter? = BrowseSearchOrdersApiVersionParameter.V5,", + "@PathVariable(name = \"apiVersion\", defaultValue = \"v5\") @NotNull apiVersion: BrowseSearchOrdersApiVersionParameter = BrowseSearchOrdersApiVersionParameter.V5,", "@Header(name = \"Content-Type\", defaultValue = \"application/json\") @Nullable contentType: String? = \"application/json\"", "@QueryValue(\"algorithm\") @Nullable algorithm: BrowseSearchOrdersAlgorithmParameter? = null" ); @@ -1447,4 +1447,25 @@ override fun hashCode(): Int = Objects.hash(super.hashCode()) """); } + + @Test + void testOptionalQueryValues() { + + var codegen = new KotlinMicronautClientCodegen(); + codegen.setGenerateSwaggerAnnotations(false); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/optional-controller-values.yml", CodegenConstants.APIS, CodegenConstants.MODELS); + String path = outputPath + "src/main/kotlin/org/openapitools/"; + + assertFileContains(path + "api/DefaultApi.kt", + """ + @Post("/sendPrimitives/{name}") + fun sendPrimitives( + @PathVariable("name") @NotNull name: String, + @QueryValue("brand") @Nullable brand: String? = null, + @CookieValue("coc") @Nullable coc: String? = null, + @Header("head") @Nullable head: String? = null, + @Body @Nullable body: String? = null, + ): Mono + """); + } } diff --git a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java index 236e3b3dd3..077584a5ef 100644 --- a/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java +++ b/openapi-generator/src/test/java/io/micronaut/openapi/generator/KotlinMicronautServerCodegenTest.java @@ -544,7 +544,12 @@ void testReservedWords() { ); String path = outputPath + "src/main/kotlin/org/openapitools/"; - assertFileContains(path + "controller/ParametersController.kt", "override fun callInterface(name: Class, `data`: String): Mono"); + assertFileContains(path + "controller/ParametersController.kt", """ + override fun callInterface( + name: Class, + `data`: String, + ): Mono { + """); assertFileContains(path + "api/ParametersApi.kt", "fun callInterface(", "@QueryValue(\"name\") @NotNull @Valid name: Class,", "@QueryValue(\"data\") @NotNull `data`: String", @@ -565,7 +570,7 @@ void testCommonPathParametersWithRef() { assertFileContains(path + "api/WeatherForecastApisApi.kt", "@Get(\"/v1/forecast/{id}\")", "@PathVariable(\"id\") @NotNull id: String,", - "@QueryValue(\"hourly\") @Nullable hourly: List?,"); + "@QueryValue(\"hourly\") @Nullable hourly: List? = null,"); assertFileContains(path + "model/V1ForecastIdGetHourlyParameterInner.kt", "enum class V1ForecastIdGetHourlyParameterInner(", @@ -653,8 +658,8 @@ void testMultipartFormData() { fun profilePasswordPost( @Header("WCToken") @NotNull wcToken: String, @Header("WCTrustedToken") @NotNull wcTrustedToken: String, - @Part("name") @Nullable name: String?, - @Part("file") @Nullable file: CompletedFileUpload?, + @Part("name") @Nullable name: String? = null, + @Part("file") @Nullable file: CompletedFileUpload? = null, ): Mono """); } @@ -671,7 +676,7 @@ void testMultipleContentTypesEndpoints() { @Consumes("application/json", "application/xml") @Secured(SecurityRule.IS_ANONYMOUS) fun myOp( - @Body @Nullable @Valid coordinates: Coordinates?, + @Body @Nullable @Valid coordinates: Coordinates? = null, ): Mono> """, """ @@ -679,8 +684,8 @@ fun myOp( @Consumes("multipart/form-data") @Secured(SecurityRule.IS_ANONYMOUS) fun myOp_1( - @Nullable @Valid coordinates: Coordinates?, - @Nullable file: CompletedFileUpload?, + @Nullable @Valid coordinates: Coordinates? = null, + @Nullable file: CompletedFileUpload? = null, ): Mono> """, """ @@ -688,7 +693,7 @@ fun myOp_1( @Consumes("application/yaml", "text/json") @Secured(SecurityRule.IS_ANONYMOUS) fun myOp_2( - @Body @Nullable @Valid mySchema: MySchema?, + @Body @Nullable @Valid mySchema: MySchema? = null, ): Mono> """); } @@ -835,46 +840,30 @@ void testSwaggerAnnotations() { @Produces("application/json", "application/xml") @Secured("write:pets", "read:pets") fun findPetsByStatus( - @QueryValue("status") @Nullable status: List<@NotNull String>?, + @QueryValue("status") @Nullable status: List<@NotNull String>? = null, ): Mono> """); } @Test - void testKspMode() { + void testOptionalQueryValues() { var codegen = new KotlinMicronautServerCodegen(); - codegen.setKsp(true); codegen.setGenerateSwaggerAnnotations(false); - String outputPath = generateFiles(codegen, "src/test/resources/3_0/spec.yml", CodegenConstants.APIS, CodegenConstants.MODELS); + String outputPath = generateFiles(codegen, "src/test/resources/3_0/optional-controller-values.yml", CodegenConstants.APIS, CodegenConstants.MODELS); String path = outputPath + "src/main/kotlin/org/openapitools/"; -// assertFileContains(path + "api/PetApi.kt", -// """ -// @Operation( -// operationId = "findPetsByStatus", -// summary = "Finds Pets by status", -// description = "Multiple status values can be provided with comma separated strings", -// responses = [ -// ApiResponse(responseCode = "200", description = "successful operation", content = [ -// Content(mediaType = "application/json", array = ArraySchema(schema = Schema(implementation = Pet::class))), -// Content(mediaType = "application/xml", array = ArraySchema(schema = Schema(implementation = Pet::class))), -// ]), -// ApiResponse(responseCode = "400", description = "Invalid status value"), -// ], -// parameters = [ -// Parameter(name = "status", description = "Status values that need to be considered for filter", `in` = ParameterIn.QUERY), -// ], -// security = [ -// SecurityRequirement(name = "petstore_auth", scopes = ["write:pets", "read:pets"]), -// ], -// ) -// @Get("/pet/findByStatus") -// @Produces("application/json", "application/xml") -// @Secured("write:pets", "read:pets") -// fun findPetsByStatus( -// @QueryValue("status") @Nullable status: List<@NotNull String>?, -// ): Mono> -// """); + assertFileContains(path + "api/DefaultApi.kt", + """ + @Post("/sendPrimitives/{name}") + @Secured(SecurityRule.IS_ANONYMOUS) + fun sendPrimitives( + @PathVariable("name") @NotNull name: String, + @QueryValue("brand") @Nullable brand: String? = null, + @CookieValue("coc") @Nullable coc: String? = null, + @Header("head") @Nullable head: String? = null, + @Body @Nullable body: String? = null, + ): Mono + """); } } diff --git a/openapi-generator/src/test/resources/3_0/optional-controller-values.yml b/openapi-generator/src/test/resources/3_0/optional-controller-values.yml new file mode 100644 index 0000000000..c87216ceb0 --- /dev/null +++ b/openapi-generator/src/test/resources/3_0/optional-controller-values.yml @@ -0,0 +1,38 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Compute API + description: API for the Compute Service +paths: + /sendPrimitives/{name}: + post: + operationId: sendPrimitives + parameters: + - in: query + name: brand + schema: + type: string + - in: path + name: name + schema: + type: string + - in: cookie + name: coc + schema: + type: string + - in: header + name: head + schema: + type: string + requestBody: + content: + application/json: + schema: + type: string + responses: + 200: + description: Success + content: + application/json: + schema: + type: string