diff --git a/build-logic/src/main/groovy/io.micronaut.build.internal.openapi-generator-test-suite.gradle b/build-logic/src/main/groovy/io.micronaut.build.internal.openapi-generator-test-suite.gradle index f59fee19d0..b636cbe8ad 100644 --- a/build-logic/src/main/groovy/io.micronaut.build.internal.openapi-generator-test-suite.gradle +++ b/build-logic/src/main/groovy/io.micronaut.build.internal.openapi-generator-test-suite.gradle @@ -24,6 +24,7 @@ def openapiGenerate = tasks.register("generateOpenApi", OpenApiGeneratorTask) { openApiDefinition.convention(layout.projectDirectory.file("petstore.json")) outputDirectory.convention(layout.buildDirectory.dir("generated/openapi")) generatorKind.convention("client") + outputKinds.convention(["models", "apis", "supportingFiles", "modelTests", "apiTests"]) } sourceSets { diff --git a/build-logic/src/main/groovy/io/micronaut/build/internal/openapi/OpenApiGeneratorTask.java b/build-logic/src/main/groovy/io/micronaut/build/internal/openapi/OpenApiGeneratorTask.java index 4db4b11d57..c0cda05602 100644 --- a/build-logic/src/main/groovy/io/micronaut/build/internal/openapi/OpenApiGeneratorTask.java +++ b/build-logic/src/main/groovy/io/micronaut/build/internal/openapi/OpenApiGeneratorTask.java @@ -20,6 +20,7 @@ import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Classpath; @@ -63,9 +64,13 @@ public Provider getGeneratedSourcesDirectory() { @Internal public Provider getGeneratedTestSourcesDirectory() { - return getOutputDirectory().dir("src/test/java"); + return getOutputDirectory().dir("src/test/groovy"); } + @Input + public abstract ListProperty getOutputKinds(); + + @Inject protected abstract ExecOperations getExecOperations(); @@ -82,6 +87,7 @@ public void execute() throws IOException { args.add(getGeneratorKind().get()); args.add(getOpenApiDefinition().get().getAsFile().toURI().toString()); args.add(getOutputDirectory().get().getAsFile().getAbsolutePath()); + args.add(String.join(",", getOutputKinds().get())); javaexec.args(args); }); } 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 d7539b508f..50fe4f7698 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 @@ -194,8 +194,8 @@ protected AbstractMicronautJavaCodegen() { // Add reserved words String[] reservedWordsArray = { - "client", "format", "queryvalue", "queryparam", "pathvariable", "header", "cookie", - "authorization", "body", "application" + "Client", "Format", "QueryValue", "QueryParam", "PathVariable", "Header", "Cookie", + "Authorization", "Body", "application" }; reservedWords.addAll(Arrays.asList(reservedWordsArray)); } @@ -312,6 +312,9 @@ public void processOpts() { setSerializationLibrary((String) additionalProperties.get(CodegenConstants.SERIALIZATION_LIBRARY)); } additionalProperties.put(this.serializationLibrary.toLowerCase(Locale.US), true); + if (SerializationLibraryKind.MICRONAUT_SERDE_JACKSON.name().equals(serializationLibrary)) { + additionalProperties.put(SerializationLibraryKind.JACKSON.name().toLowerCase(Locale.US), true); + } // Add all the supporting files String resourceFolder = projectFolder + "/resources"; @@ -360,6 +363,11 @@ public void processOpts() { additionalProperties.put("resourceFolder", resourceFolder); additionalProperties.put("apiFolder", apiFolder); additionalProperties.put("modelFolder", modelFolder); + + additionalProperties.put("formatNoEmptyLines", new Formatting.LineFormatter(0)); + additionalProperties.put("formatOneEmptyLine", new Formatting.LineFormatter(1)); + additionalProperties.put("formatSingleLine", new Formatting.SingleLineFormatter()); + additionalProperties.put("indent", new Formatting.IndentFormatter(4)); } // CHECKSTYLE:OFF diff --git a/openapi-generator/src/main/java/io/micronaut/openapi/generator/Formatting.java b/openapi-generator/src/main/java/io/micronaut/openapi/generator/Formatting.java new file mode 100644 index 0000000000..6553e944a0 --- /dev/null +++ b/openapi-generator/src/main/java/io/micronaut/openapi/generator/Formatting.java @@ -0,0 +1,166 @@ +/* + * Copyright 2017-2023 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.openapi.generator; + +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; + +import java.io.IOException; +import java.io.Writer; +import java.util.Arrays; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * A class with lambdas to format mustache-generated code and formatting utility functions. + */ +public enum Formatting { + /* This class is not supposed to be initialized */; + + /** + * Remove whitespace on the right of the line. + * @param line The line to be trimmed. + * @return The trimmed line. + */ + public static String rightTrim(String line) { + int end = 0; + for (int i = line.length(); i > 0; --i) { + if (!Character.isWhitespace(line.charAt(i - 1))) { + end = i; + break; + } + } + return line.substring(0, end); + } + + /** + * Remove whitespace from both sides of the line. + * @param line The line to be trimmed. + * @return The trimmed line. + */ + public static String trim(String line) { + int start = line.length(); + for (int i = 0; i < line.length(); ++i) { + if (!Character.isWhitespace(line.charAt(i))) { + start = i; + break; + } + } + int end = start; + for (int i = line.length(); i > start; --i) { + if (!Character.isWhitespace(line.charAt(i - 1))) { + end = i; + break; + } + } + return line.substring(start, end); + } + + /** + * A formatter that is responsible for removing extra empty lines in mustache files. + */ + public static class LineFormatter implements Mustache.Lambda { + + private final int maxEmptyLines; + + /** + * Create the lambda. + * + * @param maxEmptyLines maximal empty lines. + */ + public LineFormatter(int maxEmptyLines) { + this.maxEmptyLines = maxEmptyLines; + } + + @Override + public void execute(Template.Fragment fragment, Writer writer) throws IOException { + String text = fragment.execute(); + String finalWhitespace = getFinalWhitespace(text); + + String lines = + Arrays.stream(text.split("\n")) + .map(Formatting::rightTrim) + .filter(new LineSkippingPredicate()) + .collect(Collectors.joining("\n")); + if (!lines.isEmpty() && maxEmptyLines == 0) { + lines = "\n" + lines + "\n"; + } + if (!lines.isEmpty()) { + lines = lines + finalWhitespace; + } + writer.write(lines); + } + + private String getFinalWhitespace(String text) { + int i = text.length(); + while (i > 0 && Character.isWhitespace(text.charAt(i - 1)) && text.charAt(i - 1) != '\n') { + --i; + } + return text.substring(i); + } + + private class LineSkippingPredicate implements Predicate { + private int emptyLines; + + @Override + public boolean test(String s) { + if (s.isBlank()) { + ++emptyLines; + } else { + emptyLines = 0; + } + return emptyLines <= maxEmptyLines; + } + } + } + + /** + * A formatter that collects everything in a single line. + */ + public static class SingleLineFormatter implements Mustache.Lambda { + + @Override + public void execute(Template.Fragment fragment, Writer writer) throws IOException { + String text = + fragment.execute() + .replaceAll("\\s+", " ") + .replaceAll("(?<=<)\\s+|\\s+(?=>)", ""); + writer.write(Formatting.trim(text)); + } + + } + + /** + * A lambda that allows indenting its contents. + */ + public static class IndentFormatter implements Mustache.Lambda { + + private final String indent; + + public IndentFormatter(int indentSize) { + indent = " ".repeat(Math.max(0, indentSize)); + } + + @Override + public void execute(Template.Fragment fragment, Writer writer) throws IOException { + String text = + Arrays.stream(fragment.execute().split("\n")) + .map(line -> indent + line) + .collect(Collectors.joining("\n")); + writer.write(text); + } + } +} diff --git a/openapi-generator/src/main/java/io/micronaut/openapi/generator/JavaMicronautServerCodegen.java b/openapi-generator/src/main/java/io/micronaut/openapi/generator/JavaMicronautServerCodegen.java index a14540fb8d..8bc69c106a 100644 --- a/openapi-generator/src/main/java/io/micronaut/openapi/generator/JavaMicronautServerCodegen.java +++ b/openapi-generator/src/main/java/io/micronaut/openapi/generator/JavaMicronautServerCodegen.java @@ -34,6 +34,7 @@ class JavaMicronautServerCodegen extends AbstractMicronautJavaCodegen generatorOptionsBuilder + * @param The type of generator options builder. */ public interface MicronautCodeGenerator { T optionsBuilder(); diff --git a/openapi-generator/src/main/java/io/micronaut/openapi/generator/MicronautCodeGeneratorEntryPoint.java b/openapi-generator/src/main/java/io/micronaut/openapi/generator/MicronautCodeGeneratorEntryPoint.java index f5f2ba6146..dcb9510340 100644 --- a/openapi-generator/src/main/java/io/micronaut/openapi/generator/MicronautCodeGeneratorEntryPoint.java +++ b/openapi-generator/src/main/java/io/micronaut/openapi/generator/MicronautCodeGeneratorEntryPoint.java @@ -132,10 +132,10 @@ private void configureServerOptions() { if (serverOptions.controllerPackage() != null) { serverCodegen.setControllerPackage(serverOptions.controllerPackage()); } - serverCodegen.setGenerateImplementationFiles(serverOptions.generateAbstractClasses()); + serverCodegen.setGenerateImplementationFiles(serverOptions.generateImplementationFiles()); serverCodegen.setGenerateOperationsToReturnNotImplemented(serverOptions.generateOperationsToReturnNotImplemented()); serverCodegen.setGenerateControllerFromExamples(serverOptions.generateControllerFromExamples()); - serverCodegen.setUseAuth(serverCodegen.useAuth); + serverCodegen.setUseAuth(serverOptions.useAuth()); } } diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/bodyParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/bodyParams.mustache index fd6862369f..2b294afc7a 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/bodyParams.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/bodyParams.mustache @@ -1 +1 @@ -{{#isBodyParam}}@Body {{>common/params/beanValidation}}{{>client/params/type}} {{paramName}}{{/isBodyParam}} \ No newline at end of file +{{#isBodyParam}}@Body {{>common/params/validation}}{{>client/params/type}} {{paramName}}{{/isBodyParam}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/cookieParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/cookieParams.mustache index 60c2c377ec..8b62cfe5aa 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/cookieParams.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/cookieParams.mustache @@ -1 +1 @@ -{{#isCookieParam}}@CookieValue(value="{{baseName}}"{{#defaultValue}}, defaultValue="{{defaultValue}}"{{/defaultValue}}) {{>common/params/beanValidation}}{{>client/params/type}} {{paramName}}{{/isCookieParam}} \ No newline at end of file +{{#isCookieParam}}@CookieValue(value="{{baseName}}"{{#defaultValue}}, defaultValue="{{defaultValue}}"{{/defaultValue}}) {{>common/params/validation}}{{>client/params/type}} {{paramName}}{{/isCookieParam}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/formParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/formParams.mustache index cfbd00f342..e83a69f8dc 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/formParams.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/formParams.mustache @@ -1 +1 @@ -{{#isFormParam}}{{>common/params/beanValidation}}{{>client/params/type}} {{paramName}}{{/isFormParam}} \ No newline at end of file +{{#isFormParam}}{{>common/params/validation}}{{>client/params/type}} {{paramName}}{{/isFormParam}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/headerParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/headerParams.mustache index f8a5a79ab6..1f51ae97e0 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/headerParams.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/headerParams.mustache @@ -1 +1 @@ -{{#isHeaderParam}}@Header(name="{{baseName}}"{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) {{>common/params/beanValidation}}{{>client/params/type}} {{paramName}}{{/isHeaderParam}} \ No newline at end of file +{{#isHeaderParam}}@Header(name="{{baseName}}"{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) {{>common/params/validation}}{{>client/params/type}} {{paramName}}{{/isHeaderParam}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/pathParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/pathParams.mustache index 64a0be56c6..87f126b8a4 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/pathParams.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/pathParams.mustache @@ -1 +1 @@ -{{#isPathParam}}@PathVariable(name="{{baseName}}"{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) {{>common/params/beanValidation}}{{>client/params/type}} {{paramName}}{{/isPathParam}} \ No newline at end of file +{{#isPathParam}}@PathVariable(name="{{baseName}}"{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) {{>common/params/validation}}{{>client/params/type}} {{paramName}}{{/isPathParam}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/queryParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/queryParams.mustache index aed538a1c3..33ca4fca8e 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/client/params/queryParams.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/client/params/queryParams.mustache @@ -1 +1 @@ -{{#isQueryParam}}@QueryValue(value="{{{baseName}}}"{{!default value}}{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) {{!validation and type}}{{>common/params/beanValidation}}{{>client/params/type}} {{paramName}}{{/isQueryParam}} \ No newline at end of file +{{#isQueryParam}}@QueryValue(value="{{{baseName}}}"{{!default value}}{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) {{!validation and type}}{{>common/params/validation}}{{>client/params/type}} {{paramName}}{{/isQueryParam}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/beanValidation.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/beanValidation.mustache deleted file mode 100644 index 6c7dde3d9e..0000000000 --- a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/beanValidation.mustache +++ /dev/null @@ -1,52 +0,0 @@ -{{#useBeanValidation}}{{! -validate all pojos and enums -}}{{^isContainer}}{{#isModel}} @Valid -{{/isModel}}{{/isContainer}}{{! -nullable & nonnull -}}{{#required}}{{#isNullable}} @Nullable -{{/isNullable}}{{^isNullable}} @NotNull -{{/isNullable}}{{/required}}{{^required}}{{^useOptional}} @Nullable -{{/useOptional}}{{/required}}{{! -pattern -}}{{#pattern}}{{^isByteArray}} @Pattern(regexp="{{{pattern}}}") -{{/isByteArray}}{{/pattern}}{{! -both minLength && maxLength -}}{{#minLength}}{{#maxLength}} @Size(min={{minLength}}, max={{maxLength}}) -{{/maxLength}}{{/minLength}}{{! -just minLength -}}{{#minLength}}{{^maxLength}} @Size(min={{minLength}}) -{{/maxLength}}{{/minLength}}{{! -just maxLength -}}{{^minLength}}{{#maxLength}} @Size(max={{maxLength}}) -{{/maxLength}}{{/minLength}}{{! -@Size: both minItems && maxItems -}}{{#minItems}}{{#maxItems}} @Size(min={{minItems}}, max={{maxItems}}) -{{/maxItems}}{{/minItems}}{{! -@Size: just minItems -}}{{#minItems}}{{^maxItems}} @Size(min={{minItems}}) -{{/maxItems}}{{/minItems}}{{! -@Size: just maxItems -}}{{^minItems}}{{#maxItems}} @Size(max={{maxItems}}) -{{/maxItems}}{{/minItems}}{{! -@Email -}}{{#isEmail}} @Email -{{/isEmail}}{{! -check for integer or long / all others=decimal type with @Decimal* -isInteger set -}}{{#isInteger}}{{#minimum}} @Min({{minimum}}) -{{/minimum}}{{#maximum}} @Max({{maximum}}) -{{/maximum}}{{/isInteger}}{{! -isLong set -}}{{#isLong}}{{#minimum}} @Min({{minimum}}L) -{{/minimum}}{{#maximum}} @Max({{maximum}}L) -{{/maximum}}{{/isLong}}{{! -Not Integer, not Long => we have a decimal value! -}}{{^isInteger}}{{^isLong}}{{! -minimum for decimal value -}}{{#minimum}} @DecimalMin({{#exclusiveMinimum}}value={{/exclusiveMinimum}}"{{minimum}}"{{#exclusiveMinimum}}, inclusive=false{{/exclusiveMinimum}}) -{{/minimum}}{{! -maximal for decimal value -}}{{#maximum}} @DecimalMax({{#exclusiveMaximum}}value={{/exclusiveMaximum}}"{{maximum}}"{{#exclusiveMaximum}}, inclusive=false{{/exclusiveMaximum}}) -{{/maximum}}{{! -close decimal values -}}{{/isLong}}{{/isInteger}}{{/useBeanValidation}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/enum.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/enum.mustache new file mode 100644 index 0000000000..973f9a5bf3 --- /dev/null +++ b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/enum.mustache @@ -0,0 +1,68 @@ +/** + * {{^description}}Gets or Sets {{{name}}}{{/description}}{{#description}}{{description}}{{/description}} + */ +{{#withXml}} +@XmlType(name="{{datatypeWithEnum}}") +@XmlEnum({{dataType}}.class) +{{/withXml}} +{{#micronaut_serde_jackson}} +@Serdeable +{{/micronaut_serde_jackson}} +{{#additionalEnumTypeAnnotations}} +{{{.}}} +{{/additionalEnumTypeAnnotations}} +{{#formatSingleLine}}public enum {{>common/model/enumName}}{{/formatSingleLine}} { + {{#allowableValues}} + {{#enumVars}} + {{#enumDescription}} + /** + * {{enumDescription}} + */ + {{/enumDescription}} + {{#withXml}} + @XmlEnumValue({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}) + {{/withXml}} + {{{name}}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} + {{/enumVars}} + {{/allowableValues}} + + private final {{{dataType}}} value; + + {{#formatSingleLine}}{{>common/model/enumName}}{{/formatSingleLine}}({{{dataType}}} value) { + this.value = value; + } + + /** + * @return The value represented by this enum + */ + {{#jackson}} + @JsonValue + {{/jackson}} + public {{{dataType}}} getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + {{#formatSingleLine}}private final static Map<{{{dataType}}}, {{>common/model/enumName}}> VALUE_MAPPING = Arrays.stream(values()){{/formatSingleLine}} + .collect(Collectors.toMap(v -> v.getValue(){{#isString}}{{#useEnumCaseInsensitive}}.toLowerCase(){{/useEnumCaseInsensitive}}{{/isString}}, v -> v)); + + /** + * Create this enum from a value. + * @return The enum. + */ + {{#jackson}} + @JsonCreator + {{/jackson}} + {{#formatSingleLine}}public static {{>common/model/enumName}} fromValue(String value){{/formatSingleLine}} { + {{^isNullable}} + if (!VALUE_MAPPING.containsKey(value{{#isString}}{{#useEnumCaseInsensitive}}.toLowerCase(){{/useEnumCaseInsensitive}}{{/isString}})) { + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } + {{/isNullable}} + return VALUE_MAPPING.get(value{{#isString}}{{#useEnumCaseInsensitive}}.toLowerCase(){{/useEnumCaseInsensitive}}{{/isString}}); + } +} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/enumName.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/enumName.mustache new file mode 100644 index 0000000000..b87322feda --- /dev/null +++ b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/enumName.mustache @@ -0,0 +1 @@ +{{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/jackson_annotations.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/jackson_annotations.mustache index 7acd842ad7..802b0c1f82 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/jackson_annotations.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/jackson_annotations.mustache @@ -5,7 +5,17 @@ * Else use custom behaviour, IOW use whatever is defined on the object mapper }} @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) - @JsonInclude({{#isMap}}{{#items.isNullable}}content = JsonInclude.Include.ALWAYS, {{/items.isNullable}}{{/isMap}}value = JsonInclude.Include.{{#required}}ALWAYS{{/required}}{{^required}}USE_DEFAULTS{{/required}}) + {{#isMap}} + {{#items.isNullable}} + @JsonInclude(content = JsonInclude.Include.ALWAYS{{^required}}, value = JsonInclude.Include.USE_DEFAULTS{{/required}}) + {{/items.isNullable}} + {{^items.isNullable}}{{^required}} + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + {{/required}}{{/items.isNullable}} + {{/isMap}} + {{^isMap}}{{^required}} + @JsonInclude(JsonInclude.Include.USE_DEFAULTS) + {{/required}}{{/isMap}} {{#withXml}} {{^isContainer}} @JacksonXmlProperty({{#isXmlAttribute}}isAttribute = true, {{/isXmlAttribute}}{{#xmlNamespace}}namespace="{{xmlNamespace}}", {{/xmlNamespace}}localName = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}") @@ -19,12 +29,14 @@ {{/isContainer}} {{/withXml}} {{#jackson}} - {{#isDateTime}} - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "{{{datetimeFormat}}}") - {{/isDateTime}} - {{#isDate}} - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "{{{dateFormat}}}") - {{/isDate}} + {{^micronaut_serde_jackson}} + {{#isDateTime}} + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "{{{datetimeFormat}}}") + {{/isDateTime}} + {{#isDate}} + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "{{{dateFormat}}}") + {{/isDate}} + {{/micronaut_serde_jackson}} {{/jackson}} {{#micronaut_serde_jackson}} {{#isDateTime}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/model.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/model.mustache index e98fd59700..0c8fc80375 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/model.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/model.mustache @@ -1,22 +1,30 @@ {{>common/licenseInfo}} package {{package}}; +{{#formatNoEmptyLines}} {{#useReflectionEqualsHashCode}} import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; {{/useReflectionEqualsHashCode}} import java.util.Objects; import java.util.Arrays; +import java.util.Map; {{#useOptional}} import java.util.Optional; {{/useOptional}} +import java.util.stream.Collectors; {{#imports}} import {{import}}; {{/imports}} {{#serializableModel}} import java.io.Serializable; {{/serializableModel}} +{{#jackson}} import com.fasterxml.jackson.annotation.*; +{{/jackson}} +{{#micronaut_serde_jackson}} +import io.micronaut.serde.annotation.Serdeable; +{{/micronaut_serde_jackson}} {{#withXml}} import com.fasterxml.jackson.dataformat.xml.annotation.*; import com.fasterxml.jackson.annotation.*; @@ -31,7 +39,13 @@ import android.os.Parcel; import {{javaxPackage}}.validation.constraints.*; import {{javaxPackage}}.validation.Valid; {{/useBeanValidation}} -import io.micronaut.core.annotation.*; +import io.micronaut.core.annotation.Introspected; +{{^generateHardNullable}} +import io.micronaut.core.annotation.Nullable; +{{/generateHardNullable}} +{{#generateHardNullable}} +import {{{invokerPackage}}}.annotation.HardNullable; +{{/generateHardNullable}} import {{javaxPackage}}.annotation.Generated; {{#generateSwagger1Annotations}} import io.swagger.annotations.ApiModel; @@ -40,19 +54,22 @@ import io.swagger.annotations.ApiModelProperty; {{#generateSwagger2Annotations}} import io.swagger.v3.oas.annotations.media.Schema; {{/generateSwagger2Annotations}} +{{/formatNoEmptyLines}} -{{#models}} - {{#model}} - {{#isEnum}} -{{>common/model/modelEnum}} - {{/isEnum}} - {{^isEnum}} - {{#vendorExtensions.x-is-one-of-interface}} +{{#formatOneEmptyLine}} + {{#models}} + {{#model}} + {{#isEnum}} +{{>common/model/enum}} + {{/isEnum}} + {{^isEnum}} + {{#vendorExtensions.x-is-one-of-interface}} {{>common/model/oneof_interface}} - {{/vendorExtensions.x-is-one-of-interface}} - {{^vendorExtensions.x-is-one-of-interface}} + {{/vendorExtensions.x-is-one-of-interface}} + {{^vendorExtensions.x-is-one-of-interface}} {{>common/model/pojo}} - {{/vendorExtensions.x-is-one-of-interface}} - {{/isEnum}} - {{/model}} -{{/models}} \ No newline at end of file + {{/vendorExtensions.x-is-one-of-interface}} + {{/isEnum}} + {{/model}} + {{/models}} +{{/formatOneEmptyLine}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/modelEnum.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/modelEnum.mustache deleted file mode 100644 index bb48e80628..0000000000 --- a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/modelEnum.mustache +++ /dev/null @@ -1,61 +0,0 @@ -{{#jackson}} -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; -{{/jackson}} - -/** - * {{^description}}Gets or Sets {{{name}}}{{/description}}{{#description}}{{description}}{{/description}} - */ -{{#additionalEnumTypeAnnotations}} -{{{.}}} -{{/additionalEnumTypeAnnotations}}{{#useBeanValidation}}@Introspected -{{/useBeanValidation}}public enum {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} { - {{#allowableValues}} - {{#enumVars}} - {{#enumDescription}} - /** - * {{enumDescription}} - */ - {{/enumDescription}} - {{#withXml}} - @XmlEnumValue({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}) - {{/withXml}} - {{{name}}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} - {{/enumVars}} - {{/allowableValues}} - - private {{{dataType}}} value; - - {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}({{{dataType}}} value) { - this.value = value; - } - - {{#jackson}} - @JsonValue - {{/jackson}} - public {{{dataType}}} getValue() { - return value; - } - - @Override - public String toString() { - return String.valueOf(value); - } - - {{#jackson}} - @JsonCreator - {{/jackson}} - public static {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) { - for ({{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { - if (b.value.{{^isString}}equals{{/isString}}{{#isString}}{{#useEnumCaseInsensitive}}equalsIgnoreCase{{/useEnumCaseInsensitive}}{{^useEnumCaseInsensitive}}equals{{/useEnumCaseInsensitive}}{{/isString}}(value)) { - return b; - } - } - {{#isNullable}} - return null; - {{/isNullable}} - {{^isNullable}} - throw new IllegalArgumentException("Unexpected value '" + value + "'"); - {{/isNullable}} - } -} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/modelInnerEnum.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/modelInnerEnum.mustache deleted file mode 100644 index 669c8c0eb2..0000000000 --- a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/modelInnerEnum.mustache +++ /dev/null @@ -1,60 +0,0 @@ - /** - * {{^description}}Gets or Sets {{{name}}}{{/description}}{{#description}}{{description}}{{/description}} - */ -{{#withXml}} - @XmlType(name="{{datatypeWithEnum}}") - @XmlEnum({{dataType}}.class) -{{/withXml}} -{{#additionalEnumTypeAnnotations}} - {{{.}}} -{{/additionalEnumTypeAnnotations}} - public enum {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}} { - {{#allowableValues}} - {{#enumVars}} - {{#enumDescription}} - /** - * {{enumDescription}} - */ - {{/enumDescription}} - {{#withXml}} - @XmlEnumValue({{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}{{{value}}}{{#isInteger}}"{{/isInteger}}{{#isDouble}}"{{/isDouble}}{{#isLong}}"{{/isLong}}{{#isFloat}}"{{/isFloat}}) - {{/withXml}} - {{{name}}}({{{value}}}){{^-last}},{{/-last}}{{#-last}};{{/-last}} - {{/enumVars}} - {{/allowableValues}} - - private {{{dataType}}} value; - - {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}({{{dataType}}} value) { - this.value = value; - } - - {{#jackson}} - @JsonValue - {{/jackson}} - public {{{dataType}}} getValue() { - return value; - } - - @Override - public String toString() { - return String.valueOf(value); - } - - {{#jackson}} - @JsonCreator - {{/jackson}} - public static {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) { - for ({{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{#datatypeWithEnum}}{{{.}}}{{/datatypeWithEnum}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { - if (b.value.{{^isString}}equals{{/isString}}{{#isString}}{{#useEnumCaseInsensitive}}equalsIgnoreCase{{/useEnumCaseInsensitive}}{{^useEnumCaseInsensitive}}equals{{/useEnumCaseInsensitive}}{{/isString}}(value)) { - return b; - } - } - {{#isNullable}} - return null; - {{/isNullable}} - {{^isNullable}} - throw new IllegalArgumentException("Unexpected value '" + value + "'"); - {{/isNullable}} - } - } diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/oneof_interface.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/oneof_interface.mustache index fbf0a8340c..dda142625f 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/oneof_interface.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/oneof_interface.mustache @@ -1,8 +1,12 @@ +{{#formatNoEmptyLines}} {{#additionalOneOfTypeAnnotations}} {{{.}}} {{/additionalOneOfTypeAnnotations}} -{{>common/generatedAnnotation}}{{>common/model/typeInfoAnnotation}}{{>common/model/xmlAnnotation}} +{{>common/generatedAnnotation}} +{{>common/model/typeInfoAnnotation}} +{{>common/model/xmlAnnotation}} public interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { +{{/formatNoEmptyLines}} {{#discriminator}} public {{propertyType}} {{propertyGetter}}(); {{/discriminator}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/pojo.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/pojo.mustache index 7bd9ab2c8e..13cad6b48f 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/pojo.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/pojo.mustache @@ -1,3 +1,4 @@ +{{#formatNoEmptyLines}} /** * {{#description}}{{.}}{{/description}}{{^description}}{{classname}}{{/description}} */ @@ -10,43 +11,48 @@ {{/generateSwagger2Annotations}} {{/description}} {{#micronaut_serde_jackson}} -@io.micronaut.serde.annotation.Serdeable +@Serdeable {{/micronaut_serde_jackson}} {{#jackson}} @JsonPropertyOrder({ - {{#vars}} - {{classname}}.JSON_PROPERTY_{{nameInSnakeCase}}{{^-last}},{{/-last}} - {{/vars}} + {{#vars}} + {{classname}}.JSON_PROPERTY_{{nameInSnakeCase}}{{^-last}},{{/-last}} + {{/vars}} }) -@JsonTypeName("{{name}}") {{/jackson}} {{#additionalModelTypeAnnotations}} {{{.}}} {{/additionalModelTypeAnnotations}} -{{>common/generatedAnnotation}}{{#discriminator}}{{>common/model/typeInfoAnnotation}}{{/discriminator}}{{>common/model/xmlAnnotation}}{{#useBeanValidation}} +{{>common/generatedAnnotation}} +{{#discriminator}} +{{>common/model/typeInfoAnnotation}} +{{/discriminator}} +{{>common/model/xmlAnnotation}} +{{#useBeanValidation}} @Introspected {{/useBeanValidation}} {{#vendorExtensions.x-class-extra-annotation}} {{{vendorExtensions.x-class-extra-annotation}}} -{{/vendorExtensions.x-class-extra-annotation}}{{! -Declare the class with extends and implements -}}public class {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{ +{{/vendorExtensions.x-class-extra-annotation}} +{{!Declare the class with extends and implements}} +public class {{classname}} {{#parent}}extends {{{parent}}} {{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{ +{{/formatNoEmptyLines}} {{#serializableModel}} private static final long serialVersionUID = 1L; - {{/serializableModel}} + {{#vars}} - {{#isEnum}} - {{^isContainer}} -{{>common/model/modelInnerEnum}} - {{/isContainer}} - {{#isContainer}} - {{#mostInnerItems}} -{{>common/model/modelInnerEnum}} - {{/mostInnerItems}} - {{/isContainer}} - {{/isEnum}} public static final String JSON_PROPERTY_{{nameInSnakeCase}} = "{{baseName}}"; + {{/vars}} + + {{#vars}} + {{#formatNoEmptyLines}} + {{#description}} + /** + * {{description}} + */ + {{/description}} +{{>common/params/validation}} {{#withXml}} {{#isXmlAttribute}} @XmlAttribute(name = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}" @@ -68,8 +74,23 @@ Declare the class with extends and implements {{/isContainer}} {{/isXmlAttribute}} {{/withXml}} + {{#generateSwagger1Annotations}} + @ApiModelProperty({{#example}}example = "{{{example}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}") + {{/generateSwagger1Annotations}} + {{#generateSwagger2Annotations}} + @Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}) + {{/generateSwagger2Annotations}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + {{!Unannotated, Jackson would pick this up automatically and add it *in addition* to the _JsonNullable getter field}} + @JsonIgnore + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + {{#jackson}} +{{>common/model/jackson_annotations}} + {{/jackson}} + {{/vendorExtensions.x-is-jackson-optional-nullable}} {{#vendorExtensions.x-field-extra-annotation}} - {{{vendorExtensions.x-field-extra-annotation}}} + {{{vendorExtensions.x-field-extra-annotation}}} {{/vendorExtensions.x-field-extra-annotation}} {{#vendorExtensions.x-is-jackson-optional-nullable}} {{#isContainer}} @@ -81,14 +102,16 @@ Declare the class with extends and implements {{/vendorExtensions.x-is-jackson-optional-nullable}} {{^vendorExtensions.x-is-jackson-optional-nullable}} {{#isContainer}} - private {{{datatypeWithEnum}}} {{name}}{{#required}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/required}}{{^required}} = null{{/required}}; + private {{{datatypeWithEnum}}} {{name}}{{#required}}{{^requiredPropertiesInConstructor}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}{{/requiredPropertiesInConstructor}}{{/required}}; {{/isContainer}} {{^isContainer}} {{#isDiscriminator}}protected{{/isDiscriminator}}{{^isDiscriminator}}private{{/isDiscriminator}} {{{datatypeWithEnum}}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; {{/isContainer}} {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{/formatNoEmptyLines}} {{/vars}} + {{#requiredPropertiesInConstructor}} public {{classname}}({{#requiredVars}}{{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/requiredVars}}) { {{#parent}} @@ -98,8 +121,8 @@ Declare the class with extends and implements this.{{name}} = {{name}}; {{/vendorExtensions.requiredVars}} } - {{/requiredPropertiesInConstructor}} + {{^requiredPropertiesInConstructor}} public {{classname}}() { {{#parent}} @@ -107,8 +130,90 @@ Declare the class with extends and implements {{/parent}} } {{/requiredPropertiesInConstructor}} + {{#vars}} + /** + {{#description}} + * {{description}} + {{/description}} + * @return the {{name}} property value + */ + {{#vendorExtensions.x-extra-annotation}} + {{{vendorExtensions.x-extra-annotation}}} + {{/vendorExtensions.x-extra-annotation}} + public {{{datatypeWithEnum}}} {{getter}}() { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + {{#isReadOnly}} + {{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}} + if ({{name}} == null) { + {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; + } + {{/isReadOnly}} + return {{name}}.orElse(null); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + return {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + + {{#useOptional}} + {{^required}} + /** + {{#description}} + * {{description}} + {{/description}} + * @return the {{name}} property value wrapped in an optional + */ + {{#jackson}} + @JsonIgnore + {{/jackson}} + public Optional<{{{datatypeWithEnum}}}> {{getter}}Optional() { + {{^vendorExtensions.x-is-jackson-optional-nullable}} + return Optional.ofNullable({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{#vendorExtensions.x-is-jackson-optional-nullable}} + return Optional.ofNullable({{getter}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + {{/required}} + {{/useOptional}} + + /** + * Set the {{name}} property value + */ + {{#vendorExtensions.x-setter-extra-annotation}} + {{{vendorExtensions.x-setter-extra-annotation}}} + {{/vendorExtensions.x-setter-extra-annotation}} + {{^isReadOnly}}public {{/isReadOnly}}{{#isReadOnly}}{{^micronaut_serde_jersey}}private {{/micronaut_serde_jersey}}{{/isReadOnly}}void {{setter}}({{{datatypeWithEnum}}} {{name}}) { + {{#vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^vendorExtensions.x-is-jackson-optional-nullable}} + this.{{name}} = {{name}}; + {{/vendorExtensions.x-is-jackson-optional-nullable}} + } + + {{#vendorExtensions.x-is-jackson-optional-nullable}} + /** + * A JsonNullable getter that will be used for JSON serialization. + * @return JsonNullable version of {{name}} + */ +{{>common/model/jackson_annotations}} + public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}_JsonNullable() { + return {{name}}; + } + + @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) + {{#isReadOnly}}private{{/isReadOnly}}{{^isReadOnly}}public{{/isReadOnly}} void {{setter}}_JsonNullable(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) { + {{! For getters/setters that have name differing from attribute name, we must include setter (albeit private) for jackson to be able to set the attribute}} this.{{name}} = {{name}}; + } + {{/vendorExtensions.x-is-jackson-optional-nullable}} + {{^isReadOnly}} + /** + * Set {{name}} in a chainable fashion. + * @return The same instance of {{classname}} for chaining. + */ public {{classname}} {{name}}({{{datatypeWithEnum}}} {{name}}) { {{#vendorExtensions.x-is-jackson-optional-nullable}} this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); @@ -120,6 +225,10 @@ Declare the class with extends and implements } {{#isArray}} + /** + * Add an item to the {{name}} property in a chainable fashion. + * @return The same instance of {{classname}} for chaining. + */ public {{classname}} add{{nameInCamelCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { {{#vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null || !this.{{name}}.isPresent()) { @@ -142,9 +251,13 @@ Declare the class with extends and implements return this; {{/vendorExtensions.x-is-jackson-optional-nullable}} } - {{/isArray}} + {{#isMap}} + /** + * Set the value for the key for the {{name}} map property in a chainable fashion. + * @return The same instance of {{classname}} for chaining. + */ public {{classname}} put{{nameInCamelCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { {{#vendorExtensions.x-is-jackson-optional-nullable}} if (this.{{name}} == null || !this.{{name}}.isPresent()) { @@ -167,90 +280,11 @@ Declare the class with extends and implements return this; {{/vendorExtensions.x-is-jackson-optional-nullable}} } - {{/isMap}} - {{/isReadOnly}} - /** - {{#description}} - * {{description}} - {{/description}} - {{^description}} - * Get {{name}} - {{/description}} - {{#minimum}} - * minimum: {{minimum}} - {{/minimum}} - {{#maximum}} - * maximum: {{maximum}} - {{/maximum}} - * @return {{name}} - **/ -{{>common/model/beanValidation}}{{! - }}{{#generateSwagger1Annotations}} - @ApiModelProperty({{#example}}example = "{{{example}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}") - {{/generateSwagger1Annotations}} - {{#generateSwagger2Annotations}} - @Schema(name = "{{{baseName}}}"{{#isReadOnly}}, accessMode = Schema.AccessMode.READ_ONLY{{/isReadOnly}}{{#example}}, example = "{{{.}}}"{{/example}}{{#description}}, description = "{{{.}}}"{{/description}}, requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}) - {{/generateSwagger2Annotations}} - {{#vendorExtensions.x-extra-annotation}} - {{{vendorExtensions.x-extra-annotation}}} - {{/vendorExtensions.x-extra-annotation}} - {{#vendorExtensions.x-is-jackson-optional-nullable}} -{{!unannotated, Jackson would pick this up automatically and add it *in addition* to the _JsonNullable getter field}} @JsonIgnore - {{/vendorExtensions.x-is-jackson-optional-nullable}} - {{^vendorExtensions.x-is-jackson-optional-nullable}} - {{#jackson}} -{{>common/model/jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}} public {{#useOptional}}{{^required}}Optional<{{/required}}{{/useOptional}}{{{datatypeWithEnum}}}{{#useOptional}}{{^required}}>{{/required}}{{/useOptional}} {{getter}}() { - {{#vendorExtensions.x-is-jackson-optional-nullable}} - {{#isReadOnly}} -{{! A readonly attribute doesn't have setter => jackson will set null directly if explicitly returned by API, so make sure we have an empty JsonNullable}} if ({{name}} == null) { - {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; - } - {{/isReadOnly}} - return {{name}}.orElse(null); - {{/vendorExtensions.x-is-jackson-optional-nullable}} - {{^vendorExtensions.x-is-jackson-optional-nullable}} - {{#useOptional}} - {{#required}} - return {{name}}; - {{/required}} - {{^required}} - return Optional.ofNullable({{name}}); - {{/required}} - {{/useOptional}} - {{^useOptional}} - return {{name}}; - {{/useOptional}} - {{/vendorExtensions.x-is-jackson-optional-nullable}} - } - - {{#vendorExtensions.x-is-jackson-optional-nullable}} -{{>common/model/jackson_annotations}} - public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}_JsonNullable() { - return {{name}}; - } - - @JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}) - {{#isReadOnly}}private{{/isReadOnly}}{{^isReadOnly}}public{{/isReadOnly}} void {{setter}}_JsonNullable(JsonNullable<{{{datatypeWithEnum}}}> {{name}}) { - {{! For getters/setters that have name differing from attribute name, we must include setter (albeit private) for jackson to be able to set the attribute}} this.{{name}} = {{name}}; - } - - {{/vendorExtensions.x-is-jackson-optional-nullable}} - {{^isReadOnly}} - {{#jackson}} - {{^vendorExtensions.x-is-jackson-optional-nullable}} -{{>common/model/jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{#vendorExtensions.x-setter-extra-annotation}} {{{vendorExtensions.x-setter-extra-annotation}}} -{{/vendorExtensions.x-setter-extra-annotation}}{{/jackson}} public void {{setter}}({{{datatypeWithEnum}}} {{name}}) { - {{#vendorExtensions.x-is-jackson-optional-nullable}} - this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{name}}); - {{/vendorExtensions.x-is-jackson-optional-nullable}} - {{^vendorExtensions.x-is-jackson-optional-nullable}} - this.{{name}} = {{name}}; - {{/vendorExtensions.x-is-jackson-optional-nullable}} - } {{/isReadOnly}} {{/vars}} + @Override public boolean equals(Object o) { {{#useReflectionEqualsHashCode}} @@ -287,27 +321,11 @@ Declare the class with extends and implements @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("class {{classname}} {\n"); - {{#parent}} - sb.append(" ").append(toIndentedString(super.toString())).append("\n"); - {{/parent}} - {{#vars}} - sb.append(" {{name}}: ").append(toIndentedString({{name}})).append("\n"); - {{/vars}} - sb.append("}"); - return sb.toString(); - } - - /** - * Convert the given object to string with each line indented by 4 spaces - * (except the first line). - */ - private{{#jsonb}} static{{/jsonb}} String toIndentedString(Object o) { - if (o == null) { - return "null"; - } - return o.toString().replace("\n", "\n "); + return "{{classname}}(" + {{#allVars}} + + "{{name}}: " + {{{getter}}}(){{^-last}} + ", "{{/-last}} + {{/allVars}} + + ")"; } {{#parcelableModel}} @@ -404,4 +422,21 @@ Declare the class with extends and implements {{/interfaces.0}} {{/parent}} {{/visitable}} + + {{#vars}} + {{#isEnum}} + {{^isContainer}} + {{#indent}} +{{>common/model/enum}} + {{/indent}} + {{/isContainer}} + {{#isContainer}} + {{#mostInnerItems}} + {{#indent}} +{{>common/model/enum}} + {{/indent}} + {{/mostInnerItems}} + {{/isContainer}} + {{/isEnum}} + {{/vars}} } diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/typeInfoAnnotation.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/typeInfoAnnotation.mustache index 42f63ee9ae..7d26adf444 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/typeInfoAnnotation.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/typeInfoAnnotation.mustache @@ -1,34 +1,14 @@ {{#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 ) -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) -{{#discriminator.mappedModels}} -{{#-first}} -@JsonSubTypes({ -{{/-first}} - @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"), -{{#-last}} -}) -{{/-last}} -{{/discriminator.mappedModels}} +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}"{{^micronaut_serde_jackson}}, visible = true{{/micronaut_serde_jackson}}) +{{#discriminator}} + {{#mappedModels.0}} +@JsonSubTypes({ {{#mappedModels}} + @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"){{^-last}},{{/-last}} +{{/mappedModels}}{{closebarace}}) + {{/mappedModels.0}} +{{/discriminator}} {{/jackson}} -{{#micronaut_serde_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 -) -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}") -{{#discriminator.mappedModels}} -{{#-first}} -@JsonSubTypes({ -{{/-first}} - @JsonSubTypes.Type(value = {{modelName}}.class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"), -{{#-last}} -}) -{{/-last}} -{{/discriminator.mappedModels}} -{{/micronaut_serde_jackson}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/xmlAnnotation.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/xmlAnnotation.mustache index f0f2f475b4..d4135284cb 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/common/model/xmlAnnotation.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/common/model/xmlAnnotation.mustache @@ -1,6 +1,7 @@ {{#withXml}} - @XmlRootElement({{#xmlNamespace}}namespace="{{xmlNamespace}}", {{/xmlNamespace}}name = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{classname}}{{/xmlName}}") @XmlAccessorType(XmlAccessType.FIELD) {{#jackson}} -@JacksonXmlRootElement({{#xmlNamespace}}namespace="{{xmlNamespace}}", {{/xmlNamespace}}localName = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{classname}}{{/xmlName}}"){{/jackson}}{{/withXml}} +@JacksonXmlRootElement({{#xmlNamespace}}namespace="{{xmlNamespace}}", {{/xmlNamespace}}localName = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{classname}}{{/xmlName}}") +{{/jackson}} +{{/withXml}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/params/beanValidation.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/params/beanValidation.mustache deleted file mode 100644 index 8b625ef1b9..0000000000 --- a/openapi-generator/src/main/resources/templates/java-micronaut/common/params/beanValidation.mustache +++ /dev/null @@ -1,37 +0,0 @@ -{{!First handle the nullable - it should be present unless otherwise specified}} -{{#isNullable}}@Nullable {{/isNullable}}{{^isNullable}}{{^required}}@Nullable {{/required}}{{/isNullable}}{{! -All the validation -}}{{#useBeanValidation}}{{! -nullable overrides required -}}{{^isNullable}}{{#required}}@NotNull {{/required}}{{/isNullable}}{{! -validate all pojos and enums -}}{{^isContainer}}{{#isModel}}@Valid {{/isModel}}{{/isContainer}}{{! -pattern -}}{{#pattern}}{{^isByteArray}}@Pattern(regexp="{{{pattern}}}") {{/isByteArray}}{{/pattern}}{{! -both minLength && maxLength -}}{{#minLength}}{{#maxLength}}@Size(min={{minLength}}, max={{maxLength}}) {{/maxLength}}{{/minLength}}{{! -just minLength -}}{{#minLength}}{{^maxLength}}@Size(min={{minLength}}) {{/maxLength}}{{/minLength}}{{! -just maxLength -}}{{^minLength}}{{#maxLength}}@Size(max={{maxLength}}) {{/maxLength}}{{/minLength}}{{! -@Size: both minItems && maxItems -}}{{#minItems}}{{#maxItems}}@Size(min={{minItems}}, max={{maxItems}}) {{/maxItems}}{{/minItems}}{{! -@Size: just minItems -}}{{#minItems}}{{^maxItems}}@Size(min={{minItems}}) {{/maxItems}}{{/minItems}}{{! -@Size: just maxItems -}}{{^minItems}}{{#maxItems}}@Size(max={{maxItems}}) {{/maxItems}}{{/minItems}}{{! -@Email -}}{{#isEmail}}@Email {{/isEmail}}{{! -check for integer or long / all others=decimal type with @Decimal* -isInteger set -}}{{#isInteger}}{{#minimum}}@Min({{minimum}}) {{/minimum}}{{#maximum}}@Max({{maximum}}) {{/maximum}}{{/isInteger}}{{! -isLong set -}}{{#isLong}}{{#minimum}}@Min({{minimum}}L) {{/minimum}}{{#maximum}}@Max({{maximum}}L) {{/maximum}}{{/isLong}}{{! -Not Integer, not Long => we have a decimal value! -}}{{^isInteger}}{{^isLong}}{{! -minimum for decimal value -}}{{#minimum}}@DecimalMin({{#exclusiveMinimum}}value={{/exclusiveMinimum}}"{{minimum}}"{{#exclusiveMinimum}}, inclusive=false{{/exclusiveMinimum}}) {{/minimum}}{{! -maximal for decimal value -}}{{#maximum}}@DecimalMax({{#exclusiveMaximum}}value={{/exclusiveMaximum}}"{{maximum}}"{{#exclusiveMaximum}}, inclusive=false{{/exclusiveMaximum}}) {{/maximum}}{{! -close decimal values -}}{{/isLong}}{{/isInteger}}{{/useBeanValidation}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/common/params/validation.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/common/params/validation.mustache new file mode 100644 index 0000000000..ba84c554ab --- /dev/null +++ b/openapi-generator/src/main/resources/templates/java-micronaut/common/params/validation.mustache @@ -0,0 +1,95 @@ + +{{#isNullable}} + {{#generateHardNullable}} + @HardNullable + {{/generateHardNullable}} + {{^generateHardNullable}} + @Nullable + {{/generateHardNullable}} +{{/isNullable}} +{{^isNullable}} + {{^required}} + {{#generateHardNullable}} + @HardNullable + {{/generateHardNullable}} + {{^generateHardNullable}} + @Nullable + {{/generateHardNullable}} + {{/required}} +{{/isNullable}} +{{!All the validation}} +{{#useBeanValidation}} + {{^isNullable}} + {{#required}} + @NotNull + {{/required}} + {{/isNullable}} + {{!Validate all pojos and enums}} + {{^isContainer}} + {{#isModel}} + @Valid + {{/isModel}} + {{/isContainer}} + {{!Pattern}} + {{#pattern}} + {{^isByteArray}} + @Pattern(regexp="{{{pattern}}}") + {{/isByteArray}} + {{/pattern}} + {{!Min length && max length}} + {{#minLength}} + {{#maxLength}} + @Size(min={{minLength}}, max={{maxLength}}) + {{/maxLength}} + {{/minLength}} + {{#minLength}}{{^maxLength}} + @Size(min={{minLength}}) + {{/maxLength}}{{/minLength}} + {{^minLength}}{{#maxLength}} + @Size(max={{maxLength}}) + {{/maxLength}}{{/minLength}} + {{!Size}} + {{#minItems}}{{#maxItems}} + @Size(min={{minItems}}, max={{maxItems}}) + {{/maxItems}}{{/minItems}} + {{#minItems}}{{^maxItems}} + @Size(min={{minItems}}) + {{/maxItems}}{{/minItems}} + {{^minItems}}{{#maxItems}} + @Size(max={{maxItems}}) + {{/maxItems}}{{/minItems}} + {{!Email}} + {{#isEmail}} + @Email + {{/isEmail}} + {{!check for integer or long / all others=decimal type with @Decimal isInteger set}} + {{#isInteger}} + {{#minimum}} + @Min({{minimum}}) + {{/minimum}} + {{#maximum}} + @Max({{maximum}}) + {{/maximum}} + {{/isInteger}} + {{!isLong set}} + {{#isLong}} + {{#minimum}} + @Min({{minimum}}L) + {{/minimum}} + {{#maximum}} + @Max({{maximum}}L) + {{/maximum}} + {{/isLong}} + {{!Not Integer, not Long => we have a decimal value!}} + {{^isInteger}} + {{^isLong}}{{!minimum for decimal value}} + {{#minimum}} + @DecimalMin({{#exclusiveMinimum}}value={{/exclusiveMinimum}}"{{minimum}}"{{#exclusiveMinimum}}, inclusive=false{{/exclusiveMinimum}}) + {{/minimum}} + {{!maximal for decimal value}} + {{#maximum}} + @DecimalMax({{#exclusiveMaximum}}value={{/exclusiveMaximum}}"{{maximum}}"{{#exclusiveMaximum}}, inclusive=false{{/exclusiveMaximum}}) + {{/maximum}} + {{/isLong}} + {{/isInteger}} +{{/useBeanValidation}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/server/HardNullable.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/server/HardNullable.mustache new file mode 100644 index 0000000000..f6b2f73833 --- /dev/null +++ b/openapi-generator/src/main/resources/templates/java-micronaut/server/HardNullable.mustache @@ -0,0 +1,19 @@ +package {{invokerPackage}}.annotation; + +import io.micronaut.core.annotation.Nullable; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Target; + +/** + * An annotation specifying that a parameter is nullable, that is also inherited. + *

The annotation is used in generated operations to make implementations consistent with + * the specification.

+ */ +@Nullable +@Inherited +@Target({ElementType.PARAMETER, ElementType.TYPE_PARAMETER, ElementType.FIELD}) +public @interface HardNullable { + +} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/server/controller-interface.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/server/controller-interface.mustache index f489b68a5a..efc7a800be 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/server/controller-interface.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/server/controller-interface.mustache @@ -1,8 +1,14 @@ {{>common/licenseInfo}} package {{apiPackage}}; +{{#formatNoEmptyLines}} import io.micronaut.http.annotation.*; +{{^generateHardNullable}} import io.micronaut.core.annotation.Nullable; +{{/generateHardNullable}} +{{#generateHardNullable}} +import {{{invokerPackage}}}.annotation.HardNullable; +{{/generateHardNullable}} import io.micronaut.core.convert.format.Format; {{#useAuth}} import io.micronaut.security.annotation.Secured; @@ -35,14 +41,15 @@ import io.swagger.annotations.*; {{#generateSwagger2Annotations}} import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.Parameters; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.security.SecurityRequirement; {{/generateSwagger2Annotations}} +{{/formatNoEmptyLines}} +{{#formatOneEmptyLine}} {{>common/generatedAnnotation}} {{^generateControllerAsAbstract}} @Controller @@ -51,14 +58,15 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; @Tag(name = "{{{baseName}}}", description = {{#tagDescription}}"{{{.}}}"{{/tagDescription}}{{^tagDescription}}"The {{{baseName}}} API"{{/tagDescription}}) {{/generateSwagger2Annotations}} public interface {{classname}} { + {{#operations}} {{#operation}} -{{>common/operationAnnotations}}{{! - micronaut annotations - }} @{{#lambda.pascalcase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.pascalcase}}(uri="{{{path}}}") - @Produces(value = {{openbrace}}{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}{{closebrace}}) + {{#formatNoEmptyLines}} +{{>common/operationAnnotations}} + @{{#lambda.pascalcase}}{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}{{/lambda.pascalcase}}("{{{path}}}") + @Produces({{openbrace}}{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}{{closebrace}}) {{#consumes.0}} - @Consumes(value = {{openbrace}}{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}{{closebrace}}) + @Consumes({{openbrace}}{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}{{closebrace}}) {{/consumes.0}} {{!security annotations}} {{#useAuth}} @@ -66,11 +74,15 @@ public interface {{classname}} { {{/useAuth}} {{!the method definition}} {{>common/operationReturnType}} {{nickname}}({{#allParams}} - {{>server/params/queryParams}}{{>server/params/pathParams}}{{>server/params/headerParams}}{{>server/params/bodyParams}}{{>server/params/formParams}}{{>server/params/cookieParams}}{{^-last}}, {{/-last}}{{#-last}} - {{/-last}}{{/allParams}}); - {{^-last}} + {{#indent}} + {{>common/params/validation}} + {{/indent}} + {{>server/params/annotations}} + {{#formatSingleLine}}{{>server/params/type}} {{paramName}}{{^-last}},{{/-last}}{{/formatSingleLine}} + {{/allParams}}); + {{/formatNoEmptyLines}} - {{/-last}} {{/operation}} {{/operations}} } +{{/formatOneEmptyLine}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/annotations.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/server/params/annotations.mustache new file mode 100644 index 0000000000..3e215963d2 --- /dev/null +++ b/openapi-generator/src/main/resources/templates/java-micronaut/server/params/annotations.mustache @@ -0,0 +1,23 @@ + +{{#isQueryParam}} + @QueryValue({{#defaultValue}}value={{/defaultValue}}"{{{baseName}}}"{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) +{{/isQueryParam}} +{{#isPathParam}} + @PathVariable({{#defaultValue}}value={{/defaultValue}}"{{baseName}}"{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) +{{/isPathParam}} +{{#isHeaderParam}} + @Header({{#defaultValue}}value={{/defaultValue}}"{{baseName}}"{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) +{{/isHeaderParam}} +{{#isBodyParam}} + {{#isMultipart}} + @Part("{{baseName}}") + {{/isMultipart}} + {{^isMultipart}} + @Body + {{/isMultipart}} +{{/isBodyParam}} +{{#isFormParam}} +{{/isFormParam}} +{{#isCookieParam}} + @CookieValue({{#defaultValue}}value={{/defaultValue}}"{{baseName}}"{{#defaultValue}}, defaultValue="{{defaultValue}}"{{/defaultValue}}) +{{/isCookieParam}} diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/bodyParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/server/params/bodyParams.mustache deleted file mode 100644 index 80bca1989f..0000000000 --- a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/bodyParams.mustache +++ /dev/null @@ -1,7 +0,0 @@ -{{#isBodyParam}}{{! -Multi part -}}{{#isMultipart}}@Part("{{baseName}}"){{/isMultipart}}{{! -Non-multipart body -}}{{^isMultipart}}@Body{{/isMultipart}}{{! -The type -}} {{>common/params/beanValidation}}{{>server/params/type}} {{paramName}}{{/isBodyParam}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/cookieParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/server/params/cookieParams.mustache deleted file mode 100644 index be25ae2174..0000000000 --- a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/cookieParams.mustache +++ /dev/null @@ -1 +0,0 @@ -{{#isCookieParam}}@CookieValue(value="{{baseName}}"{{#defaultValue}}, defaultValue="{{defaultValue}}"{{/defaultValue}}) {{>common/params/beanValidation}}{{>server/params/type}} {{paramName}}{{/isCookieParam}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/formParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/server/params/formParams.mustache deleted file mode 100644 index 22c02d391b..0000000000 --- a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/formParams.mustache +++ /dev/null @@ -1 +0,0 @@ -{{#isFormParam}}{{>common/params/beanValidation}}{{>server/params/type}} {{paramName}}{{/isFormParam}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/headerParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/server/params/headerParams.mustache deleted file mode 100644 index 1cb2c90a91..0000000000 --- a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/headerParams.mustache +++ /dev/null @@ -1 +0,0 @@ -{{#isHeaderParam}}@Header(value="{{baseName}}"{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) {{>common/params/beanValidation}}{{>server/params/type}} {{paramName}}{{/isHeaderParam}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/pathParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/server/params/pathParams.mustache deleted file mode 100644 index 4e2fd4c2e1..0000000000 --- a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/pathParams.mustache +++ /dev/null @@ -1 +0,0 @@ -{{#isPathParam}}@PathVariable(value="{{baseName}}"{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) {{>common/params/beanValidation}}{{>server/params/type}} {{paramName}}{{/isPathParam}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/queryParams.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/server/params/queryParams.mustache deleted file mode 100644 index 0ba7b46061..0000000000 --- a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/queryParams.mustache +++ /dev/null @@ -1 +0,0 @@ -{{#isQueryParam}}@QueryValue(value="{{{baseName}}}"{{!default value}}{{#defaultValue}}, defaultValue="{{{defaultValue}}}"{{/defaultValue}}) {{!validation and type}}{{>common/params/beanValidation}}{{>server/params/type}} {{paramName}}{{/isQueryParam}} \ No newline at end of file diff --git a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/type.mustache b/openapi-generator/src/main/resources/templates/java-micronaut/server/params/type.mustache index 96d45a0c0c..ce27379188 100644 --- a/openapi-generator/src/main/resources/templates/java-micronaut/server/params/type.mustache +++ b/openapi-generator/src/main/resources/templates/java-micronaut/server/params/type.mustache @@ -1,7 +1,12 @@ -{{! -default type -}}{{^isDate}}{{^isDateTime}}{{{dataType}}}{{/isDateTime}}{{/isDate}}{{! -date-time -}}{{#isDateTime}}@Format("{{{datetimeFormat}}}") {{{dataType}}}{{/isDateTime}}{{! -date -}}{{#isDate}}@Format("{{{dateFormat}}}") {{{dataType}}}{{/isDate}} \ No newline at end of file +{{^isDate}}{{^isDateTime}}{{^isEnum}} + {{{dataType}}} +{{/isEnum}}{{/isDateTime}}{{/isDate}} +{{#isDateTime}} + @Format("{{{datetimeFormat}}}") {{{dataType}}} +{{/isDateTime}} +{{#isDate}} + @Format("{{{dateFormat}}}") {{{dataType}}} +{{/isDate}} +{{#isEnum}} + {{{datatypeWithEnum}}} +{{/isEnum}} diff --git a/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenSerializationLibraryTest.java b/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenSerializationLibraryTest.java index c97d2fa8bc..4a5564ac9a 100644 --- a/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenSerializationLibraryTest.java +++ b/openapi-generator/src/test/java/io/micronaut/openapi/generator/JavaMicronautClientCodegenSerializationLibraryTest.java @@ -13,7 +13,7 @@ public void testSerializationLibraryJackson() { CodegenConstants.MODELS); // Model does not contain micronaut serde annotation - String micronautSerDeAnnotation = "@io.micronaut.serde.annotation.Serdeable"; + String micronautSerDeAnnotation = "@Serdeable"; String modelPath = outputPath + "src/main/java/org/openapitools/model/"; assertFileNotContains(modelPath + "Pet.java", micronautSerDeAnnotation); assertFileNotContains(modelPath + "User.java", micronautSerDeAnnotation); @@ -37,13 +37,14 @@ public void testSerializationLibraryMicronautSerdeJackson() { CodegenConstants.MODELS); // Model contains micronaut serde annotation - String micronautSerDeAnnotation = "@io.micronaut.serde.annotation.Serdeable"; + String micronautSerdeAnnotation = "@Serdeable"; String modelPath = outputPath + "src/main/java/org/openapitools/model/"; - assertFileContains(modelPath + "Pet.java", micronautSerDeAnnotation); - assertFileContains(modelPath + "User.java", micronautSerDeAnnotation); - assertFileContains(modelPath + "Order.java", micronautSerDeAnnotation); - assertFileContains(modelPath + "Tag.java", micronautSerDeAnnotation); - assertFileContains(modelPath + "Category.java", micronautSerDeAnnotation); + assertFileContains(modelPath + "Pet.java", "import io.micronaut.serde.annotation.Serdeable"); + assertFileContains(modelPath + "Pet.java", micronautSerdeAnnotation); + assertFileContains(modelPath + "User.java", micronautSerdeAnnotation); + assertFileContains(modelPath + "Order.java", micronautSerdeAnnotation); + assertFileContains(modelPath + "Tag.java", micronautSerdeAnnotation); + assertFileContains(modelPath + "Category.java", micronautSerdeAnnotation); //JsonFormat with micronaut-serde-jackson must be without shape attribute assertFileNotContains(modelPath + "Order.java", "@JsonFormat(shape = JsonFormat.Shape.STRING"); diff --git a/test-suite-client-generator/build.gradle b/test-suite-client-generator/build.gradle index 06bea279f5..ddaccc92ad 100644 --- a/test-suite-client-generator/build.gradle +++ b/test-suite-client-generator/build.gradle @@ -1,5 +1,6 @@ plugins { id 'io.micronaut.build.internal.openapi-generator-test-suite' + id 'groovy' } description = """ diff --git a/test-suite-generator-util/src/main/java/io/micronaut/openapi/testsuite/GeneratorMain.java b/test-suite-generator-util/src/main/java/io/micronaut/openapi/testsuite/GeneratorMain.java index 965e6ea78a..67d936749b 100644 --- a/test-suite-generator-util/src/main/java/io/micronaut/openapi/testsuite/GeneratorMain.java +++ b/test-suite-generator-util/src/main/java/io/micronaut/openapi/testsuite/GeneratorMain.java @@ -20,40 +20,53 @@ import java.io.File; import java.net.URI; import java.net.URISyntaxException; - -import static io.micronaut.openapi.generator.MicronautCodeGeneratorEntryPoint.OutputKind.*; +import java.util.Arrays; /** * An entry point to be used in tests, to simulate * what the Micronaut OpenAPI Gradle plugin would do */ public class GeneratorMain { + + /** + * The main executable. + * + * @param args The argument array, consisting of: + *
    + *
  1. Server or client boolean.
  2. + *
  3. The definition file path.
  4. + *
  5. The output directory.
  6. + *
  7. A comma-separated list of output kinds.
  8. + *
+ * @throws URISyntaxException In case definition file path is incorrect. + */ public static void main(String[] args) throws URISyntaxException { boolean server = "server".equals(args[0]); + MicronautCodeGeneratorEntryPoint.OutputKind[] outputKinds + = Arrays.stream(args[3].split(",")) + .map(MicronautCodeGeneratorEntryPoint.OutputKind::of) + .toArray(MicronautCodeGeneratorEntryPoint.OutputKind[]::new); + var builder = MicronautCodeGeneratorEntryPoint.builder() .withDefinitionFile(new URI(args[1])) .withOutputDirectory(new File(args[2])) - .withOutputs( - MODELS, - APIS, - API_TESTS, - MODEL_TESTS - ) + .withOutputs(outputKinds) .withOptions(options -> { - options.withInvokerPackage("io.micronaut.openapi.test.invoker"); + options.withInvokerPackage("io.micronaut.openapi.test"); options.withApiPackage("io.micronaut.openapi.test.api"); options.withModelPackage("io.micronaut.openapi.test.model"); options.withBeanValidation(true); options.withOptional(true); options.withReactive(true); - options.withTestFramework(MicronautCodeGeneratorEntryPoint.TestFramework.JUNIT5); + options.withTestFramework(MicronautCodeGeneratorEntryPoint.TestFramework.SPOCK); }); if (server) { builder.forServer(serverOptions -> { serverOptions.withControllerPackage("io.micronaut.openapi.test.controller"); // commented out because currently this would prevent the test project from compiling // because we generate both abstract classes _and_ dummy implementations - serverOptions.withGenerateAbstractClasses(true); + serverOptions.withGenerateImplementationFiles(false); + serverOptions.withAuthentication(false); }); } else { builder.forClient(client -> { diff --git a/test-suite-server-generator/build.gradle b/test-suite-server-generator/build.gradle index a5bef42ad9..297f675539 100644 --- a/test-suite-server-generator/build.gradle +++ b/test-suite-server-generator/build.gradle @@ -1,5 +1,6 @@ plugins { id 'io.micronaut.build.internal.openapi-generator-test-suite' + id 'groovy' } description = """ @@ -9,7 +10,7 @@ that tests can be ran with Micronaut 4 dependencies { annotationProcessor("io.micronaut:micronaut-http-validation") - implementation("io.micronaut.security:micronaut-security") + annotationProcessor("io.micronaut.serde:micronaut-serde-processor") implementation("io.micronaut:micronaut-http-client") implementation("io.micronaut.serde:micronaut-serde-api") implementation("jakarta.annotation:jakarta.annotation-api") @@ -18,11 +19,23 @@ dependencies { implementation(projects.micronautOpenapi) { because("Required for Swagger") } + testCompileOnly("io.micronaut:micronaut-inject-groovy-test") + testCompileOnly("io.micronaut:micronaut-inject-java-test") + testImplementation("io.micronaut.test:micronaut-test-spock") + testRuntimeOnly("io.micronaut:micronaut-json-core") testRuntimeOnly("io.micronaut.serde:micronaut-serde-jackson") runtimeOnly("ch.qos.logback:logback-classic") } +sourceSets { + test { + java.srcDir('src/test/groovy') + } +} + tasks.named("generateOpenApi") { generatorKind = "server" + openApiDefinition = layout.projectDirectory.file("spec.yaml") + outputKinds = ["models", "apis", "supportingFiles"] } diff --git a/test-suite-server-generator/petstore.json b/test-suite-server-generator/petstore.json deleted file mode 100644 index 66d5f18836..0000000000 --- a/test-suite-server-generator/petstore.json +++ /dev/null @@ -1,973 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "description": "This is a sample server Petstore server. For this sample, you can use the api key \"special-key\" to test the authorization filters", - "version": "1.0.0", - "title": "OpenAPI Petstore", - "license": { - "name": "Apache 2.0", - "url": "https://www.apache.org/licenses/LICENSE-2.0.html" - } - }, - "host": "petstore.swagger.io", - "basePath": "/v2", - "schemes": [ - "http" - ], - "paths": { - "/pet": { - "post": { - "tags": [ - "pet" - ], - "summary": "Add a new pet to the store", - "description": "aa", - "operationId": "addPet", - "consumes": [ - "application/json", - "application/xml" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Pet object that needs to be added to the store", - "required": false, - "schema": { - "$ref": "#/definitions/Pet" - } - } - ], - "responses": { - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "", - "operationId": "updatePet", - "consumes": [ - "application/json", - "application/xml" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Pet object that needs to be added to the store", - "required": false, - "schema": { - "$ref": "#/definitions/Pet" - } - } - ], - "responses": { - "405": { - "description": "Validation exception" - }, - "404": { - "description": "Pet not found" - }, - "400": { - "description": "Invalid ID supplied" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": false, - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi", - "default": ["available"] - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Pet" - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByTags": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by tags", - "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", - "operationId": "findPetsByTags", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "tags", - "in": "query", - "description": "Tags to filter by", - "required": false, - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Pet" - } - } - }, - "400": { - "description": "Invalid tag value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/{petId}": { - "get": { - "tags": [ - "pet" - ], - "summary": "Find pet by ID", - "description": "Returns a pet when ID < 10. ID > 10 or nonintegers will simulate API error conditions", - "operationId": "getPetById", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet that needs to be fetched", - "required": true, - "type": "integer", - "format": "int64" - } - ], - "responses": { - "404": { - "description": "Pet not found" - }, - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/Pet" - } - }, - "400": { - "description": "Invalid ID supplied" - } - }, - "security": [ - { - "api_key": [] - }, - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Updates a pet in the store with form data", - "description": "", - "operationId": "updatePetWithForm", - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet that needs to be updated", - "required": true, - "type": "string" - }, - { - "name": "name", - "in": "formData", - "description": "Updated name of the pet", - "required": false, - "type": "string" - }, - { - "name": "status", - "in": "formData", - "description": "Updated status of the pet", - "required": false, - "type": "string" - } - ], - "responses": { - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "delete": { - "tags": [ - "pet" - ], - "summary": "Deletes a pet", - "description": "", - "operationId": "deletePet", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "api_key", - "in": "header", - "description": "", - "required": false, - "type": "string" - }, - { - "name": "petId", - "in": "path", - "description": "Pet id to delete", - "required": true, - "type": "integer", - "format": "int64" - } - ], - "responses": { - "400": { - "description": "Invalid pet value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/{petId}/uploadImage": { - "post": { - "tags": [ - "pet" - ], - "summary": "uploads an image", - "description": "", - "operationId": "uploadFile", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet to update", - "required": true, - "type": "integer", - "format": "int64" - }, - { - "name": "additionalMetadata", - "in": "formData", - "description": "Additional data to pass to server", - "required": false, - "type": "string" - }, - { - "name": "file", - "in": "formData", - "description": "file to upload", - "required": false, - "type": "file" - } - ], - "responses": { - "default": { - "description": "successful operation" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/inventory": { - "get": { - "tags": [ - "store" - ], - "summary": "Returns pet inventories by status", - "description": "Returns a map of status codes to quantities", - "operationId": "getInventory", - "produces": [ - "application/json", - "application/xml" - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - } - } - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "", - "operationId": "placeOrder", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "order placed for purchasing the pet", - "required": false, - "schema": { - "$ref": "#/definitions/Order" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/Order" - } - }, - "400": { - "description": "Invalid Order" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions", - "operationId": "getOrderById", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of pet that needs to be fetched", - "required": true, - "type": "string" - } - ], - "responses": { - "404": { - "description": "Order not found" - }, - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/Order" - } - }, - "400": { - "description": "Invalid ID supplied" - } - } - }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", - "operationId": "deleteOrder", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "type": "string" - } - ], - "responses": { - "404": { - "description": "Order not found" - }, - "400": { - "description": "Invalid ID supplied" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Created user object", - "required": false, - "schema": { - "$ref": "#/definitions/User" - } - } - ], - "responses": { - "default": { - "description": "successful operation" - } - } - } - }, - "/user/createWithArray": { - "post": { - "tags": [ - "user" - ], - "summary": "Creates list of users with given input array", - "description": "", - "operationId": "createUsersWithArrayInput", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "List of user object", - "required": false, - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/User" - } - } - } - ], - "responses": { - "default": { - "description": "successful operation" - } - } - } - }, - "/user/createWithList": { - "post": { - "tags": [ - "user" - ], - "summary": "Creates list of users with given input array", - "description": "", - "operationId": "createUsersWithListInput", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "List of user object", - "required": false, - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/User" - } - } - } - ], - "responses": { - "default": { - "description": "successful operation" - } - } - } - }, - "/user/login": { - "get": { - "tags": [ - "user" - ], - "summary": "Logs user into the system", - "description": "", - "operationId": "loginUser", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "username", - "in": "query", - "description": "The user name for login", - "required": false, - "type": "string" - }, - { - "name": "password", - "in": "query", - "description": "The password for login in clear text", - "required": false, - "type": "string" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "type": "string" - } - }, - "400": { - "description": "Invalid username/password supplied" - } - } - } - }, - "/user/logout": { - "get": { - "tags": [ - "user" - ], - "summary": "Logs out current logged in user session", - "description": "", - "operationId": "logoutUser", - "produces": [ - "application/json", - "application/xml" - ], - "responses": { - "default": { - "description": "successful operation" - } - } - } - }, - "/user/{username}": { - "get": { - "tags": [ - "user" - ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "type": "string" - } - ], - "responses": { - "404": { - "description": "User not found" - }, - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/User" - }, - "examples": { - "application/json": { - "id": 1, - "username": "johnp", - "firstName": "John", - "lastName": "Public", - "email": "johnp@swagger.io", - "password": "-secret-", - "phone": "0123456789", - "userStatus": 0 - } - } - }, - "400": { - "description": "Invalid username supplied" - } - } - }, - "put": { - "tags": [ - "user" - ], - "summary": "Updated user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "username", - "in": "path", - "description": "name that need to be deleted", - "required": true, - "type": "string" - }, - { - "in": "body", - "name": "body", - "description": "Updated user object", - "required": false, - "schema": { - "$ref": "#/definitions/User" - } - } - ], - "responses": { - "404": { - "description": "User not found" - }, - "400": { - "description": "Invalid user supplied" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "type": "string" - } - ], - "responses": { - "404": { - "description": "User not found" - }, - "400": { - "description": "Invalid username supplied" - } - } - } - } - }, - "securityDefinitions": { - "api_key": { - "type": "apiKey", - "name": "api_key", - "in": "header" - }, - "petstore_auth": { - "type": "oauth2", - "authorizationUrl": "http://petstore.swagger.io/api/oauth/dialog", - "flow": "implicit", - "scopes": { - "write:pets": "modify pets in your account", - "read:pets": "read your pets" - } - } - }, - "definitions": { - "User": { - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "username": { - "type": "string" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "userStatus": { - "type": "integer", - "format": "int32", - "description": "User Status" - } - }, - "xml": { - "name": "User" - } - }, - "Category": { - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - } - }, - "xml": { - "name": "Category" - } - }, - "Pet": { - "required": [ - "name", - "photoUrls" - ], - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "category": { - "$ref": "#/definitions/Category" - }, - "name": { - "type": "string", - "example": "doggie" - }, - "photoUrls": { - "type": "array", - "xml": { - "name": "photoUrl", - "wrapped": true - }, - "items": { - "type": "string" - } - }, - "tags": { - "type": "array", - "xml": { - "name": "tag", - "wrapped": true - }, - "items": { - "$ref": "#/definitions/Tag" - } - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "Pet" - } - }, - "Tag": { - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - } - }, - "xml": { - "name": "Tag" - } - }, - "Order": { - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "petId": { - "type": "integer", - "format": "int64" - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "shipDate": { - "type": "string", - "format": "date-time" - }, - "status": { - "type": "string", - "description": "Order Status", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "Order" - } - } - } -} diff --git a/test-suite-server-generator/spec.yaml b/test-suite-server-generator/spec.yaml new file mode 100644 index 0000000000..85deaafed2 --- /dev/null +++ b/test-suite-server-generator/spec.yaml @@ -0,0 +1,787 @@ +swagger: "2.0" +info: + version: 1.0.0 + title: Compute API + description: API for the Compute Service +host: localhost:8000 +basePath: /api +schemes: + - http +consumes: + - application/json +produces: + - application/json + +x-headers: + PageNumberHeader: &Page-Number-header + name: Page-Number + in: header + type: string + description: The page number of the current page + PageSizeHeader: &Page-Size-header + name: Page-Size + in: header + type: string + description: The number of items per page + TotalCountHeader: &Total-Count-header + name: Total-Count + in: header + type: string + description: | + The total number of items available in the entire collections, not just the items returned in the current page + PageCountHeader: &Page-Count-header + name: Page-Count + in: header + type: string + description: The total number of pages based on the page size and total count + LinkHeader: &Link-header + name: Link + in: header + type: string + description: The URLS to the first, last, previous and next pages. + +paths: + /sendPrimitives/{name}: + get: + operationId: sendPrimitives + tags: [ parameters ] + description: A method to send primitives as request parameters + parameters: + - name: name + in: path + type: string + required: true + - name: age + in: query + type: number + required: true + - name: height + in: header + type: number + format: float + required: true + - name: isPositive + in: query + type: boolean + required: true + responses: + 200: + description: Success + schema: + type: object + title: SendPrimitivesResponse + properties: + name: + type: string + age: + type: number + height: + type: number + format: float + isPositive: + type: boolean + default: + $ref: '#/responses/Error' + /sendValidatedPrimitives: + get: + operationId: sendValidatedPrimitives + tags: [ parameters ] + description: a method to send primitives with validation + produces: [ 'text/plain' ] + parameters: + - name: name + in: query + type: string + minLength: 3 + pattern: "[a-zA-Z]+" + - name: age + in: query + type: integer + format: int + minimum: 10 + maximum: 200 + - name: favoriteNumber + in: query + type: number + minimum: -100.5 + maximum: 100.5 + - name: height + in: query + type: number + format: double + minimum: 0.1 + maximum: 3 + exclusiveMaximum: true + exclusiveMinimum: true + responses: + 200: + description: Success + schema: + type: string + default: + $ref: '#/responses/Error' + /sendDates: + get: + operationId: sendDates + tags: [ parameters ] + description: A method to send dates as parameters + parameters: + - name: commitDate + type: string + in: query + format: date + - name: commitDateTime + type: string + in: query + format: date-time + responses: + 200: + description: Success + schema: + type: object + title: SendDatesResponse + properties: + commitDate: + type: string + format: date + commitDateTime: + type: string + format: date-time + default: + $ref: '#/responses/Error' + /sendOpcRequestId: + get: + operationId: sendIgnoredHeader + tags: [ parameters ] + description: | + A method that takes ignored-header as a header. + It will be ignored, this behavior is most likely used when handling is implemented in a filter. + parameters: + - in: header + name: ignored-header + type: string + responses: + 200: + description: Success + schema: + type: string + /getIgnoredHeader: + get: + operationId: getIgnoredHeader + tags: [ parameters ] + description: | + A method that returns a header that should be ignored. + It will be ignored, this behavior is most likely used when it will be set in a filter. + responses: + 200: + description: Success + headers: + ignored-header: + type: string + schema: + type: string + /sendPageQuery: + get: + operationId: sendPageQuery + tags: [ parameters ] + description: A method that takes page query as its argument + parameters: + - $ref: '#/parameters/PageQueryParam' + - $ref: '#/parameters/PageSizeQueryParam' + - $ref: '#/parameters/PageSortQueryParam' + responses: + 200: + description: Success + schema: + type: string + /sendValidatedCollection: + post: + operationId: sendValidatedCollection + tags: [ requestBody ] + description: A method to send a validated collection in body + requestBody: + content: + 'application/json': + schema: + name: collection + type: array + items: + type: array + items: + type: string + minLength: 3 + pattern: "[a-zA-Z]+" + responses: + 200: + description: Success + /sendSimpleModel: + post: + operationId: sendSimpleModel + tags: [ requestBody ] + description: A method to send a simple model in body + parameters: + - name: simpleModel + in: body + required: true + schema: + $ref: '#/definitions/SimpleModel' + responses: + 200: + description: Success + schema: + $ref: '#/definitions/SimpleModel' + default: + $ref: '#/responses/Error' + /sendListOfSimpleModels: + post: + operationId: sendListOfSimpleModels + tags: [ requestBody ] + description: A method to send a list of simple models in body + parameters: + - name: simpleModels + in: body + required: true + schema: + type: array + items: + $ref: '#/definitions/SimpleModel' + responses: + 200: + description: Success + schema: + type: array + items: + $ref: '#/definitions/SimpleModel' + /sendModelWithRequiredProperties: + post: + operationId: sendModelWithRequiredProperties + tags: [ requestBody ] + description: A method to send a model with required properties + parameters: + - name: model + in: body + required: true + schema: + $ref: '#/definitions/ModelWithRequiredProperties' + responses: + 200: + description: Success + schema: + $ref: '#/definitions/ModelWithRequiredProperties' + default: + $ref: '#/responses/Error' + /sendDateModel: + post: + operationId: sendDateModel + tags: [ requestBody ] + description: A method to send a model with dates as properties + parameters: + - name: model + in: body + required: true + schema: + $ref: '#/definitions/DateModel' + responses: + 200: + description: Success + schema: + $ref: '#/definitions/DateModel' + default: + $ref: '#/responses/Error' + /sendEnum: + post: + operationId: sendEnum + tags: [ requestBody ] + description: A method to send a simple enum in body + parameters: + - name: color + in: body + required: true + schema: + $ref: '#/definitions/ColorEnum' + responses: + 200: + description: Success + schema: + $ref: '#/definitions/ColorEnum' + default: + $ref: '#/responses/Error' + /sendEnumList: + post: + operationId: sendEnumList + tags: [ requestBody ] + description: A method to send an enum list + parameters: + - name: available-colors + in: body + required: true + schema: + type: array + items: + $ref: '#/definitions/ColorEnum' + responses: + 200: + description: Success + schema: + type: array + items: + $ref: '#/definitions/ColorEnum' + default: + $ref: '#/responses/Error' + /sendModelWithMapProperty: + post: + operationId: sendModelWithMapProperty + tags: [ requestBody ] + description: A method to send a model that contains maps + parameters: + - name: model + in: body + required: true + schema: + $ref: '#/definitions/ModelWithMapProperty' + responses: + 200: + description: Success + schema: + $ref: '#/definitions/ModelWithMapProperty' + /sendNestedModel: + post: + operationId: sendNestedModel + tags: [ requestBody ] + description: A method to send a model with another one nested as a property + parameters: + - name: model + in: body + required: true + schema: + $ref: '#/definitions/NestedModel' + responses: + 200: + description: Success + schema: + $ref: '#/definitions/NestedModel' + default: + $ref: '#/responses/Error' + /sendModelWithInnerEnum: + post: + operationId: sendModelWithInnerEnum + tags: [ requestBody ] + description: A method to send a model with an inner enum as property + parameters: + - name: model + in: body + required: true + schema: + $ref: '#/definitions/ModelWithInnerEnum' + responses: + 200: + description: Success + schema: + $ref: '#/definitions/ModelWithInnerEnum' + default: + $ref: '#/responses/Error' + /sendModelWithEnumList: + post: + operationId: sendModelWithEnumList + tags: [ requestBody ] + description: A method to send a model with an enum list as a property + parameters: + - name: model + in: body + required: true + schema: + $ref: '#/definitions/ModelWithEnumList' + responses: + 200: + description: Success + schema: + $ref: '#/definitions/ModelWithEnumList' + default: + $ref: '#/responses/Error' + /sendModelWithDiscriminator: + put: + operationId: sendModelWithDiscriminator + tags: [ requestBody ] + description: A method to send a model with discriminator in body + parameters: + - name: model + in: body + required: true + schema: + $ref: '#/definitions/Animal' + responses: + 200: + description: Success + schema: + $ref: '#/definitions/Animal' + /sendFile: + put: + operationId: sendFile + tags: [ requestBody ] + description: A method to send file as a request body + consumes: [ "multipart/form-data" ] + parameters: + - in: formData + name: file + type: file + responses: + 200: + description: Success + schema: + type: string + format: byte + default: + $ref: '#/responses/Error' + /sendBytes: + put: + operationId: sendBytes + tags: [ requestBody ] + description: A method to send bytes + produces: [ "application/octet-stream" ] + consumes: [ "application/octet-stream" ] + parameters: + - in: body + name: bytes + schema: + type: string + format: byte + responses: + 200: + description: Success + schema: + type: string + format: byte + /getSimpleModel: + get: + operationId: getSimpleModel + tags: [ responseBody ] + description: A method to get a simple model as a response + responses: + 200: + description: Success + schema: + $ref: '#/definitions/SimpleModel' + default: + $ref: '#/responses/Error' + /getSimpleModelWithNonStandardStatus: + get: + operationId: getSimpleModelWithNonStandardStatus + tags: [ responseBody ] + description: A method to get a simple model as a response + responses: + 202: + description: Success + schema: + $ref: '#/definitions/SimpleModel' + default: + $ref: '#/responses/Error' + /getPaginatedSimpleModel: + get: + operationId: getPaginatedSimpleModel + parameters: + - $ref: '#/parameters/PageQueryParam' + tags: [ responseBody ] + description: A method to get a simple model list as a paginated response + responses: + 200: + description: Success + headers: + Page-Number: *Page-Number-header + Page-Size: *Page-Size-header + Total-Count: *Total-Count-header + Page-Count: *Page-Count-header + Link: *Link-header + schema: + type: array + items: + $ref: '#/definitions/SimpleModel' + /getDatedSimpleModel: + get: + operationId: getDatedSimpleModel + tags: [ responseBody ] + description: A method to get a simple model with last-modified header + responses: + 202: + description: Success + headers: + Last-Modified: + type: string + format: date-time + description: The last modified date for the requested object + schema: + $ref: '#/definitions/SimpleModel' + /getSimpleModelWithNonMappedHeader: + get: + operationId: getSimpleModelWithNonMappedHeader + tags: [ responseBody ] + description: A method to get a simple model as a response + responses: + 200: + description: Success + headers: + custom-header: + type: string + description: A header with an additional description + schema: + $ref: '#/definitions/SimpleModel' + default: + $ref: '#/responses/Error' + /getDatedSimpleModelWithNonMappedHeader: + get: + operationId: getDatedSimpleModelWithNonMappedHeader + tags: [ responseBody ] + description: A method to get a tagged simple model with non standard headers + responses: + 200: + description: Success + headers: + Last-Modified: + type: string + custom-header: + type: string + description: A custom header + schema: + $ref: '#/definitions/SimpleModel' + /getErrorResponse: + get: + operationId: getErrorResponse + tags: [ responseBody ] + description: A method throwing an error response + responses: + 404: + description: Not Found + schema: + $ref: '#/definitions/Error' + /getFile: + get: + operationId: getFile + tags: [ responseBody ] + description: A method to get file as a response body + produces: [ "text/plain" ] + responses: + 200: + description: Success + schema: + type: file + default: + $ref: '#/responses/Error' + +definitions: + SimpleModel: + type: object + properties: + color: + type: string + minLength: 2 + numEdges: + minimum: 1 + type: integer + format: int64 + area: + minimum: 0 + exclusiveMinimum: true + type: number + format: float + exactArea: + minimum: 0 + exclusiveMinimum: true + type: number + convex: + type: boolean + points: + type: array + minItems: 3 + items: + type: string + state: + $ref: '#/definitions/StateEnum' + ModelWithRequiredProperties: + type: object + properties: + species: + type: string + weight: + type: number + format: float + numRepresentatives: + type: integer + format: int32 + description: + type: string + required: [species, weight] + DateModel: + type: object + properties: + commitDate: + type: string + format: date + commitDateTime: + type: string + format: date-time + NestedModel: + type: object + properties: + simpleModel: + $ref: '#/definitions/SimpleModel' + additionalInfo: + type: string + ModelWithInnerEnum: + type: object + properties: + species-name: + type: string + num-representatives: + type: integer + format: int64 + mammal-order: + title: MammalOrder + type: string + enum: [ monotreme, marsupial, placental ] + ModelWithEnumList: + type: object + properties: + favoriteColors: + type: array + items: + $ref: '#/definitions/ColorEnum' + ModelWithMapProperty: + type: object + properties: + map: + type: object + additionalProperties: + type: string + deepMap: + type: object + additionalProperties: + type: object + additionalProperties: + minLength: 2 + type: string + deepObjectMap: + type: object + additionalProperties: + type: object + additionalProperties: + type: object + $ref: '#/definitions/SimpleModel' + StateEnum: + type: string + enum: ['starting', 'running', 'stopped', 'deleted'] + Animal: + type: object + discriminator: class + properties: + class: + type: string + color: + $ref: '#/definitions/ColorEnum' + Bird: + type: object + allOf: + - $ref: '#/definitions/Animal' + - discriminator: ave + properties: + numWings: + type: integer + format: int32 + beakLength: + type: number + featherDescription: + type: string + Mammal: + type: object + required: [ weight, description ] + allOf: + - $ref: '#/definitions/Animal' + - discriminator: mammalia + properties: + weight: + type: number + format: float + description: + type: string + Reptile: + type: object + required: [numLegs, fangs] + allOf: + - $ref: '#/definitions/Animal' + - discriminator: reptilia + properties: + numLegs: + type: integer + fangs: + type: boolean + fangDescription: + type: string + ColorEnum: + type: string + enum: ['red', 'blue', 'green', 'light-blue', 'dark-green'] + ProxyFleetSummary: + type: object + description: proxy fleet summary object + properties: + proxyFleetName: + type: string + description: The unique name of the splat proxy fleet. + compartmentId: + type: string + description: The compartment id + location: + type: string + description: the location of the fleet, SUBSTRATE/OVERLAY + enum: [SUBSTRATE, OVERLAY] + lifecycleState: + type: string + description: the status of the fleet, ACTIVE/DELETED + enum: [ACTIVE, DELETED] + services: + type: array + description: an array of service names on the fleet + items: + type: string + Error: + type: object + description: An object for describing errors + properties: + message: + type: string + description: The error message + +parameters: + PageQueryParam: + name: page + in: query + type: integer + minimum: 0 + default: 0 + description: The page number to retrieve starting from 0. + PageSizeQueryParam: + name: size + in: query + type: integer + minimum: 1 + default: 10 + description: The number of items per page. + PageSortQueryParam: + name: sortOrder + in: query + type: string + description: | + Parameter describing the sort. Allows specifying the sorting direction using the keywords {@code asc} and + {@code desc} after each property. For example, {@code "sort=name desc,age"} will sort by name in descending + order and age in ascending. + +responses: + Error: + description: An unexpected error has occurred + 400: + description: Bad Request + 401: + description: Unauthorized + 404: + description: Not Found + 500: + description: Internal Server Error + default: + description: Unknown Error diff --git a/test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/ParametersController.java b/test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/ParametersController.java new file mode 100644 index 0000000000..4b55f560c4 --- /dev/null +++ b/test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/ParametersController.java @@ -0,0 +1,56 @@ +package io.micronaut.openapi.test.api; + +import io.micronaut.openapi.test.model.SendDatesResponse; +import io.micronaut.openapi.test.model.SendPrimitivesResponse; +import io.micronaut.http.annotation.Controller; +import reactor.core.publisher.Mono; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.OffsetDateTime; + +@Controller +public class ParametersController implements ParametersApi { + + @Override + public Mono sendPrimitives( + String name, BigDecimal age, Float height, Boolean isPositive) { + return Mono.just(new SendPrimitivesResponse() + .name(name) + .age(age) + .height(height) + .isPositive(isPositive)); + } + + @Override + public Mono sendValidatedPrimitives( + String name, + Integer age, + BigDecimal favoriteNumber, + Double height) { + return Mono.just("Success"); + } + + @Override + public Mono sendDates( + LocalDate commitDate, OffsetDateTime commitDateTime) { + return Mono.just(new SendDatesResponse() + .commitDate(commitDate) + .commitDateTime(commitDateTime)); + } + + @Override + public Mono getIgnoredHeader() { + return Mono.just("Success"); + } + + @Override + public Mono sendIgnoredHeader(String header) { + return Mono.just("Success"); + } + + @Override + public Mono sendPageQuery(Integer page, Integer size, String sort) { + return Mono.just("(page: " + page + ", size: " + size + ", sort: " + sort + ")"); + } +} diff --git a/test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/RequestBodyController.java b/test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/RequestBodyController.java new file mode 100644 index 0000000000..084497bb9b --- /dev/null +++ b/test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/RequestBodyController.java @@ -0,0 +1,109 @@ +package io.micronaut.openapi.test.api; + +import io.micronaut.openapi.test.model.Animal; +import io.micronaut.openapi.test.model.ColorEnum; +import io.micronaut.openapi.test.model.DateModel; +import io.micronaut.openapi.test.model.ModelWithEnumList; +import io.micronaut.openapi.test.model.ModelWithInnerEnum; +import io.micronaut.openapi.test.model.ModelWithMapProperty; +import io.micronaut.openapi.test.model.ModelWithRequiredProperties; +import io.micronaut.openapi.test.model.NestedModel; +import io.micronaut.openapi.test.model.SimpleModel; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.annotation.Post; +import io.micronaut.http.multipart.CompletedFileUpload; +import jakarta.validation.Valid; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.List; + +@Controller +public class RequestBodyController implements RequestBodyApi { + + @Post(value = "/echo", consumes = "text/plain", produces = "text/plain") + public String echo(String request) { + return request; + } + + @Override + public Mono sendValidatedCollection() { + return Mono.empty(); + } + + @Override + public Mono sendSimpleModel(SimpleModel simpleModel) { + return Mono.just(simpleModel); + } + + @Override + public Mono> sendListOfSimpleModels(List simpleModels) { + return Mono.just(simpleModels); + } + + @Override + public Mono sendModelWithRequiredProperties(ModelWithRequiredProperties model) { + return Mono.just(model); + } + + @Override + public Mono sendDateModel(DateModel model) { + return Mono.just(model); + } + + @Override + public Mono sendEnum(String color) { + return Mono.just(ColorEnum.fromValue(color.replace("\"", ""))); + } + + @Override + public Mono> sendEnumList( + List<@Valid ColorEnum> availableColors) { + return Mono.just(availableColors); + } + + @Override + public Mono sendModelWithMapProperty(ModelWithMapProperty model) { + return Mono.just(model); + } + + @Override + public Mono sendNestedModel(NestedModel model) { + return Mono.just(model); + } + + @Override + public Mono sendModelWithInnerEnum(ModelWithInnerEnum model) { + return Mono.just(model); + } + + @Override + public Mono sendModelWithDiscriminator(Animal model) { + return Mono.just(model); + } + + @Override + public Mono sendBytes(byte[] bytes) { + return Mono.just(bytes); + } + + @Override + public Mono sendModelWithEnumList(ModelWithEnumList model) { + return Mono.just(model); + } + + @Override + public Mono sendFile(CompletedFileUpload file) { + return Mono.fromCallable(() -> { + InputStream inputStream = file.getInputStream(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write("name: ".getBytes()); + outputStream.write(file.getFilename().getBytes()); + outputStream.write(", content: ".getBytes()); + inputStream.transferTo(outputStream); + inputStream.close(); + return outputStream.toByteArray(); + }); + } +} diff --git a/test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/ResponseBodyController.java b/test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/ResponseBodyController.java new file mode 100644 index 0000000000..7276e6d263 --- /dev/null +++ b/test-suite-server-generator/src/main/java/io/micronaut/openapi/test/api/ResponseBodyController.java @@ -0,0 +1,76 @@ +package io.micronaut.openapi.test.api; + +import io.micronaut.http.multipart.CompletedFileUpload; +import io.micronaut.openapi.test.model.SimpleModel; +import io.micronaut.openapi.test.model.StateEnum; +import io.micronaut.http.HttpStatus; +import io.micronaut.http.annotation.Controller; +import io.micronaut.http.exceptions.HttpStatusException; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayInputStream; +import java.util.List; + +@Controller +public class ResponseBodyController implements ResponseBodyApi { + + public static SimpleModel SIMPLE_MODEL = + new SimpleModel() + .color("red") + .area(10.5f) + .numEdges(10L) + .convex(false) + .points(List.of("1,1", "2,2", "2,4")); + + public static List SIMPLE_MODELS = + List.of( + SIMPLE_MODEL, + new SimpleModel().color("red").area(10.5f).numEdges(3L), + new SimpleModel() + .color("blue") + .state(StateEnum.RUNNING) + .points(List.of("1,1", "2,2", "3,3"))); + + @Override + public Mono getSimpleModel() { + return Mono.just(SIMPLE_MODEL); + } + + @Override + public Mono> getPaginatedSimpleModel(Integer page) { + return Mono.just(SIMPLE_MODELS); + } + + @Override + public Mono getDatedSimpleModel() { + return Mono.just(SIMPLE_MODEL); + } + + @Override + public Mono getSimpleModelWithNonStandardStatus() { + return Mono.just(SIMPLE_MODEL); + } + + @Override + public Mono getDatedSimpleModelWithNonMappedHeader() { + return Mono.just(SIMPLE_MODEL); + } + + @Override + public Mono getSimpleModelWithNonMappedHeader() { + return Mono.just(SIMPLE_MODEL); + } + + @Override + public Mono getErrorResponse() { + return Mono.fromCallable(() -> { + throw new HttpStatusException(HttpStatus.NOT_FOUND, "This is the error"); + }); + } + + @Override + public Mono getFile() { + ByteArrayInputStream stream = new ByteArrayInputStream("My file content".getBytes()); + return Mono.empty(); + } +} diff --git a/test-suite-server-generator/src/test/groovy/io/micronaut/openapi/test/api/ParametersControllerSpec.groovy b/test-suite-server-generator/src/test/groovy/io/micronaut/openapi/test/api/ParametersControllerSpec.groovy new file mode 100644 index 0000000000..4c4462d7ee --- /dev/null +++ b/test-suite-server-generator/src/test/groovy/io/micronaut/openapi/test/api/ParametersControllerSpec.groovy @@ -0,0 +1,134 @@ +package io.micronaut.openapi.test.api + +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.BlockingHttpClient +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Specification + +import io.micronaut.openapi.test.model.SendPrimitivesResponse +import spock.lang.Unroll + + +@MicronautTest +class ParametersControllerSpec extends Specification { + + @Inject + EmbeddedServer server + + @Inject + @Client("/api") + HttpClient reactiveClient + + BlockingHttpClient client + + void setup() { + this.client = reactiveClient.toBlocking() + } + + void "test send primitives"() { + given: + HttpRequest request = + HttpRequest.GET("/sendPrimitives/Andrew?age=1&isPositive=true") + .header("height", "17.3") + + when: + Argument arg = Argument.of(SendPrimitivesResponse) + SendPrimitivesResponse response = client.retrieve(request, arg, Argument.of(String)) + + then: + "Andrew" == response.name + BigDecimal.valueOf(1) == response.age + Float.valueOf(17.3f) == response.height + Boolean.TRUE == response.isPositive + } + + void "test send not null primitives"() { + given: + HttpRequest request = HttpRequest.GET("/sendPrimitives/Andrew") + + when: + Argument arg = Argument.of(SendPrimitivesResponse) + client.retrieve(request, arg, Argument.of(String)) + + then: + var e = thrown(HttpClientResponseException) + + HttpStatus.BAD_REQUEST == e.getStatus() + e.getMessage().contains("not specified") + } + + void "test send nullable primitives"() { + when: + HttpRequest request = + HttpRequest.GET("/sendValidatedPrimitives").accept(MediaType.TEXT_PLAIN_TYPE) + client.retrieve(request, Argument.of(String), Argument.of(String)) + + then: + notThrown(Exception) + } + + void "test send validated primitives"() { + when: + HttpRequest request = + HttpRequest.GET("/sendValidatedPrimitives?name=Andrew&age=20&favoriteNumber=3.14&height=1.5") + client.retrieve(request, Argument.of(String)) + + then: + notThrown(Exception) + } + + @Unroll + void "test send invalid primitives: #message"() { + given: + HttpRequest request = HttpRequest.GET("/sendValidatedPrimitives?" + query) + + when: + client.retrieve(request, Argument.of(String), Argument.of(String)) + + then: + def e = thrown(HttpClientResponseException) + HttpStatus.BAD_REQUEST == e.status + e.message != null + e.message.contains(message) + + where: + query | message + "name=aa" | "name: size must be between 3 and 2147483647" + "name=123456" | 'name: must match \\"[a-zA-Z]+\\"' + "age=-2" | "age: must be greater than or equal to 10" + "age=500" | "age: must be less than or equal to 200" + "favoriteNumber=-500" | "favoriteNumber: must be greater than or equal to -100.5" + "favoriteNumber=100.6" | "favoriteNumber: must be less than or equal to 100.5" + "height=0" | "height: must be greater than or equal to 0.1" + "height=100" | "height: must be less than or equal to 3" + } + + void "test send dates"() { + when: + HttpRequest request = + HttpRequest.GET("/sendDates?commitDate=2022-03-04&commitDateTime=2022-03-04T12:00:01.321Z") + String response = client.retrieve(request, Argument.of(String), Argument.of(String)) + + then: + '{"commitDate":"2022-03-04","commitDateTime":"2022-03-04T12:00:01.321Z"}' == response + } + + void "test send page query"() { + when: + HttpRequest request = + HttpRequest.GET("/sendPageQuery?page=2&size=20&sort=my-property%20desc,my-property-2") + String response = client.retrieve(request, Argument.of(String), Argument.of(String)) + + then: + "(page: 2, size: 20, sort: my-property desc,my-property-2)" == response + } + +} diff --git a/test-suite-server-generator/src/test/groovy/io/micronaut/openapi/test/api/RequestBodyControllerSpec.groovy b/test-suite-server-generator/src/test/groovy/io/micronaut/openapi/test/api/RequestBodyControllerSpec.groovy new file mode 100644 index 0000000000..716dc86f83 --- /dev/null +++ b/test-suite-server-generator/src/test/groovy/io/micronaut/openapi/test/api/RequestBodyControllerSpec.groovy @@ -0,0 +1,362 @@ +package io.micronaut.openapi.test.api + +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.BlockingHttpClient +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.http.client.multipart.MultipartBody +import io.micronaut.openapi.test.model.Animal +import io.micronaut.openapi.test.model.Bird +import io.micronaut.openapi.test.model.ColorEnum +import io.micronaut.openapi.test.model.DateModel +import io.micronaut.openapi.test.model.Mammal +import io.micronaut.openapi.test.model.ModelWithEnumList +import io.micronaut.openapi.test.model.ModelWithInnerEnum +import io.micronaut.openapi.test.model.ModelWithMapProperty +import io.micronaut.openapi.test.model.ModelWithRequiredProperties +import io.micronaut.openapi.test.model.NestedModel +import io.micronaut.openapi.test.model.Reptile +import io.micronaut.openapi.test.model.SimpleModel +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Ignore +import spock.lang.Specification + +import java.time.LocalDate +import java.time.OffsetDateTime + +@MicronautTest +class RequestBodyControllerSpec extends Specification { + + @Inject + EmbeddedServer server + + @Inject + @Client("/api") + HttpClient reactiveClient + + BlockingHttpClient client + + void setup() { + this.client = reactiveClient.toBlocking() + } + + @Ignore("Not yet supported") + void "test send validated collection"() { + given: + HttpRequest request = + HttpRequest.POST("/sendValidatedCollection", List.of(List.of("a", "hello", "123"))) + .contentType(MediaType.APPLICATION_JSON_TYPE) + + when: + client.retrieve(request, Argument.of(String), Argument.of(String)) + + then: + def e = thrown(HttpClientResponseException) + + HttpStatus.BAD_REQUEST == e.status + e.message.contains("collection[0][0]: size must be between 3 and 2147483647") + } + + void "test send simple model"() { + given: + SimpleModel model = + new SimpleModel().color("red").numEdges(10L).area(11.5f) + .convex(true).points(List.of("1,1", "2,2", "1,2")) + HttpRequest request = HttpRequest.POST("/sendSimpleModel", model) + .contentType(MediaType.APPLICATION_JSON_TYPE) + + when: + Argument arg = Argument.of(SimpleModel) + SimpleModel response = client.retrieve(request, arg, Argument.of(String)) + + then: + model == response + } + + void "test send validated simple model: #message"() { + given: + HttpRequest request = HttpRequest.POST("/sendSimpleModel", model) + + when: + Argument arg = Argument.of(SimpleModel) + client.retrieve(request, arg, Argument.of(String)) + + then: + def e = thrown(HttpClientResponseException) + HttpStatus.BAD_REQUEST == e.status + e.message != null + e.message.contains(message) + + where: + model | message + new SimpleModel().color("1") | "simpleModel.color: size must be between 2 and 2147483647" + new SimpleModel().numEdges(0L) | "simpleModel.numEdges: must be greater than or equal to 1" + new SimpleModel().area(0f) | "simpleModel.area: must be greater than or equal to 0" + new SimpleModel().points(["0,0"]) | "simpleModel.points: size must be between 3 and 2147483647" + } + + void "test send list of simple models"() { + given: + List models = [ + new SimpleModel().color("red").numEdges(10L).area(11.5f) + .convex(true).points(["1,0", "0,0", "0,1", "2,2"]), + new SimpleModel().color("azure").numEdges(2L).area(1.45f) + .convex(true).points(["1,1", "2,2"]), + new SimpleModel().numEdges(11L).convex(false) + ] + + when: + HttpRequest request = HttpRequest.POST("/sendListOfSimpleModels", models) + .contentType(MediaType.APPLICATION_JSON_TYPE) + Argument> arg = Argument.listOf(SimpleModel) + List response = client.retrieve(request, arg, Argument.of(String)) + + then: + models == response + } + + void "test send models with required properties request"() { + given: + ModelWithRequiredProperties model = new ModelWithRequiredProperties("Walaby", 1.2f) + HttpRequest request = HttpRequest.POST("/sendModelWithRequiredProperties", model) + + when: + Argument arg = Argument.of(ModelWithRequiredProperties) + def response = client.retrieve(request, arg, Argument.of(String)) + + then: + model == response + } + + void "test send model with missing required properties"() { + given: + HttpRequest request = HttpRequest.POST("/sendModelWithRequiredProperties", model) + + when: + Argument arg = Argument.of(ModelWithRequiredProperties) + client.retrieve(request, arg, Argument.of(String)) + + then: + def e = thrown(HttpClientResponseException) + HttpStatus.BAD_REQUEST == e.status + + where: + model | _ + new ModelWithRequiredProperties(null, 1.3f) + .numRepresentatives(100000).description("A hopping animal") | _ + new ModelWithRequiredProperties("Walaby", null) + .numRepresentatives(100000).description("A hopping animal") | _ + } + + void "test send date model"() { + given: + DateModel dateModel = new DateModel() + .commitDate(LocalDate.parse("2022-01-03")) + .commitDateTime(OffsetDateTime.parse("1999-01-01T00:01:10.456+01:00")) + HttpRequest request = HttpRequest.POST("/sendDateModel", dateModel) + + when: + String response = client.retrieve(request, Argument.of(String), Argument.of(String)) + + then: + '{"commitDate":"2022-01-03","commitDateTime":"1998-12-31T23:01:10.456Z"}' == response + } + + void "test send nested model"() { + given: + SimpleModel simpleModel = new SimpleModel() + .color("red").numEdges(10L).area(11.5f) + .convex(true).points(List.of("1,1", "2,2", "1,2")) + NestedModel model = new NestedModel().simpleModel(simpleModel) + HttpRequest request = HttpRequest.POST("/sendNestedModel", model) + .contentType(MediaType.APPLICATION_JSON_TYPE) + + when: + Argument arg = Argument.of(NestedModel) + NestedModel response = client.retrieve(request, arg, Argument.of(String)) + + then: + model == response + } + + void "test send model with inner enum"() { + given: + ModelWithInnerEnum model = new ModelWithInnerEnum() + .speciesName("Short-eared rock wallaby") + .numRepresentatives(40000L) + .mammalOrder(ModelWithInnerEnum.MammalOrderEnum.MARSUPIAL) + HttpRequest request = HttpRequest.POST("/sendModelWithInnerEnum", model) + .contentType(MediaType.APPLICATION_JSON_TYPE) + + when: + String response = client.retrieve(request, Argument.of(String), Argument.of(String)) + + then: + '{"species-name":"Short-eared rock wallaby","num-representatives":40000,"mammal-order":"marsupial"}' == response + } + + void "test send model with enum list"() { + given: + List colors = [ColorEnum.DARK_GREEN, ColorEnum.LIGHT_BLUE] + ModelWithEnumList model = new ModelWithEnumList().favoriteColors(colors) + HttpRequest request = HttpRequest.POST("/sendModelWithEnumList", model) + .contentType(MediaType.APPLICATION_JSON_TYPE) + + when: + String response = client.retrieve(request, Argument.of(String), Argument.of(String)) + + then: + '{"favoriteColors":["dark-green","light-blue"]}' == response + } + + void "test send enum #color"() { + given: + HttpRequest request = HttpRequest.POST("/sendEnum", color) + + when: + ColorEnum response = client.retrieve(request, Argument.of(ColorEnum)) + + then: + color == response + + when: + String stringResponse = client.retrieve(request, Argument.of(String)) + + then: + '"' + color.value + '"' == stringResponse + + where: + color | _ + ColorEnum.BLUE | _ + ColorEnum.RED | _ + ColorEnum.GREEN | _ + ColorEnum.LIGHT_BLUE | _ + ColorEnum.DARK_GREEN | _ + } + + void "test send enum list"() { + given: + List colors = [ColorEnum.GREEN, ColorEnum.RED] + HttpRequest request = HttpRequest.POST("/sendEnumList", colors) + + when: + String response = client.retrieve(request, Argument.of(String)) + + then: + '["green","red"]' == response + } + + void "test send model with simple map property"() { + given: + def model = new ModelWithMapProperty().map(["color": "pink", "weight": "30.4"]) + HttpRequest request = HttpRequest.POST("/sendModelWithMapProperty", model) + + when: + ModelWithMapProperty response = client.retrieve(request, ModelWithMapProperty) + + then: + model == response + } + + void "test send model with deep map property"() { + given: + Map> map = [ + "characteristics": ["color": "pink"], + "issues": ["isWorking": "false", "hasCracks": "true"] + ] + ModelWithMapProperty model = new ModelWithMapProperty().deepMap(map) + HttpRequest request = HttpRequest.POST("/sendModelWithMapProperty", model) + + when: + ModelWithMapProperty response = client.retrieve(request, ModelWithMapProperty) + + then: + model == response + } + + void "test send model with deep map model property"() { + given: + def map = [ + "polygons": [ + "triangle": new SimpleModel().numEdges(3L), + "smallRectangle": new SimpleModel().numEdges(4L).area(1f) + ] + ] + ModelWithMapProperty model = new ModelWithMapProperty().deepObjectMap(map) + HttpRequest request = HttpRequest.POST("/sendModelWithMapProperty", model) + + when: + ModelWithMapProperty response = client.retrieve(request, ModelWithMapProperty) + + then: + model == response + } + + private static String BIRD_DISCRIMINATOR = "ave" + private static String MAMMAL_DISCRIMINATOR = "mammalia" + private static String REPTILE_DISCRIMINATOR = "reptilia" + + @Ignore("Requires fixing") + void "test send model with discriminator child: #discriminatorName"() { + given: + HttpRequest request = HttpRequest.PUT("/sendModelWithDiscriminator", model) + + when: + Argument arg = Argument.of(Animal) + Animal response = client.retrieve(request, arg, Argument.of(String)) + + then: + model == response + + when: + String stringResponse = client.retrieve(request, Argument.of(String)) + + then: + stringResponse.contains('"class":"' + discriminatorName + '"') + + where: + discriminatorName | model + BIRD_DISCRIMINATOR | new Bird().beakLength(BigDecimal.valueOf(12, 1)) + .featherDescription("Large blue and white feathers").numWings(2).color(ColorEnum.BLUE) + MAMMAL_DISCRIMINATOR | new Mammal().weight(20.5f) + .description("A typical Canadian beaver").color(ColorEnum.BLUE) + REPTILE_DISCRIMINATOR | new Reptile().fangs(true).fangDescription("A pair of venomous fangs") + .numLegs(0).color(ColorEnum.BLUE) + } + + void "test send bytes"() { + given: + String content = "my small bytes content" + HttpRequest request = HttpRequest.PUT("/sendBytes", content.getBytes()) + .contentType(MediaType.APPLICATION_OCTET_STREAM_TYPE) + + when: + Argument arg = Argument.of(byte[]) + byte[] response = client.retrieve(request, arg, Argument.of(String)) + + then: + content == new String(response) + } + + void "test send file"() { + given: + String content = "my favorite file content" + MultipartBody body = MultipartBody.builder() + .addPart("file", "my-file.txt", content.getBytes()).build() + HttpRequest request = HttpRequest.PUT("/sendFile", body).contentType(MediaType.MULTIPART_FORM_DATA_TYPE) + + when: + Argument arg = Argument.of(byte[]) + byte[] response = client.retrieve(request, arg, Argument.of(String)) + + then: + 'name: my-file.txt, content: my favorite file content' == new String(response) + } + +} diff --git a/test-suite-server-generator/src/test/groovy/io/micronaut/openapi/test/api/ResponseBodyControllerSpec.groovy b/test-suite-server-generator/src/test/groovy/io/micronaut/openapi/test/api/ResponseBodyControllerSpec.groovy new file mode 100644 index 0000000000..fc58fa786c --- /dev/null +++ b/test-suite-server-generator/src/test/groovy/io/micronaut/openapi/test/api/ResponseBodyControllerSpec.groovy @@ -0,0 +1,111 @@ +package io.micronaut.openapi.test.api + +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.client.BlockingHttpClient +import io.micronaut.http.client.HttpClient +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.openapi.test.model.SimpleModel +import io.micronaut.runtime.server.EmbeddedServer +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import jakarta.inject.Inject +import spock.lang.Ignore +import spock.lang.Specification + +@MicronautTest +class ResponseBodyControllerSpec extends Specification { + + @Inject + EmbeddedServer server + + @Inject + @Client("/api") + HttpClient reactiveClient + + BlockingHttpClient client + + void setup() { + this.client = reactiveClient.toBlocking() + } + + void "test get simple model"() { + when: + HttpResponse response = client.exchange("/getSimpleModel", SimpleModel ) + + then: + HttpStatus.OK == response.status + ResponseBodyController.SIMPLE_MODEL == response.body() + } + + // TODO implement the behavior and test + void "test get paginated simple model"() { + when: + HttpResponse> response = + client.exchange(HttpRequest.GET("/getPaginatedSimpleModel"), Argument.listOf(SimpleModel)) + + then: + HttpStatus.OK == response.status + ResponseBodyController.SIMPLE_MODELS == response.body() + } + + // TODO implement the behavior and test + void "test get dated simple model"() { + HttpResponse response = + client.exchange(HttpRequest.GET("/getDatedSimpleModel"), Argument.of(SimpleModel)) + + HttpStatus.ACCEPTED == response.status() + ResponseBodyController.SIMPLE_MODEL == response.body() + } + + void "test get simple model with non standard status"() { + HttpResponse response = client.exchange("/getSimpleModelWithNonStandardStatus", SimpleModel) + + HttpStatus.ACCEPTED == response.status() + ResponseBodyController.SIMPLE_MODEL == response.body() + } + + // TODO implement the behavior and test + void "test get dated simple model with non standard headers"() { + HttpResponse response = client.exchange( + HttpRequest.GET("/getTaggedSimpleModelWithNonStandardHeaders"), Argument.of(SimpleModel)) + + HttpStatus.OK == response.status + ResponseBodyController.SIMPLE_MODEL == response.body() + } + + void "test get simple model with non mapped header"() { + HttpResponse response = client.exchange("/getSimpleModelWithNonStandardHeader", SimpleModel) + + HttpStatus.OK == response.status + "simple model" == response.headers.get("custom-header") + ResponseBodyController.SIMPLE_MODEL == response.body() + } + + void "test get error response"() { + when: + client.retrieve(HttpRequest.GET("/getErrorResponse"), Argument.of(String), Argument.of(String)) + + then: + def e = thrown(HttpClientResponseException) + HttpStatus.NOT_FOUND == e.status + } + + @Ignore("Requires fixing") + void "test get file"() { + given: + HttpRequest request = HttpRequest.GET("/getFile").contentType(MediaType.MULTIPART_FORM_DATA_TYPE) + + when: + Argument arg = Argument.of(byte[]) + byte[] response = client.retrieve(request, arg, Argument.of(String)) + + then: + def expectedContent = "My file content" + expectedContent == new String(response) + } + +} diff --git a/test-suite-server-generator/src/test/resources/application-test.properties b/test-suite-server-generator/src/test/resources/application-test.properties new file mode 100644 index 0000000000..b393408a81 --- /dev/null +++ b/test-suite-server-generator/src/test/resources/application-test.properties @@ -0,0 +1 @@ +micronaut.server.context-path=/api