diff --git a/customer-service-client/pom.xml b/customer-service-client/pom.xml index b3a57ae..451ded1 100644 --- a/customer-service-client/pom.xml +++ b/customer-service-client/pom.xml @@ -6,7 +6,7 @@ io.github.bsayli customer-service-client - 0.5.0 + 0.6.0 customer-service-client Generated client (RestClient) using generics-aware OpenAPI templates jar @@ -20,16 +20,20 @@ 7.15.0 3.13.0 3.6.0 + 3.3.1 + 3.8.1 3.1.1 3.0.0 5.1.0 5.5 5.13.4 + + ${project.build.directory}/upstream-templates + ${project.build.directory}/effective-templates - org.springframework.boot spring-boot-starter-web @@ -80,18 +84,90 @@ - src/main/resources - **/*.yaml openapi-templates/** + + org.apache.maven.plugins + maven-dependency-plugin + ${maven.dependency.plugin.version} + + + unpack-openapi-upstream-templates + generate-sources + + unpack + + + + + org.openapitools + openapi-generator + ${openapi.generator.version} + jar + true + templates/Java/** + ${openapi.templates.upstream} + + + + + + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven.resources.plugin.version} + + + copy-upstream-to-effective + generate-sources + + copy-resources + + + ${openapi.templates.effective} + + + ${openapi.templates.upstream}/templates + + Java/** + + + + + + + + overlay-local-templates + generate-sources + + copy-resources + + + ${openapi.templates.effective}/Java + true + + + src/main/resources/openapi-templates + false + + **/*.mustache + + + + + + + org.openapitools @@ -100,6 +176,7 @@ generate-client + generate-sources generate @@ -113,8 +190,7 @@ io.github.bsayli.openapi.client.generated.dto io.github.bsayli.openapi.client.generated.invoker - ${project.basedir}/src/main/resources/openapi-templates - + ${openapi.templates.effective}/Java true false @@ -131,7 +207,7 @@ java8 true false - src/gen/java/main + src/gen/java @@ -151,7 +227,7 @@ - ${project.build.directory}/generated-sources/openapi/src/gen/java/main + ${project.build.directory}/generated-sources/openapi/src/gen/java diff --git a/customer-service-client/src/main/resources/customer-api-docs.yaml b/customer-service-client/src/main/resources/customer-api-docs.yaml index a773c19..a45aa19 100644 --- a/customer-service-client/src/main/resources/customer-api-docs.yaml +++ b/customer-service-client/src/main/resources/customer-api-docs.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 info: title: Customer Service API description: "Demo: type-safe generic API responses with OpenAPI" - version: 0.3.0 + version: 0.6.0 servers: - url: http://localhost:8084/customer-service description: Local service URL diff --git a/customer-service-client/src/main/resources/openapi-templates/modelEnum.mustache b/customer-service-client/src/main/resources/openapi-templates/modelEnum.mustache deleted file mode 100644 index 90cd7f8..0000000 --- a/customer-service-client/src/main/resources/openapi-templates/modelEnum.mustache +++ /dev/null @@ -1,120 +0,0 @@ -{{#jackson}} -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; -{{/jackson}} -{{#gson}} -import java.io.IOException; -import com.google.gson.TypeAdapter; -import com.google.gson.annotations.JsonAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; -{{/gson}} -{{#isUri}} -import java.net.URI; -{{/isUri}} - -/** - * {{description}}{{^description}}Gets or Sets {{{name}}}{{/description}} - */ -{{#gson}} -@JsonAdapter({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.Adapter.class) -{{/gson}} -{{#jsonb}} -@JsonbTypeSerializer({{datatypeWithEnum}}.Serializer.class) -@JsonbTypeDeserializer({{datatypeWithEnum}}.Deserializer.class) -{{/jsonb}} -{{>additionalEnumTypeAnnotations}}public enum {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}{{#vendorExtensions.x-implements}}{{#-first}} implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} { - {{#allowableValues}}{{#enumVars}} - {{#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}}{{{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}}{{{classname}}}{{/datatypeWithEnum}} fromValue({{{dataType}}} value) { - for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{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}}{{#enumUnknownDefaultCase}}{{#allowableValues}}{{#enumVars}}{{#-last}}return {{{name}}};{{/-last}}{{/enumVars}}{{/allowableValues}}{{/enumUnknownDefaultCase}}{{^enumUnknownDefaultCase}}throw new IllegalArgumentException("Unexpected value '" + value + "'");{{/enumUnknownDefaultCase}}{{/isNullable}} - } -{{#gson}} - - public static class Adapter extends TypeAdapter<{{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}> { - @Override - public void write(final JsonWriter jsonWriter, final {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} enumeration) throws IOException { - jsonWriter.value(enumeration.getValue(){{#isUri}}.toASCIIString(){{/isUri}}); - } - - @Override - public {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} read(final JsonReader jsonReader) throws IOException { - {{^isNumber}}{{{dataType}}}{{/isNumber}}{{#isNumber}}String{{/isNumber}} value = {{#isFloat}}(float){{/isFloat}}{{#isUri}}URI.create({{/isUri}}jsonReader.{{#isNumber}}nextString(){{/isNumber}}{{#isInteger}}nextInt(){{/isInteger}}{{#isUri}}nextString()){{/isUri}}{{^isNumber}}{{^isInteger}}{{^isUri}}{{#isFloat}}nextDouble{{/isFloat}}{{^isFloat}}next{{{dataType}}}{{/isFloat}}(){{/isUri}}{{/isInteger}}{{/isNumber}}; - return {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.fromValue({{#isNumber}}new BigDecimal({{/isNumber}}value{{#isNumber}}){{/isNumber}}); - } - } -{{/gson}} -{{#jsonb}} - - public static final class Deserializer implements JsonbDeserializer<{{datatypeWithEnum}}> { - @Override - public {{datatypeWithEnum}} deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) { - for ({{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}} b : {{{datatypeWithEnum}}}{{^datatypeWithEnum}}{{{classname}}}{{/datatypeWithEnum}}.values()) { - if (String.valueOf(b.value).equals(parser.getString())) { - return b; - } - } - {{#useNullForUnknownEnumValue}}return null;{{/useNullForUnknownEnumValue}}{{^useNullForUnknownEnumValue}}throw new IllegalArgumentException("Unexpected value '" + parser.getString() + "'");{{/useNullForUnknownEnumValue}} - } - } - - public static final class Serializer implements JsonbSerializer<{{datatypeWithEnum}}> { - @Override - public void serialize({{datatypeWithEnum}} obj, JsonGenerator generator, SerializationContext ctx) { - generator.write(obj.value); - } - } -{{/jsonb}} -{{#supportUrlQuery}} - - /** - * Convert the instance into URL query string. - * - * @param prefix prefix of the query string - * @return URL query string - */ - public String toUrlQueryString(String prefix) { - if (prefix == null) { - prefix = ""; - } - - return String.format("%s=%s", prefix, this.toString()); - } -{{/supportUrlQuery}} -} diff --git a/customer-service-client/src/main/resources/openapi-templates/oneof_interface.mustache b/customer-service-client/src/main/resources/openapi-templates/oneof_interface.mustache deleted file mode 100644 index eadcb26..0000000 --- a/customer-service-client/src/main/resources/openapi-templates/oneof_interface.mustache +++ /dev/null @@ -1,6 +0,0 @@ -{{>additionalOneOfTypeAnnotations}}{{>generatedAnnotation}}{{>typeInfoAnnotation}}{{>xmlAnnotation}} -public {{>sealed}}interface {{classname}} {{#vendorExtensions.x-implements}}{{#-first}}extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}}{{>permits}}{ - {{#discriminator}} - public {{propertyType}} {{propertyGetter}}(); - {{/discriminator}} -} diff --git a/customer-service-client/src/main/resources/openapi-templates/pojo.mustache b/customer-service-client/src/main/resources/openapi-templates/pojo.mustache deleted file mode 100644 index 25974f2..0000000 --- a/customer-service-client/src/main/resources/openapi-templates/pojo.mustache +++ /dev/null @@ -1,624 +0,0 @@ -/** - * {{description}}{{^description}}{{classname}}{{/description}}{{#isDeprecated}} - * @deprecated{{/isDeprecated}} - */{{#isDeprecated}} -@Deprecated{{/isDeprecated}} -{{#swagger1AnnotationLibrary}} -{{#description}} -@ApiModel(description = "{{{.}}}") -{{/description}} -{{/swagger1AnnotationLibrary}} -{{#swagger2AnnotationLibrary}} -{{#description}} -@Schema(description = "{{{.}}}") -{{/description}} -{{/swagger2AnnotationLibrary}} -{{#jackson}} -@JsonPropertyOrder({ -{{#vars}} - {{classname}}.JSON_PROPERTY_{{nameInSnakeCase}}{{^-last}},{{/-last}} -{{/vars}} -}) -{{#isClassnameSanitized}} -{{^hasDiscriminatorWithNonEmptyMapping}} -@JsonTypeName("{{name}}") -{{/hasDiscriminatorWithNonEmptyMapping}} -{{/isClassnameSanitized}} -{{/jackson}} -{{>additionalModelTypeAnnotations}}{{>generatedAnnotation}}{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{>xmlAnnotation}} -{{#vendorExtensions.x-class-extra-annotation}} -{{{vendorExtensions.x-class-extra-annotation}}} -{{/vendorExtensions.x-class-extra-annotation}} -public class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{#-last}} {{/-last}}{{/vendorExtensions.x-implements}}{ -{{#serializableModel}} - private static final long serialVersionUID = 1L; - -{{/serializableModel}} - {{#vars}} - {{#isEnum}} - {{^isContainer}} -{{>modelInnerEnum}} - - {{/isContainer}} - {{#isContainer}} - {{#mostInnerItems}} -{{>modelInnerEnum}} - - {{/mostInnerItems}} - {{/isContainer}} - {{/isEnum}} - {{#gson}} - public static final String SERIALIZED_NAME_{{nameInSnakeCase}} = "{{baseName}}"; - {{/gson}} - {{#jackson}} - public static final String JSON_PROPERTY_{{nameInSnakeCase}} = "{{baseName}}"; - {{/jackson}} - {{#withXml}} - @Xml{{#isXmlAttribute}}Attribute{{/isXmlAttribute}}{{^isXmlAttribute}}Element{{/isXmlAttribute}}(name = "{{items.xmlName}}{{^items.xmlName}}{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}{{/items.xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}}) - {{#isXmlWrapped}} - @XmlElementWrapper(name = "{{xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}"{{#xmlNamespace}}, namespace = "{{.}}"{{/xmlNamespace}}) - {{/isXmlWrapped}} - {{^isXmlAttribute}} - {{#isDateTime}} - @XmlJavaTypeAdapter(OffsetDateTimeXmlAdapter.class) - {{/isDateTime}} - {{/isXmlAttribute}} - {{/withXml}} - {{#gson}} - @SerializedName(SERIALIZED_NAME_{{nameInSnakeCase}}) - {{/gson}} - {{^isDiscriminator}} - {{>nullable_var_annotations}}{{! prevent indent}} - {{/isDiscriminator}} - {{#isDiscriminator}} - // The discriminator does not have Nullability-annotation since it is added during serialization by the @JsonTypeName annotation - {{/isDiscriminator}} - {{#vendorExtensions.x-field-extra-annotation}} - {{{vendorExtensions.x-field-extra-annotation}}} - {{/vendorExtensions.x-field-extra-annotation}} - {{#vendorExtensions.x-is-jackson-optional-nullable}} - {{#isContainer}} - {{#hasChildren}}protected{{/hasChildren}}{{^hasChildren}}private{{/hasChildren}} JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>undefined(); - {{/isContainer}} - {{^isContainer}} - {{#hasChildren}}protected{{/hasChildren}}{{^hasChildren}}private{{/hasChildren}} JsonNullable<{{{datatypeWithEnum}}}> {{name}} = JsonNullable.<{{{datatypeWithEnum}}}>{{#defaultValue}}of({{{.}}}){{/defaultValue}}{{^defaultValue}}undefined(){{/defaultValue}}; - {{/isContainer}} - {{/vendorExtensions.x-is-jackson-optional-nullable}} - {{^vendorExtensions.x-is-jackson-optional-nullable}} - {{#isContainer}} - {{#hasChildren}}protected{{/hasChildren}}{{^hasChildren}}private{{/hasChildren}} {{{datatypeWithEnum}}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; - {{/isContainer}} - {{^isContainer}} - {{#hasChildren}}protected{{/hasChildren}}{{^hasChildren}}private{{/hasChildren}} {{{datatypeWithEnum}}} {{name}}{{#defaultValue}} = {{{.}}}{{/defaultValue}}; - {{/isContainer}} - {{/vendorExtensions.x-is-jackson-optional-nullable}} - - {{/vars}} - public {{classname}}() { - {{#parent}} - {{#parcelableModel}} - super();{{/parcelableModel}} - {{/parent}} - {{#gson}} - {{#discriminator}} - {{#discriminator.isEnum}} - this.{{{discriminatorName}}} = this.getClass().getSimpleName(); - {{/discriminator.isEnum}} - {{/discriminator}} - {{/gson}} - } - {{#vendorExtensions.x-has-readonly-properties}} - {{^withXml}} - /** - * Constructor with only readonly parameters{{#generateConstructorWithAllArgs}}{{^vendorExtensions.x-java-all-args-constructor}} and all parameters{{/vendorExtensions.x-java-all-args-constructor}}{{/generateConstructorWithAllArgs}} - */ - {{#jsonb}}@JsonbCreator{{/jsonb}}{{#jackson}}@JsonCreator{{/jackson}} - public {{classname}}( - {{#readOnlyVars}} - {{#jsonb}}@JsonbProperty(value = "{{baseName}}"{{^required}}, nullable = true{{/required}}){{/jsonb}}{{#jackson}}@JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}){{/jackson}} {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}} - {{/readOnlyVars}} - ) { - this(); - {{#readOnlyVars}} - this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}}; - {{/readOnlyVars}} - } - {{/withXml}} - {{/vendorExtensions.x-has-readonly-properties}} -{{#vendorExtensions.x-java-all-args-constructor}} - - /** - * Constructor with all args parameters - */ - public {{classname}}({{#vendorExtensions.x-java-all-args-constructor-vars}}{{#jsonb}}@JsonbProperty(value = "{{baseName}}"{{^required}}, nullable = true{{/required}}){{/jsonb}}{{#jackson}}@JsonProperty(JSON_PROPERTY_{{nameInSnakeCase}}){{/jackson}} {{{datatypeWithEnum}}} {{name}}{{^-last}}, {{/-last}}{{/vendorExtensions.x-java-all-args-constructor-vars}}) { -{{#parent}} - super({{#parentVars}}{{name}}{{^-last}}, {{/-last}}{{/parentVars}}); -{{/parent}} - {{#vars}} - this.{{name}} = {{#vendorExtensions.x-is-jackson-optional-nullable}}{{name}} == null ? JsonNullable.<{{{datatypeWithEnum}}}>undefined() : JsonNullable.of({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{name}}{{/vendorExtensions.x-is-jackson-optional-nullable}}; -{{/vars}} - } -{{/vendorExtensions.x-java-all-args-constructor}} - -{{#vars}} - {{^isReadOnly}} - public {{classname}} {{name}}({{>nullable_var_annotations}} {{{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}} - return this; - } - {{#isArray}} - - public {{classname}} add{{nameInPascalCase}}Item({{{items.datatypeWithEnum}}} {{name}}Item) { - {{#vendorExtensions.x-is-jackson-optional-nullable}} - if (this.{{name}} == null || !this.{{name}}.isPresent()) { - this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}); - } - try { - this.{{name}}.get().add({{name}}Item); - } catch (java.util.NoSuchElementException e) { - // this can never happen, as we make sure above that the value is present - } - return this; - {{/vendorExtensions.x-is-jackson-optional-nullable}} - {{^vendorExtensions.x-is-jackson-optional-nullable}} - if (this.{{name}} == null) { - this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new {{#uniqueItems}}LinkedHashSet{{/uniqueItems}}{{^uniqueItems}}ArrayList{{/uniqueItems}}<>(){{/defaultValue}}; - } - this.{{name}}.add({{name}}Item); - return this; - {{/vendorExtensions.x-is-jackson-optional-nullable}} - } - {{/isArray}} - {{#isMap}} - - public {{classname}} put{{nameInPascalCase}}Item(String key, {{{items.datatypeWithEnum}}} {{name}}Item) { - {{#vendorExtensions.x-is-jackson-optional-nullable}} - if (this.{{name}} == null || !this.{{name}}.isPresent()) { - this.{{name}} = JsonNullable.<{{{datatypeWithEnum}}}>of({{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}); - } - try { - this.{{name}}.get().put(key, {{name}}Item); - } catch (java.util.NoSuchElementException e) { - // this can never happen, as we make sure above that the value is present - } - return this; - {{/vendorExtensions.x-is-jackson-optional-nullable}} - {{^vendorExtensions.x-is-jackson-optional-nullable}} - {{^required}} - if (this.{{name}} == null) { - this.{{name}} = {{{defaultValue}}}{{^defaultValue}}new HashMap<>(){{/defaultValue}}; - } - {{/required}} - this.{{name}}.put(key, {{name}}Item); - return this; - {{/vendorExtensions.x-is-jackson-optional-nullable}} - } - {{/isMap}} - - {{/isReadOnly}} - /** - {{#description}} - * {{.}} - {{/description}} - {{^description}} - * Get {{name}} - {{/description}} - {{#minimum}} - * minimum: {{.}} - {{/minimum}} - {{#maximum}} - * maximum: {{.}} - {{/maximum}} - * @return {{name}} - {{#deprecated}} - * @deprecated - {{/deprecated}} - */ -{{#deprecated}} - @Deprecated -{{/deprecated}} - {{>nullable_var_annotations}}{{! prevent indent}} -{{#jsonb}} - @JsonbProperty("{{baseName}}") -{{/jsonb}} -{{#useBeanValidation}} -{{>beanValidation}} - -{{/useBeanValidation}} -{{#swagger1AnnotationLibrary}} - @ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}value = "{{{description}}}") -{{/swagger1AnnotationLibrary}} -{{#swagger2AnnotationLibrary}} - @Schema({{#example}}example = "{{{.}}}", {{/example}}requiredMode = {{#required}}Schema.RequiredMode.REQUIRED{{/required}}{{^required}}Schema.RequiredMode.NOT_REQUIRED{{/required}}, description = "{{{description}}}") -{{/swagger2AnnotationLibrary}} -{{#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}}{{> jackson_annotations}}{{/jackson}}{{/vendorExtensions.x-is-jackson-optional-nullable}} - 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}} - } - - {{#vendorExtensions.x-is-jackson-optional-nullable}} -{{> jackson_annotations}} - - public JsonNullable<{{{datatypeWithEnum}}}> {{getter}}_JsonNullable() { - return {{name}}; - } - {{/vendorExtensions.x-is-jackson-optional-nullable}}{{#vendorExtensions.x-is-jackson-optional-nullable}} - @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}} -{{#vendorExtensions.x-setter-extra-annotation}} {{{vendorExtensions.x-setter-extra-annotation}}} -{{/vendorExtensions.x-setter-extra-annotation}}{{#jackson}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{> jackson_annotations}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{/jackson}} public void {{setter}}({{>nullable_var_annotations}} {{{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}} - {{#parent}} - {{#readWriteVars}} - {{#isOverridden}} - @Override - public {{classname}} {{name}}({{>nullable_var_annotations}} {{{datatypeWithEnum}}} {{name}}) { - {{#vendorExtensions.x-is-jackson-optional-nullable}} - this.{{setter}}(JsonNullable.<{{{datatypeWithEnum}}}>of({{name}})); - {{/vendorExtensions.x-is-jackson-optional-nullable}} - {{^vendorExtensions.x-is-jackson-optional-nullable}} - this.{{setter}}({{name}}); - {{/vendorExtensions.x-is-jackson-optional-nullable}} - return this; - } - - {{/isOverridden}} - {{/readWriteVars}} - {{/parent}} - @Override - public boolean equals(Object o) { - {{#useReflectionEqualsHashCode}} - return EqualsBuilder.reflectionEquals(this, o, false, null, true); - {{/useReflectionEqualsHashCode}} - {{^useReflectionEqualsHashCode}} - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - }{{#hasVars}} - {{classname}} {{classVarName}} = ({{classname}}) o; - return {{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}equalsNullable(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{#isByteArray}}Arrays{{/isByteArray}}{{^isByteArray}}Objects{{/isByteArray}}.equals(this.{{name}}, {{classVarName}}.{{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}} && - {{/-last}}{{/vars}}{{#parent}} && - super.equals(o){{/parent}};{{/hasVars}}{{^hasVars}} - return {{#parent}}super.equals(o){{/parent}}{{^parent}}true{{/parent}};{{/hasVars}} - {{/useReflectionEqualsHashCode}} - }{{#vendorExtensions.x-jackson-optional-nullable-helpers}} - - private static boolean equalsNullable(JsonNullable a, JsonNullable b) { - return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get())); - }{{/vendorExtensions.x-jackson-optional-nullable-helpers}} - - @Override - public int hashCode() { - {{#useReflectionEqualsHashCode}} - return HashCodeBuilder.reflectionHashCode(this); - {{/useReflectionEqualsHashCode}} - {{^useReflectionEqualsHashCode}} - return Objects.hash({{#vars}}{{#vendorExtensions.x-is-jackson-optional-nullable}}hashCodeNullable({{name}}){{/vendorExtensions.x-is-jackson-optional-nullable}}{{^vendorExtensions.x-is-jackson-optional-nullable}}{{^isByteArray}}{{name}}{{/isByteArray}}{{#isByteArray}}Arrays.hashCode({{name}}){{/isByteArray}}{{/vendorExtensions.x-is-jackson-optional-nullable}}{{^-last}}, {{/-last}}{{/vars}}{{#parent}}{{#hasVars}}, {{/hasVars}}super.hashCode(){{/parent}}); - {{/useReflectionEqualsHashCode}} - }{{#vendorExtensions.x-jackson-optional-nullable-helpers}} - - private static int hashCodeNullable(JsonNullable a) { - if (a == null) { - return 1; - } - return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31; - }{{/vendorExtensions.x-jackson-optional-nullable-helpers}} - - @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({{#isPassword}}"*"{{/isPassword}}{{^isPassword}}toIndentedString({{name}}){{/isPassword}}).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 "); - } -{{#supportUrlQuery}} - - /** - * Convert the instance into URL query string. - * - * @return URL query string - */ - public String toUrlQueryString() { - return toUrlQueryString(null); - } - - /** - * Convert the instance into URL query string. - * - * @param prefix prefix of the query string - * @return URL query string - */ - public String toUrlQueryString(String prefix) { - String suffix = ""; - String containerSuffix = ""; - String containerPrefix = ""; - if (prefix == null) { - // style=form, explode=true, e.g. /pet?name=cat&type=manx - prefix = ""; - } else { - // deepObject style e.g. /pet?id[name]=cat&id[type]=manx - prefix = prefix + "["; - suffix = "]"; - containerSuffix = "]"; - containerPrefix = "["; - } - - StringJoiner joiner = new StringJoiner("&"); - - {{#allVars}} - // add `{{baseName}}` to the URL query string - {{#isArray}} - {{#items.isPrimitiveType}} - {{#uniqueItems}} - if ({{getter}}() != null) { - int i = 0; - for ({{{items.datatypeWithEnum}}} _item : {{getter}}()) { - try { - joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, - "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), - URLEncoder.encode(String.valueOf(_item), "UTF-8").replaceAll("\\+", "%20"))); - } catch (UnsupportedEncodingException e) { - // Should never happen, UTF-8 is always supported - throw new RuntimeException(e); - } - } - i++; - } - {{/uniqueItems}} - {{^uniqueItems}} - if ({{getter}}() != null) { - for (int i = 0; i < {{getter}}().size(); i++) { - try { - joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, - "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), - URLEncoder.encode(String.valueOf({{getter}}().get(i)), "UTF-8").replaceAll("\\+", "%20"))); - } catch (UnsupportedEncodingException e) { - // Should never happen, UTF-8 is always supported - throw new RuntimeException(e); - } - } - } - {{/uniqueItems}} - {{/items.isPrimitiveType}} - {{^items.isPrimitiveType}} - {{#items.isModel}} - {{#uniqueItems}} - if ({{getter}}() != null) { - int i = 0; - for ({{{items.dataType}}} _item : {{getter}}()) { - if (_item != null) { - joiner.add(_item.toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, - "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); - } - } - i++; - } - {{/uniqueItems}} - {{^uniqueItems}} - if ({{getter}}() != null) { - for (int i = 0; i < {{getter}}().size(); i++) { - if ({{getter}}().get(i) != null) { - joiner.add({{getter}}().get(i).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, - "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix)))); - } - } - } - {{/uniqueItems}} - {{/items.isModel}} - {{^items.isModel}} - {{#uniqueItems}} - if ({{getter}}() != null) { - int i = 0; - for ({{{items.dataType}}} _item : {{getter}}()) { - if (_item != null) { - try { - joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, - "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), - URLEncoder.encode(String.valueOf(_item), "UTF-8").replaceAll("\\+", "%20"))); - } catch (UnsupportedEncodingException e) { - // Should never happen, UTF-8 is always supported - throw new RuntimeException(e); - } - } - i++; - } - } - {{/uniqueItems}} - {{^uniqueItems}} - if ({{getter}}() != null) { - for (int i = 0; i < {{getter}}().size(); i++) { - if ({{getter}}().get(i) != null) { - try { - joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, - "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, i, containerSuffix), - URLEncoder.encode(String.valueOf({{getter}}().get(i)), "UTF-8").replaceAll("\\+", "%20"))); - } catch (UnsupportedEncodingException e) { - // Should never happen, UTF-8 is always supported - throw new RuntimeException(e); - } - } - } - } - {{/uniqueItems}} - {{/items.isModel}} - {{/items.isPrimitiveType}} - {{/isArray}} - {{^isArray}} - {{#isMap}} - {{^items.isModel}} - if ({{getter}}() != null) { - for (String _key : {{getter}}().keySet()) { - try { - joiner.add(String.format("%s{{baseName}}%s%s=%s", prefix, suffix, - "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix), - {{getter}}().get(_key), URLEncoder.encode(String.valueOf({{getter}}().get(_key)), "UTF-8").replaceAll("\\+", "%20"))); - } catch (UnsupportedEncodingException e) { - // Should never happen, UTF-8 is always supported - throw new RuntimeException(e); - } - } - } - {{/items.isModel}} - {{#items.isModel}} - if ({{getter}}() != null) { - for (String _key : {{getter}}().keySet()) { - if ({{getter}}().get(_key) != null) { - joiner.add({{getter}}().get(_key).toUrlQueryString(String.format("%s{{baseName}}%s%s", prefix, suffix, - "".equals(suffix) ? "" : String.format("%s%d%s", containerPrefix, _key, containerSuffix)))); - } - } - } - {{/items.isModel}} - {{/isMap}} - {{^isMap}} - {{#isPrimitiveType}} - if ({{getter}}() != null) { - try { - joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), "UTF-8").replaceAll("\\+", "%20"))); - } catch (UnsupportedEncodingException e) { - // Should never happen, UTF-8 is always supported - throw new RuntimeException(e); - } - } - {{/isPrimitiveType}} - {{^isPrimitiveType}} - {{#isModel}} - if ({{getter}}() != null) { - joiner.add({{getter}}().toUrlQueryString(prefix + "{{{baseName}}}" + suffix)); - } - {{/isModel}} - {{^isModel}} - if ({{getter}}() != null) { - try { - joiner.add(String.format("%s{{{baseName}}}%s=%s", prefix, suffix, URLEncoder.encode(String.valueOf({{{getter}}}()), "UTF-8").replaceAll("\\+", "%20"))); - } catch (UnsupportedEncodingException e) { - // Should never happen, UTF-8 is always supported - throw new RuntimeException(e); - } - } - {{/isModel}} - {{/isPrimitiveType}} - {{/isMap}} - {{/isArray}} - - {{/allVars}} - return joiner.toString(); - } -{{/supportUrlQuery}} -{{#parcelableModel}} - - public void writeToParcel(Parcel out, int flags) { -{{#model}} -{{#isArray}} - out.writeList(this); -{{/isArray}} -{{^isArray}} -{{#parent}} - super.writeToParcel(out, flags); -{{/parent}} -{{#vars}} - out.writeValue({{name}}); -{{/vars}} -{{/isArray}} -{{/model}} - } - - {{classname}}(Parcel in) { -{{#isArray}} - in.readTypedList(this, {{arrayModelType}}.CREATOR); -{{/isArray}} -{{^isArray}} -{{#parent}} - super(in); -{{/parent}} -{{#vars}} -{{#isPrimitiveType}} - {{name}} = ({{{datatypeWithEnum}}})in.readValue(null); -{{/isPrimitiveType}} -{{^isPrimitiveType}} - {{name}} = ({{{datatypeWithEnum}}})in.readValue({{complexType}}.class.getClassLoader()); -{{/isPrimitiveType}} -{{/vars}} -{{/isArray}} - } - - public int describeContents() { - return 0; - } - - public static final Parcelable.Creator<{{classname}}> CREATOR = new Parcelable.Creator<{{classname}}>() { - public {{classname}} createFromParcel(Parcel in) { -{{#model}} -{{#isArray}} - {{classname}} result = new {{classname}}(); - result.addAll(in.readArrayList({{arrayModelType}}.class.getClassLoader())); - return result; -{{/isArray}} -{{^isArray}} - return new {{classname}}(in); -{{/isArray}} -{{/model}} - } - public {{classname}}[] newArray(int size) { - return new {{classname}}[size]; - } - }; -{{/parcelableModel}} -{{#generateBuilders}} - - {{>javaBuilder}}{{! prevent indent}} -{{/generateBuilders}} - -} diff --git a/customer-service-client/src/main/resources/openapi-templates/sealed.mustache b/customer-service-client/src/main/resources/openapi-templates/sealed.mustache deleted file mode 100644 index 8e5076b..0000000 --- a/customer-service-client/src/main/resources/openapi-templates/sealed.mustache +++ /dev/null @@ -1 +0,0 @@ -{{#useSealedOneOfInterfaces}}{{#vendorExtensions.x-is-one-of-interface}}{{#permits.0}}sealed {{/permits.0}}{{/vendorExtensions.x-is-one-of-interface}}{{^permits.0}}{{#vendorExtensions.x-implements}}final {{/vendorExtensions.x-implements}}{{/permits.0}}{{/useSealedOneOfInterfaces}} \ No newline at end of file diff --git a/customer-service/pom.xml b/customer-service/pom.xml index 6320edc..12b343b 100644 --- a/customer-service/pom.xml +++ b/customer-service/pom.xml @@ -13,7 +13,7 @@ com.example.demo customer-service - 0.5.0 + 0.6.0 customer-service Demo service: Spring Boot 3.4 + Springdoc (OpenAPI) for generics-aware client generation diff --git a/customer-service/src/main/java/com/example/demo/common/openapi/autoreg/AutoWrapperSchemaCustomizer.java b/customer-service/src/main/java/com/example/demo/common/openapi/autoreg/AutoWrapperSchemaCustomizer.java new file mode 100644 index 0000000..75e64ee --- /dev/null +++ b/customer-service/src/main/java/com/example/demo/common/openapi/autoreg/AutoWrapperSchemaCustomizer.java @@ -0,0 +1,48 @@ +package com.example.demo.common.openapi.autoreg; + +import com.example.demo.common.openapi.ApiResponseSchemaFactory; +import com.example.demo.common.openapi.OpenApiSchemas; +import com.example.demo.common.openapi.introspector.ResponseTypeIntrospector; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +@Configuration +public class AutoWrapperSchemaCustomizer { + + private final Set dataRefs; + + public AutoWrapperSchemaCustomizer( + ListableBeanFactory beanFactory, ResponseTypeIntrospector introspector) { + Set refs = new LinkedHashSet<>(); + Map mappings = + beanFactory.getBeansOfType(RequestMappingHandlerMapping.class); + mappings + .values() + .forEach( + rmh -> + rmh.getHandlerMethods().values().stream() + .map(HandlerMethod::getMethod) + .forEach(m -> introspector.extractDataRefName(m).ifPresent(refs::add))); + this.dataRefs = Collections.unmodifiableSet(refs); + } + + @Bean + public OpenApiCustomizer autoResponseWrappers() { + return openApi -> + dataRefs.forEach( + ref -> { + String name = OpenApiSchemas.SCHEMA_SERVICE_RESPONSE + ref; + openApi + .getComponents() + .addSchemas(name, ApiResponseSchemaFactory.createComposedWrapper(ref)); + }); + } +} diff --git a/customer-service/src/main/java/com/example/demo/common/openapi/introspector/ResponseTypeIntrospector.java b/customer-service/src/main/java/com/example/demo/common/openapi/introspector/ResponseTypeIntrospector.java new file mode 100644 index 0000000..d1a9cec --- /dev/null +++ b/customer-service/src/main/java/com/example/demo/common/openapi/introspector/ResponseTypeIntrospector.java @@ -0,0 +1,63 @@ +package com.example.demo.common.openapi.introspector; + +import com.example.demo.common.api.response.ServiceResponse; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import org.springframework.core.ResolvableType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +@Component +public class ResponseTypeIntrospector { + + private static final List ASYNC_WRAPPERS = + List.of( + "reactor.core.publisher.Mono", + "reactor.core.publisher.Flux", + "java.util.concurrent.CompletionStage", + "java.util.concurrent.Future", + "org.springframework.web.context.request.async.DeferredResult", + "org.springframework.web.context.request.async.WebAsyncTask"); + + public Optional extractDataRefName(Method method) { + if (method == null) return Optional.empty(); + + ResolvableType rt = ResolvableType.forMethodReturnType(method); + + rt = unwrapIf(rt); + + for (String wrapper : ASYNC_WRAPPERS) { + rt = unwrapIf(rt, wrapper); + } + + Class raw = rt.resolve(); + if (raw == null || !ServiceResponse.class.isAssignableFrom(raw)) { + return Optional.empty(); + } + + if (rt.getGenerics().length == 0) { + return Optional.empty(); + } + + Class dataClass = rt.getGeneric(0).resolve(); + return Optional.ofNullable(dataClass).map(Class::getSimpleName); + } + + private ResolvableType unwrapIf(ResolvableType type) { + Class raw = type.resolve(); + if (raw != null && ResponseEntity.class.isAssignableFrom(raw)) { + return type.getGeneric(0); + } + return type; + } + + private ResolvableType unwrapIf(ResolvableType type, String wrapperClassName) { + Class raw = type.resolve(); + if (raw != null && Objects.equals(raw.getName(), wrapperClassName)) { + return type.getGeneric(0); + } + return type; + } +} diff --git a/customer-service/src/main/java/com/example/demo/customer/openapi/SwaggerCustomerResponseCustomizer.java b/customer-service/src/main/java/com/example/demo/customer/openapi/SwaggerCustomerResponseCustomizer.java deleted file mode 100644 index 1fe6a78..0000000 --- a/customer-service/src/main/java/com/example/demo/customer/openapi/SwaggerCustomerResponseCustomizer.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.example.demo.customer.openapi; - -import static com.example.demo.common.openapi.OpenApiSchemas.SCHEMA_SERVICE_RESPONSE; - -import com.example.demo.common.openapi.ApiResponseSchemaFactory; -import com.example.demo.customer.api.dto.CustomerCreateResponse; -import com.example.demo.customer.api.dto.CustomerDeleteResponse; -import com.example.demo.customer.api.dto.CustomerDto; -import com.example.demo.customer.api.dto.CustomerListResponse; -import com.example.demo.customer.api.dto.CustomerUpdateResponse; -import java.util.List; -import org.springdoc.core.customizers.OpenApiCustomizer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class SwaggerCustomerResponseCustomizer { - - private static final String REF_CUSTOMER_DTO = CustomerDto.class.getSimpleName(); - private static final String REF_CUSTOMER_CREATE_RESPONSE = - CustomerCreateResponse.class.getSimpleName(); - private static final String REF_CUSTOMER_UPDATE_RESPONSE = - CustomerUpdateResponse.class.getSimpleName(); - private static final String REF_CUSTOMER_DELETE_RESPONSE = - CustomerDeleteResponse.class.getSimpleName(); - private static final String REF_CUSTOMER_LIST_RESPONSE = - CustomerListResponse.class.getSimpleName(); - - private static final List REGISTERED_RESPONSE_REFS = - List.of( - REF_CUSTOMER_DTO, - REF_CUSTOMER_CREATE_RESPONSE, - REF_CUSTOMER_UPDATE_RESPONSE, - REF_CUSTOMER_DELETE_RESPONSE, - REF_CUSTOMER_LIST_RESPONSE); - - private static String apiResponseWrapperNameFor(String ref) { - return SCHEMA_SERVICE_RESPONSE + ref; // e.g. ServiceResponseCustomerDto - } - - @Bean - public OpenApiCustomizer customerWrappers() { - return openApi -> - REGISTERED_RESPONSE_REFS.forEach( - ref -> - openApi - .getComponents() - .addSchemas( - apiResponseWrapperNameFor(ref), - ApiResponseSchemaFactory.createComposedWrapper(ref))); - } -} diff --git a/docs/images/generated-client-wrapper-after.png b/docs/images/generated-client-wrapper-after.png index 1c2fc05..37421ea 100644 Binary files a/docs/images/generated-client-wrapper-after.png and b/docs/images/generated-client-wrapper-after.png differ diff --git a/docs/images/generated-client-wrapper-before.png b/docs/images/generated-client-wrapper-before.png index 939e6dc..cfcc607 100644 Binary files a/docs/images/generated-client-wrapper-before.png and b/docs/images/generated-client-wrapper-before.png differ