diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml index af0d2207..ef7ec3f6 100644 --- a/multiapi-engine/pom.xml +++ b/multiapi-engine/pom.xml @@ -4,7 +4,7 @@ com.sngular multiapi-engine - 6.3.3 + 6.4.0 jar diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/TypeConstants.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/TypeConstants.java index a4fd82a7..7b98ed51 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/TypeConstants.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/TypeConstants.java @@ -18,6 +18,8 @@ public final class TypeConstants { public static final String ARRAY = "array"; + public static final String NULL = "null"; + public static final String MAP = "map"; public static final String BIG_DECIMAL = "bigDecimal"; diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java index 11333ecf..c271dc9e 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java @@ -220,7 +220,23 @@ public static boolean hasType(final JsonNode schema) { } public static String getType(final JsonNode schema) { - return hasType(schema) ? StringUtils.defaultIfEmpty(getNodeAsString(schema, "type"), "") : ""; + if (!hasType(schema)) { + return ""; + } + final JsonNode typeNode = getNode(schema, "type"); + if (typeNode.isArray()) { + // OpenAPI 3.1 / JSON Schema 2020-12: "type" may be an array (e.g. ["string", "null"]). + // Resolve to the first non-"null" entry; "null" only marks the type as nullable. + String resolvedType = ""; + for (final JsonNode element : typeNode) { + if (!TypeConstants.NULL.equalsIgnoreCase(element.asText())) { + resolvedType = element.asText(); + break; + } + } + return resolvedType; + } + return StringUtils.defaultIfEmpty(typeNode.textValue(), ""); } public static boolean hasItems(final JsonNode schema) { diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/MapperUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/MapperUtil.java index b7734089..0850c200 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/MapperUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/MapperUtil.java @@ -50,7 +50,7 @@ private static boolean checkIfNumber(final String nodeType) { private static String processNumber(final JsonNode schema) { - final var nodeType = schema.get("type").asText(); + final var nodeType = ApiTool.getType(schema); final var formatType = schema.has("format") ? schema.get("format").asText() : null; String type = TypeConstants.INTEGER; if (TypeConstants.NUMBER.equalsIgnoreCase(nodeType)) { diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java index 8e92e4e4..6299c6f9 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java @@ -147,6 +147,13 @@ public final class OpenApiGeneratorFixtures { .clientPackage("com.sngular.multifileplugin.externalref.client").modelNamePrefix("Api") .modelNameSuffix("DTO").build()); + static final List TEST_OPEN_API_31_TYPES = List + .of(SpecFile.builder().filePath("openapigenerator/testOpenApi31Types/api-test.yml") + .apiPackage("com.sngular.multifileplugin.openapi31types") + .modelPackage("com.sngular.multifileplugin.openapi31types.model") + .clientPackage("com.sngular.multifileplugin.openapi31types.client") + .modelNameSuffix("DTO").build()); + static final List TEST_EXTERNAL_PATH_ITEM_REF_GENERATION = List .of(SpecFile.builder().filePath("openapigenerator/testExternalPathItemRefsGeneration/api-test.yml") .apiPackage("com.sngular.multifileplugin.externalpathitemref") @@ -770,6 +777,24 @@ static Function validateExternalRefGeneration() { DEFAULT_MODEL_API, expectedExceptionFiles, DEFAULT_EXCEPTION_API); } + static Function validateOpenApi31Types() { + + final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/openapi31types"; + + final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/openapi31types/model"; + + final String COMMON_PATH = "openapigenerator/testOpenApi31Types/"; + + final String ASSETS_PATH = COMMON_PATH + "assets/"; + + final List expectedTestApiFile = List.of(ASSETS_PATH + "ProfileApi.java"); + + final List expectedTestApiModelFiles = List.of(ASSETS_PATH + "ProfileDTO.java"); + + return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API, + DEFAULT_MODEL_API, Collections.emptyList(), null); + } + static Function validateExternalPathItemRefGeneration() { final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/externalpathitemref"; diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java index 2bf48a1d..9ff2db7c 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java @@ -83,6 +83,8 @@ static Stream fileSpecToProcess() { OpenApiGeneratorFixtures.validateEnumsLombokGeneration()), Arguments.of("testExternalRefsGeneration", OpenApiGeneratorFixtures.TEST_EXTERNAL_REF_GENERATION, OpenApiGeneratorFixtures.validateExternalRefGeneration()), + Arguments.of("testOpenApi31Types", OpenApiGeneratorFixtures.TEST_OPEN_API_31_TYPES, + OpenApiGeneratorFixtures.validateOpenApi31Types()), Arguments.of("testExternalPathItemRefsGeneration", OpenApiGeneratorFixtures.TEST_EXTERNAL_PATH_ITEM_REF_GENERATION, OpenApiGeneratorFixtures.validateExternalPathItemRefGeneration()), Arguments.of("testAnyOfInResponse", OpenApiGeneratorFixtures.TEST_ANY_OF_IN_RESPONSE, diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/api-test.yml new file mode 100644 index 00000000..352d08d2 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/api-test.yml @@ -0,0 +1,49 @@ +--- +# Reproduction/regression for OpenAPI 3.1.x support (issue #375). +# Exercises JSON Schema 2020-12 array-valued `type`, including the ["", "null"] +# nullable idiom that replaces 3.0's `nullable: true`, plus a int64 and a plain array. +openapi: "3.1.0" +info: + version: 1.0.0 + title: Profile API (OpenAPI 3.1) + license: + name: MIT +servers: + - url: http://localhost:8080/v1 +tags: + - name: profile +paths: + /profile: + get: + summary: Get the profile + operationId: getProfile + tags: + - profile + responses: + '200': + description: The requested profile + content: + application/json: + schema: + $ref: "#/components/schemas/Profile" +components: + schemas: + Profile: + type: object + properties: + id: + type: string + nickname: + type: ["string", "null"] + age: + type: ["integer", "null"] + score: + type: ["number", "null"] + format: double + loginCount: + type: integer + format: int64 + tags: + type: array + items: + type: string diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileApi.java b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileApi.java new file mode 100644 index 00000000..549f239b --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileApi.java @@ -0,0 +1,46 @@ +package com.sngular.multifileplugin.openapi31types; + +import java.util.Optional; +import java.util.List; +import java.util.Map; +import javax.validation.Valid; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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 org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.NativeWebRequest; + +import com.sngular.multifileplugin.openapi31types.model.ProfileDTO; + +public interface ProfileApi { + + /** + * GET /profile: Get the profile + * @return The requested profile; (status code 200) + */ + + @Operation( + operationId = "getProfile", + summary = "Get the profile", + tags = {"profile"}, + responses = { + @ApiResponse(responseCode = "200", description = "The requested profile", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProfileDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/profile", + produces = {"application/json"} + ) + + default ResponseEntity getProfile() { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileDTO.java new file mode 100644 index 00000000..7a0671d0 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Types/assets/ProfileDTO.java @@ -0,0 +1,177 @@ +package com.sngular.multifileplugin.openapi31types.model; + +import java.util.Objects; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.ArrayList; + +@JsonDeserialize(builder = ProfileDTO.ProfileDTOBuilder.class) +public class ProfileDTO { + + @JsonProperty(value ="tags") + private List tags; + @JsonProperty(value ="id") + private String id; + @JsonProperty(value ="loginCount") + private Long loginCount; + @JsonProperty(value ="age") + private Integer age; + @JsonProperty(value ="score") + private Double score; + @JsonProperty(value ="nickname") + private String nickname; + + private ProfileDTO(ProfileDTOBuilder builder) { + this.tags = builder.tags; + this.id = builder.id; + this.loginCount = builder.loginCount; + this.age = builder.age; + this.score = builder.score; + this.nickname = builder.nickname; + + } + + public static ProfileDTO.ProfileDTOBuilder builder() { + return new ProfileDTO.ProfileDTOBuilder(); + } + + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static class ProfileDTOBuilder { + + private List tags = new ArrayList(); + private String id; + private Long loginCount; + private Integer age; + private Double score; + private String nickname; + + public ProfileDTO.ProfileDTOBuilder tags(List tags) { + if (!tags.isEmpty()) { + this.tags.addAll(tags); + } + return this; + } + + public ProfileDTO.ProfileDTOBuilder tag(String tag) { + if (Objects.nonNull(tag)) { + this.tags.add(tag); + } + return this; + } + + public ProfileDTO.ProfileDTOBuilder id(String id) { + this.id = id; + return this; + } + + public ProfileDTO.ProfileDTOBuilder loginCount(Long loginCount) { + this.loginCount = loginCount; + return this; + } + + public ProfileDTO.ProfileDTOBuilder age(Integer age) { + this.age = age; + return this; + } + + public ProfileDTO.ProfileDTOBuilder score(Double score) { + this.score = score; + return this; + } + + public ProfileDTO.ProfileDTOBuilder nickname(String nickname) { + this.nickname = nickname; + return this; + } + + public ProfileDTO build() { + ProfileDTO profileDTO = new ProfileDTO(this); + return profileDTO; + } + } + + @Schema(name = "tags", required = false) + public List getTags() { + return tags; + } + public void setTags(List tags) { + this.tags = tags; + } + + @Schema(name = "id", required = false) + public String getId() { + return id; + } + public void setId(String id) { + this.id = id; + } + + @Schema(name = "loginCount", required = false) + public Long getLoginCount() { + return loginCount; + } + public void setLoginCount(Long loginCount) { + this.loginCount = loginCount; + } + + @Schema(name = "age", required = false) + public Integer getAge() { + return age; + } + public void setAge(Integer age) { + this.age = age; + } + + @Schema(name = "score", required = false) + public Double getScore() { + return score; + } + public void setScore(Double score) { + this.score = score; + } + + @Schema(name = "nickname", required = false) + public String getNickname() { + return nickname; + } + public void setNickname(String nickname) { + this.nickname = nickname; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ProfileDTO profileDTO = (ProfileDTO) o; + return Objects.equals(this.tags, profileDTO.tags) && Objects.equals(this.id, profileDTO.id) && Objects.equals(this.loginCount, profileDTO.loginCount) && Objects.equals(this.age, profileDTO.age) && Objects.equals(this.score, profileDTO.score) && Objects.equals(this.nickname, profileDTO.nickname); + } + + @Override + public int hashCode() { + return Objects.hash(tags, id, loginCount, age, score, nickname); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ProfileDTO{"); + sb.append(" tags:").append(tags).append(","); + sb.append(" id:").append(id).append(","); + sb.append(" loginCount:").append(loginCount).append(","); + sb.append(" age:").append(age).append(","); + sb.append(" score:").append(score).append(","); + sb.append(" nickname:").append(nickname); + sb.append("}"); + return sb.toString(); + } + + +} diff --git a/scs-multiapi-gradle-plugin/build.gradle b/scs-multiapi-gradle-plugin/build.gradle index 410ffb23..67976a57 100644 --- a/scs-multiapi-gradle-plugin/build.gradle +++ b/scs-multiapi-gradle-plugin/build.gradle @@ -21,7 +21,7 @@ repositories { } group = 'com.sngular' -version = '6.3.3' +version = '6.4.0' def SCSMultiApiPluginGroupId = group def SCSMultiApiPluginVersion = version @@ -31,7 +31,7 @@ dependencies { shadow localGroovy() shadow gradleApi() - implementation 'com.sngular:multiapi-engine:6.3.3' + implementation 'com.sngular:multiapi-engine:6.4.0' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'com.puppycrawl.tools:checkstyle:10.12.3' testImplementation 'org.junit.platform:junit-platform-launcher:1.9.2' @@ -100,7 +100,7 @@ testing { integrationTest(JvmTestSuite) { dependencies { - implementation 'com.sngular:scs-multiapi-gradle-plugin:6.3.1' + implementation 'com.sngular:scs-multiapi-gradle-plugin:6.4.0' implementation 'org.assertj:assertj-core:3.24.2' } diff --git a/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/AsyncApiTask.groovy b/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/AsyncApiTask.groovy index 74c39b13..2eab7796 100644 --- a/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/AsyncApiTask.groovy +++ b/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/AsyncApiTask.groovy @@ -17,7 +17,9 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault +@DisableCachingByDefault(because = "Generation depends on external spec files that are not declared as cacheable inputs") abstract class AsyncApiTask extends DefaultTask { @Optional diff --git a/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/OpenApiTask.groovy b/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/OpenApiTask.groovy index b7b21fe0..f5b13937 100644 --- a/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/OpenApiTask.groovy +++ b/scs-multiapi-gradle-plugin/src/main/groovy/com/sngular/api/generator/plugin/OpenApiTask.groovy @@ -16,7 +16,9 @@ import org.gradle.api.file.DirectoryProperty import org.gradle.api.tasks.Optional import org.gradle.api.tasks.OutputDirectory import org.gradle.api.tasks.TaskAction +import org.gradle.work.DisableCachingByDefault +@DisableCachingByDefault(because = "Generation depends on external spec files that are not declared as cacheable inputs") abstract class OpenApiTask extends DefaultTask { @Optional diff --git a/scs-multiapi-maven-plugin/pom.xml b/scs-multiapi-maven-plugin/pom.xml index 80a69f6c..b9e8fd8b 100644 --- a/scs-multiapi-maven-plugin/pom.xml +++ b/scs-multiapi-maven-plugin/pom.xml @@ -4,7 +4,7 @@ com.sngular scs-multiapi-maven-plugin - 6.3.3 + 6.4.0 maven-plugin AsyncApi - OpenApi Code Generator Maven Plugin @@ -271,7 +271,7 @@ com.sngular multiapi-engine - 6.3.3 + 6.4.0 org.apache.maven