diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml index ec41b1ee..6c668e8a 100644 --- a/multiapi-engine/pom.xml +++ b/multiapi-engine/pom.xml @@ -4,7 +4,7 @@ com.sngular multiapi-engine - 6.4.2 + 6.5.0 jar diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/SchemaFieldObject.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/SchemaFieldObject.java index 4d546b85..117a998f 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/SchemaFieldObject.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/model/SchemaFieldObject.java @@ -28,4 +28,8 @@ public class SchemaFieldObject { private Map enumValues; private Object constValue; + + private String description; + + private String example; } 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 2f260e18..7de70e5c 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 @@ -42,6 +42,8 @@ public final class ApiTool { public static final String SCHEMAS = "schemas"; + public static final String DEFS = "$defs"; + public static final String REQUIRED = "required"; public static final String PARAMETERS = "parameters"; @@ -153,7 +155,21 @@ public static JsonNode getItems(final JsonNode schema) { } public static Map getComponentSchemas(final JsonNode openApi) { - return getComponentSchemasByType(openApi, SCHEMAS); + final var schemasMap = getComponentSchemasByType(openApi, SCHEMAS); + // JSON Schema 2020-12 / OpenAPI 3.1: reusable schemas may live under a + // top-level `$defs` object instead of `components/schemas`. + schemasMap.putAll(getDefsSchemas(openApi)); + return schemasMap; + } + + private static Map getDefsSchemas(final JsonNode openApi) { + final var schemasMap = new HashMap(); + if (hasNode(openApi, DEFS)) { + final var defs = getNode(openApi, DEFS); + defs.fieldNames().forEachRemaining(name -> schemasMap.put( + DEFS.toUpperCase() + "/" + StringCaseUtils.titleToSnakeCase(name), getNode(defs, name))); + } + return schemasMap; } private static Map getComponentSchemasByType(final JsonNode openApi, final String schemaType) { @@ -226,7 +242,15 @@ public static String getType(final JsonNode schema) { return ""; } final JsonNode typeNode = getNode(schema, "type"); - return typeNode.isArray() ? getArrayType(typeNode) : StringUtils.defaultIfEmpty(typeNode.textValue(), ""); + if (typeNode.isArray()) { + return getArrayType(typeNode); + } + // OpenAPI 3.1 / JSON Schema 2020-12: a scalar `type: "null"` carries no concrete + // Java type; degrade to object so we never emit an invalid `null` type. + if (TypeConstants.NULL.equalsIgnoreCase(typeNode.textValue())) { + return TypeConstants.OBJECT; + } + return StringUtils.defaultIfEmpty(typeNode.textValue(), ""); } private static String getArrayType(final JsonNode typeNode) { @@ -347,10 +371,12 @@ public static boolean isDateTime(final JsonNode schema) { public static boolean isBinary(final JsonNode schema) { final boolean isMultipartFile; if (hasType(schema) && TypeConstants.STRING.equalsIgnoreCase(getType(schema))) { - if (hasNode(schema, FORMAT)) { - isMultipartFile = "binary".equalsIgnoreCase(getNode(schema, FORMAT).textValue()); + if (hasNode(schema, FORMAT) && "binary".equalsIgnoreCase(getNode(schema, FORMAT).textValue())) { + isMultipartFile = true; } else { - isMultipartFile = false; + // OpenAPI 3.1 / JSON Schema 2020-12 replace `format: binary` with the + // `contentEncoding` / `contentMediaType` keywords for binary payloads. + isMultipartFile = hasNode(schema, "contentEncoding") || hasNode(schema, "contentMediaType"); } } else { isMultipartFile = false; @@ -358,6 +384,46 @@ public static boolean isBinary(final JsonNode schema) { return isMultipartFile; } + public static boolean isNullable(final JsonNode schema) { + if (Objects.isNull(schema)) { + return false; + } + // OpenAPI 3.0 `nullable: true`. + if (getNodeAsBoolean(schema, "nullable")) { + return true; + } + // OpenAPI 3.1 idiom: `type: ["", "null"]`. + if (hasType(schema)) { + final JsonNode typeNode = getNode(schema, "type"); + if (typeNode.isArray()) { + for (final JsonNode element : typeNode) { + if (TypeConstants.NULL.equalsIgnoreCase(element.asText())) { + return true; + } + } + } else { + return TypeConstants.NULL.equalsIgnoreCase(typeNode.textValue()); + } + } + return false; + } + + public static boolean hasPatternProperties(final JsonNode schema) { + return hasNode(schema, "patternProperties"); + } + + public static JsonNode getPatternProperties(final JsonNode schema) { + return getNode(schema, "patternProperties"); + } + + public static boolean hasPrefixItems(final JsonNode schema) { + return hasNode(schema, "prefixItems"); + } + + public static JsonNode getPrefixItems(final JsonNode schema) { + return getNode(schema, "prefixItems"); + } + public static List findContentSchemas(final JsonNode schema) { return hasNode(schema, "content") ? schema.findValues("schema") : Collections.emptyList(); } @@ -414,6 +480,32 @@ public static JsonNode nodeFromFile(final FileLocation ymlParent, final String f return om.readTree(file); } + public static String getDescription(final JsonNode schema) { + return getNodeAsString(schema, "description"); + } + + public static String getExample(final JsonNode schema) { + // OpenAPI 3.0 uses a single `example`; OpenAPI 3.1 / JSON Schema 2020-12 use an + // `examples` array. Prefer `example`, otherwise take the first `examples` entry. + if (hasNode(schema, "example")) { + return asExampleText(getNode(schema, "example")); + } + if (hasNode(schema, "examples")) { + final JsonNode examples = getNode(schema, "examples"); + if (examples.isArray() && examples.elements().hasNext()) { + return asExampleText(examples.elements().next()); + } + } + return null; + } + + private static String asExampleText(final JsonNode example) { + if (Objects.isNull(example) || example.isNull()) { + return null; + } + return example.isValueNode() ? example.asText() : example.toString(); + } + public static boolean hasConst(final JsonNode fieldBody) { return hasNode(fieldBody, "const"); } diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ModelBuilder.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ModelBuilder.java index 5e05f831..736f476e 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ModelBuilder.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ModelBuilder.java @@ -169,12 +169,12 @@ private static Set getFields( baseDir)); } } else if (TypeConstants.ARRAY.equalsIgnoreCase(ApiTool.getType(schema))) { - final String itemType = ApiTool.hasRef(ApiTool.getItems(schema)) ? MapperUtil.getPojoNameFromRef(ApiTool.getItems(schema), specFile, null) - : ApiTool.getType(ApiTool.getItems(schema)); fieldObjectArrayList.add(SchemaFieldObject.builder() .baseName("items") - .dataType(SchemaFieldObjectType.fromTypeList(TypeConstants.ARRAY, itemType)) + .dataType(SchemaFieldObjectType.fromTypeList(TypeConstants.ARRAY, resolveArrayItemType(schema, specFile))) .build()); + } else if (ApiTool.hasPatternProperties(schema)) { + fieldObjectArrayList.add(buildPatternPropertiesField(ApiTool.getName(schema), schema, specFile)); } else if (ApiTool.isAllOf(schema)) { fieldObjectArrayList.addAll(processAllOf(totalSchemas, ApiTool.getAllOf(schema), specFile, compositedSchemas, antiLoopList, baseDir)); } else if (ApiTool.isAnyOf(schema)) { @@ -209,6 +209,8 @@ private static List processFieldObjectList( fieldObjectArrayList.addAll(processArray(fieldName, className, schema, specFile, totalSchemas, compositedSchemas, antiLoopList, baseDir)); } else if (ApiTool.hasAdditionalProperties(schema)) { fieldObjectArrayList.addAll(processMap(fieldName, schema, specFile, totalSchemas, compositedSchemas, antiLoopList, baseDir)); + } else if (ApiTool.hasPatternProperties(schema)) { + fieldObjectArrayList.add(buildPatternPropertiesField(fieldName, schema, specFile)); } else if (ApiTool.hasRef(schema)) { fieldObjectArrayList.add( processRef(fieldName, schema, new SchemaFieldObjectType(MapperUtil.getSimpleType(schema, specFile)), totalSchemas, compositedSchemas, antiLoopList, specFile, baseDir)); @@ -337,9 +339,28 @@ private static List processObjectProperty( } else { fieldObjectArrayList.addAll(processFieldObjectList(buildingSchema, fieldName, fieldName, fieldBody, specFile, totalSchemas, compositedSchemas, antiLoopList, baseDir)); } + applyMetadata(fieldObjectArrayList, fieldName, fieldBody); return fieldObjectArrayList; } + private static void applyMetadata(final List fields, final String fieldName, final JsonNode fieldBody) { + final String description = ApiTool.getDescription(fieldBody); + final String example = ApiTool.getExample(fieldBody); + if (Objects.isNull(description) && Objects.isNull(example)) { + return; + } + for (final var field : fields) { + if (Objects.equals(field.getBaseName(), fieldName)) { + if (Objects.nonNull(description)) { + field.setDescription(description); + } + if (Objects.nonNull(example)) { + field.setExample(example); + } + } + } + } + private static Object getConst(final JsonNode fieldBody) { return ApiTool.hasConst(fieldBody) ? ApiTool.getConst(fieldBody) : null; } @@ -426,11 +447,17 @@ private static List processArray( final Map compositedSchemas, final Set antiLoopList, final Path baseDir) { final List fieldObjectArrayList = new LinkedList<>(); - if (!ApiTool.hasItems(schema)) { + if (!ApiTool.hasItems(schema) || ApiTool.getItems(schema).isBoolean()) { + // No `items` schema, or `items: false` (JSON Schema 2020-12). If the array + // declares positional `prefixItems` (tuple), or nothing typable at all, the + // element type degrades to Object -> List. + final boolean isTuple = ApiTool.hasPrefixItems(schema); fieldObjectArrayList.add(SchemaFieldObject .builder() .baseName(fieldName) - .dataType(new SchemaFieldObjectType(TypeConstants.OBJECT)) + .dataType(isTuple + ? SchemaFieldObjectType.fromTypeList(TypeConstants.ARRAY, TypeConstants.OBJECT) + : new SchemaFieldObjectType(TypeConstants.OBJECT)) .build()); } else { final var items = ApiTool.getItems(schema); @@ -626,7 +653,9 @@ private static SchemaObject createComposedSchema( private static void setFieldType( final SchemaFieldObject field, final JsonNode schemaProperty, final JsonNode schema, final CommonSpecFile specFile, final String key) { - field.setRequired(ApiTool.hasRequired(schema) && ApiTool.checkIfRequired(schema, key)); + // A nullable field (3.0 `nullable: true` or the 3.1 ["T","null"] idiom) must not be + // marked required, otherwise the generated @NotNull would reject a legitimate null value. + field.setRequired(ApiTool.hasRequired(schema) && ApiTool.checkIfRequired(schema, key) && !ApiTool.isNullable(schemaProperty)); if (ApiTool.isArray(schemaProperty)) { final String typeArray; if (ApiTool.hasItems(schemaProperty)) { @@ -888,5 +917,38 @@ private static String getImportClass(final String type) { return StringUtils.isNotBlank(type) && !TypeConstants.NO_IMPORT_TYPE.contains(type) ? StringUtils.capitalize(type) : ""; } + private static String resolveArrayItemType(final JsonNode schema, final CommonSpecFile specFile) { + final var items = ApiTool.getItems(schema); + if (Objects.nonNull(items) && !items.isBoolean()) { + return ApiTool.hasRef(items) ? MapperUtil.getPojoNameFromRef(items, specFile, null) : ApiTool.getType(items); + } + // `prefixItems` (tuple) or `items: false` -> degrade the element type to Object. + return TypeConstants.OBJECT; + } + + private static SchemaFieldObject buildPatternPropertiesField( + final String fieldName, final JsonNode schema, final CommonSpecFile specFile) { + // JSON Schema 2020-12 `patternProperties` maps regex keys to a value schema. + // We model it as Map using the first declared pattern's value schema. + final var patternProps = ApiTool.getPatternProperties(schema); + final var valueSchemas = patternProps.elements(); + final JsonNode valueSchema = valueSchemas.hasNext() ? valueSchemas.next() : null; + final String valueType; + if (Objects.isNull(valueSchema)) { + valueType = TypeConstants.OBJECT; + } else if (ApiTool.hasRef(valueSchema)) { + valueType = MapperUtil.getPojoNameFromRef(valueSchema, specFile, null); + } else if (isBasicType(valueSchema) && ApiTool.hasType(valueSchema)) { + valueType = MapperUtil.getSimpleType(valueSchema, specFile); + } else { + valueType = TypeConstants.OBJECT; + } + return SchemaFieldObject + .builder() + .baseName(StringUtils.defaultIfBlank(fieldName, ADDITIONAL_PROPERTIES)) + .dataType(SchemaFieldObjectType.fromTypeList(TypeConstants.MAP, valueType)) + .build(); + } + } diff --git a/multiapi-engine/src/main/resources/templates/model/templateSchema.ftlh b/multiapi-engine/src/main/resources/templates/model/templateSchema.ftlh index 1cc1b1e4..daf298d2 100644 --- a/multiapi-engine/src/main/resources/templates/model/templateSchema.ftlh +++ b/multiapi-engine/src/main/resources/templates/model/templateSchema.ftlh @@ -340,7 +340,7 @@ public class ${schema.className} { } <#list schema.fieldObjectList as field> - @Schema(name = "${field.baseName?uncap_first}", required = <#if field.required?has_content && field.required == true>true<#else>false) + @Schema(name = "${field.baseName?uncap_first}", required = <#if field.required?has_content && field.required == true>true<#else>false<#if field.description?has_content>, description = "${field.description?j_string}"<#if field.example?has_content>, example = "${field.example?j_string}") <#if field.dataType.baseType == "array"> public ${field.dataType} get${field.baseName?cap_first}() { return ${calculateSafeName (field.baseName, ";")} 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 1dc6859b..c3892474 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 @@ -182,6 +182,13 @@ public final class OpenApiGeneratorFixtures { .clientPackage("com.sngular.multifileplugin.refwithdescription.client") .modelNameSuffix("DTO").build()); + static final List TEST_OPEN_API_31_COMPLETENESS = List + .of(SpecFile.builder().filePath("openapigenerator/testOpenApi31Completeness/api-test.yml") + .apiPackage("com.sngular.multifileplugin.openapi31completeness") + .modelPackage("com.sngular.multifileplugin.openapi31completeness.model") + .clientPackage("com.sngular.multifileplugin.openapi31completeness.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") @@ -891,6 +898,25 @@ static Function validateOpenApi31Types() { DEFAULT_MODEL_API, Collections.emptyList(), null); } + static Function validateOpenApi31Completeness() { + + final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/openapi31completeness"; + + final String DEFAULT_MODEL_API = "generated/com/sngular/multifileplugin/openapi31completeness/model"; + + final String COMMON_PATH = "openapigenerator/testOpenApi31Completeness/"; + + final String ASSETS_PATH = COMMON_PATH + "assets/"; + + final List expectedTestApiFile = List.of(ASSETS_PATH + "GadgetApi.java"); + + final List expectedTestApiModelFiles = List.of(ASSETS_PATH + "GadgetDTO.java", + ASSETS_PATH + "PersonDTO.java"); + + return path -> commonTest(path, expectedTestApiFile, expectedTestApiModelFiles, DEFAULT_TARGET_API, + DEFAULT_MODEL_API, Collections.emptyList(), null); + } + static Function validateRefWithDescription() { final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/refwithdescription"; 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 90d3b56e..18d9edcf 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 @@ -87,6 +87,8 @@ static Stream fileSpecToProcess() { OpenApiGeneratorFixtures.validateOpenApi31Types()), Arguments.of("testRefWithDescription", OpenApiGeneratorFixtures.TEST_REF_WITH_DESCRIPTION, OpenApiGeneratorFixtures.validateRefWithDescription()), + Arguments.of("testOpenApi31Completeness", OpenApiGeneratorFixtures.TEST_OPEN_API_31_COMPLETENESS, + OpenApiGeneratorFixtures.validateOpenApi31Completeness()), Arguments.of("testWebhooks", OpenApiGeneratorFixtures.TEST_WEBHOOKS, OpenApiGeneratorFixtures.validateWebhooks()), Arguments.of("testOpenApi31Union", OpenApiGeneratorFixtures.TEST_OPEN_API_31_UNION, diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testCustomValidators/assets/DataDTO.java b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testCustomValidators/assets/DataDTO.java index aefeaad3..9ac07c5f 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testCustomValidators/assets/DataDTO.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testCustomValidators/assets/DataDTO.java @@ -98,22 +98,22 @@ public DataDTO build() { } } - @Schema(name = "clientName", required = true) + @Schema(name = "clientName", required = true, description = "Nombre del cliente.") public String getClientName() { return clientName; } - @Schema(name = "flightNumber", required = true) + @Schema(name = "flightNumber", required = true, description = "Número de vuelo.") public String getFlightNumber() { return flightNumber; } - @Schema(name = "clientId", required = true) + @Schema(name = "clientId", required = true, description = "Id del cliente.") public Integer getClientId() { return clientId; } - @Schema(name = "test", required = false) + @Schema(name = "test", required = false, description = "Array para probar anotaciones") public List getTest() { return test; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testCustomValidators/assets/StatusMsgDTO.java b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testCustomValidators/assets/StatusMsgDTO.java index 6142677f..83c528ea 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testCustomValidators/assets/StatusMsgDTO.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testCustomValidators/assets/StatusMsgDTO.java @@ -76,7 +76,7 @@ public void setStatus(Status status) { this.status = status; } - @Schema(name = "clientId", required = false) + @Schema(name = "clientId", required = false, description = "Id del cliente.") public Integer getClientId() { return clientId; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueGeneration/assets/DataDTO.java b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueGeneration/assets/DataDTO.java index 93069cfe..06ae966f 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueGeneration/assets/DataDTO.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueGeneration/assets/DataDTO.java @@ -56,7 +56,7 @@ public DataDTO build() { } } - @Schema(name = "clientName", required = false) + @Schema(name = "clientName", required = false, description = "Nombre del cliente.") public String getClientName() { return clientName; } @@ -64,7 +64,7 @@ public void setClientName(String clientName) { this.clientName = clientName; } - @Schema(name = "flightNumber", required = false) + @Schema(name = "flightNumber", required = false, description = "Número de vuelo.") public String getFlightNumber() { return flightNumber; } @@ -72,7 +72,7 @@ public void setFlightNumber(String flightNumber) { this.flightNumber = flightNumber; } - @Schema(name = "clientId", required = false) + @Schema(name = "clientId", required = false, description = "Id del cliente.") public Integer getClientId() { return clientId; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueGeneration/assets/StatusMsgDTO.java b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueGeneration/assets/StatusMsgDTO.java index 37d1a919..ae1fc628 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueGeneration/assets/StatusMsgDTO.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueGeneration/assets/StatusMsgDTO.java @@ -76,7 +76,7 @@ public void setStatus(Status status) { this.status = status; } - @Schema(name = "clientId", required = false) + @Schema(name = "clientId", required = false, description = "Id del cliente.") public Integer getClientId() { return clientId; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueSimpleTypeGeneration/assets/DataDTO.java b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueSimpleTypeGeneration/assets/DataDTO.java index 1f42795e..54e8aa20 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueSimpleTypeGeneration/assets/DataDTO.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testIssueSimpleTypeGeneration/assets/DataDTO.java @@ -56,7 +56,7 @@ public DataDTO build() { } } - @Schema(name = "clientName", required = false) + @Schema(name = "clientName", required = false, description = "Nombre del cliente.") public String getClientName() { return clientName; } @@ -64,7 +64,7 @@ public void setClientName(String clientName) { this.clientName = clientName; } - @Schema(name = "flightNumber", required = false) + @Schema(name = "flightNumber", required = false, description = "Número de vuelo.") public String getFlightNumber() { return flightNumber; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testReferenceFromLocalIssue/assets/UserMessage.java b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testReferenceFromLocalIssue/assets/UserMessage.java index f3cabcb2..8f0500ef 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testReferenceFromLocalIssue/assets/UserMessage.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testReferenceFromLocalIssue/assets/UserMessage.java @@ -47,7 +47,7 @@ public UserMessage build() { } } - @Schema(name = "firstName", required = false) + @Schema(name = "firstName", required = false, description = "foo") public String getFirstName() { return firstName; } @@ -55,7 +55,7 @@ public void setFirstName(String firstName) { this.firstName = firstName; } - @Schema(name = "lastName", required = false) + @Schema(name = "lastName", required = false, description = "bar") public String getLastName() { return lastName; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testSubObjectSameName/assets/input/model/Data.java b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testSubObjectSameName/assets/input/model/Data.java index b1ff789a..5e687988 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testSubObjectSameName/assets/input/model/Data.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testSubObjectSameName/assets/input/model/Data.java @@ -38,7 +38,7 @@ public Data build() { } } - @Schema(name = "commitId", required = false) + @Schema(name = "commitId", required = false, example = "toto") public String getCommitId() { return commitId; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testSubObjectSameName/assets/output/model/Data.java b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testSubObjectSameName/assets/output/model/Data.java index 4a0c24dd..789cb004 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v2/testSubObjectSameName/assets/output/model/Data.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v2/testSubObjectSameName/assets/output/model/Data.java @@ -47,7 +47,7 @@ public Data build() { } } - @Schema(name = "name", required = false) + @Schema(name = "name", required = false, example = "hugues") public String getName() { return name; } @@ -55,7 +55,7 @@ public void setName(String name) { this.name = name; } - @Schema(name = "tenantId", required = false) + @Schema(name = "tenantId", required = false, example = "tenant1") public String getTenantId() { return tenantId; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testCustomValidators/assets/DataDTO.java b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testCustomValidators/assets/DataDTO.java index 22da9b72..71e0fb62 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testCustomValidators/assets/DataDTO.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testCustomValidators/assets/DataDTO.java @@ -98,22 +98,22 @@ public DataDTO build() { } } - @Schema(name = "clientName", required = true) + @Schema(name = "clientName", required = true, description = "Nombre del cliente.") public String getClientName() { return clientName; } - @Schema(name = "flightNumber", required = true) + @Schema(name = "flightNumber", required = true, description = "Número de vuelo.") public String getFlightNumber() { return flightNumber; } - @Schema(name = "clientId", required = true) + @Schema(name = "clientId", required = true, description = "Id del cliente.") public Integer getClientId() { return clientId; } - @Schema(name = "test", required = false) + @Schema(name = "test", required = false, description = "Array para probar anotaciones") public List getTest() { return test; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testCustomValidators/assets/StatusMsgDTO.java b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testCustomValidators/assets/StatusMsgDTO.java index 6142677f..83c528ea 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testCustomValidators/assets/StatusMsgDTO.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testCustomValidators/assets/StatusMsgDTO.java @@ -76,7 +76,7 @@ public void setStatus(Status status) { this.status = status; } - @Schema(name = "clientId", required = false) + @Schema(name = "clientId", required = false, description = "Id del cliente.") public Integer getClientId() { return clientId; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueGeneration/assets/DataDTO.java b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueGeneration/assets/DataDTO.java index 93069cfe..06ae966f 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueGeneration/assets/DataDTO.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueGeneration/assets/DataDTO.java @@ -56,7 +56,7 @@ public DataDTO build() { } } - @Schema(name = "clientName", required = false) + @Schema(name = "clientName", required = false, description = "Nombre del cliente.") public String getClientName() { return clientName; } @@ -64,7 +64,7 @@ public void setClientName(String clientName) { this.clientName = clientName; } - @Schema(name = "flightNumber", required = false) + @Schema(name = "flightNumber", required = false, description = "Número de vuelo.") public String getFlightNumber() { return flightNumber; } @@ -72,7 +72,7 @@ public void setFlightNumber(String flightNumber) { this.flightNumber = flightNumber; } - @Schema(name = "clientId", required = false) + @Schema(name = "clientId", required = false, description = "Id del cliente.") public Integer getClientId() { return clientId; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueGeneration/assets/StatusMsgDTO.java b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueGeneration/assets/StatusMsgDTO.java index 37d1a919..ae1fc628 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueGeneration/assets/StatusMsgDTO.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueGeneration/assets/StatusMsgDTO.java @@ -76,7 +76,7 @@ public void setStatus(Status status) { this.status = status; } - @Schema(name = "clientId", required = false) + @Schema(name = "clientId", required = false, description = "Id del cliente.") public Integer getClientId() { return clientId; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueSimpleTypeGeneration/assets/DataDTO.java b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueSimpleTypeGeneration/assets/DataDTO.java index 1f42795e..54e8aa20 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueSimpleTypeGeneration/assets/DataDTO.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testIssueSimpleTypeGeneration/assets/DataDTO.java @@ -56,7 +56,7 @@ public DataDTO build() { } } - @Schema(name = "clientName", required = false) + @Schema(name = "clientName", required = false, description = "Nombre del cliente.") public String getClientName() { return clientName; } @@ -64,7 +64,7 @@ public void setClientName(String clientName) { this.clientName = clientName; } - @Schema(name = "flightNumber", required = false) + @Schema(name = "flightNumber", required = false, description = "Número de vuelo.") public String getFlightNumber() { return flightNumber; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testReferenceFromLocalIssue/assets/UserMessage.java b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testReferenceFromLocalIssue/assets/UserMessage.java index f3cabcb2..8f0500ef 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testReferenceFromLocalIssue/assets/UserMessage.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testReferenceFromLocalIssue/assets/UserMessage.java @@ -47,7 +47,7 @@ public UserMessage build() { } } - @Schema(name = "firstName", required = false) + @Schema(name = "firstName", required = false, description = "foo") public String getFirstName() { return firstName; } @@ -55,7 +55,7 @@ public void setFirstName(String firstName) { this.firstName = firstName; } - @Schema(name = "lastName", required = false) + @Schema(name = "lastName", required = false, description = "bar") public String getLastName() { return lastName; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testSubObjectSameName/assets/input/model/Data.java b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testSubObjectSameName/assets/input/model/Data.java index b1ff789a..5e687988 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testSubObjectSameName/assets/input/model/Data.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testSubObjectSameName/assets/input/model/Data.java @@ -38,7 +38,7 @@ public Data build() { } } - @Schema(name = "commitId", required = false) + @Schema(name = "commitId", required = false, example = "toto") public String getCommitId() { return commitId; } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testSubObjectSameName/assets/output/model/Data.java b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testSubObjectSameName/assets/output/model/Data.java index 4a0c24dd..789cb004 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/v3/testSubObjectSameName/assets/output/model/Data.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/v3/testSubObjectSameName/assets/output/model/Data.java @@ -47,7 +47,7 @@ public Data build() { } } - @Schema(name = "name", required = false) + @Schema(name = "name", required = false, example = "hugues") public String getName() { return name; } @@ -55,7 +55,7 @@ public void setName(String name) { this.name = name; } - @Schema(name = "tenantId", required = false) + @Schema(name = "tenantId", required = false, example = "tenant1") public String getTenantId() { return tenantId; } diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/api-test.yml new file mode 100644 index 00000000..f96902cc --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/api-test.yml @@ -0,0 +1,70 @@ +--- +# Regression for additional OpenAPI 3.1 / JSON Schema 2020-12 features: +# - $defs as a schema source (#/$defs/Person) -> generates PersonDTO +# - prefixItems tuple arrays -> List +# - scalar type: "null" -> Object (no invalid `null` type) +# - nullable via ["T","null"] idiom even when listed as required -> no @NotNull +# - patternProperties object -> Map +# - 3.1 binary string via contentEncoding -> MultipartFile +openapi: "3.1.0" +info: + version: 1.0.0 + title: Gadget API (OpenAPI 3.1) + license: + name: MIT +servers: + - url: http://localhost:8080/v1 +tags: + - name: gadget +paths: + /gadget: + get: + summary: Get a gadget + operationId: getGadget + tags: + - gadget + responses: + '200': + description: The requested gadget + content: + application/json: + schema: + $ref: "#/components/schemas/Gadget" +components: + schemas: + Gadget: + type: object + required: + - serial + properties: + id: + type: string + description: The unique gadget identifier + examples: + - "gadget-001" + serial: + type: ["string", "null"] + example: "SN-12345" + payload: + type: string + contentEncoding: base64 + coords: + type: array + prefixItems: + - type: number + - type: number + metadata: + type: object + patternProperties: + "^x-": + type: string + nothing: + type: "null" + owner: + $ref: "#/$defs/Person" +$defs: + Person: + type: object + properties: + name: + type: string diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/assets/GadgetApi.java b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/assets/GadgetApi.java new file mode 100644 index 00000000..a85f9770 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/assets/GadgetApi.java @@ -0,0 +1,46 @@ +package com.sngular.multifileplugin.openapi31completeness; + +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.openapi31completeness.model.GadgetDTO; + +public interface GadgetApi { + + /** + * GET /gadget: Get a gadget + * @return The requested gadget; (status code 200) + */ + + @Operation( + operationId = "getGadget", + summary = "Get a gadget", + tags = {"gadget"}, + responses = { + @ApiResponse(responseCode = "200", description = "The requested gadget", content = @Content(mediaType = "application/json", schema = @Schema(implementation = GadgetDTO.class))) + } + ) + @RequestMapping( + method = RequestMethod.GET, + value = "/gadget", + produces = {"application/json"} + ) + + default ResponseEntity getGadget() { + return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED); + } + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/assets/GadgetDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/assets/GadgetDTO.java new file mode 100644 index 00000000..414b1cfc --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/assets/GadgetDTO.java @@ -0,0 +1,203 @@ +package com.sngular.multifileplugin.openapi31completeness.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 org.springframework.web.multipart.MultipartFile; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; + +@JsonDeserialize(builder = GadgetDTO.GadgetDTOBuilder.class) +public class GadgetDTO { + + @JsonProperty(value ="payload") + private MultipartFile payload; + @JsonProperty(value ="nothing") + private Object nothing; + @JsonProperty(value ="id") + private String id; + @JsonProperty(value ="metadata") + private Map metadata; + @JsonProperty(value ="coords") + private List coords; + @JsonProperty(value ="owner") + private PersonDTO owner; + @JsonProperty(value ="serial") + private String serial; + + private GadgetDTO(GadgetDTOBuilder builder) { + this.payload = builder.payload; + this.nothing = builder.nothing; + this.id = builder.id; + this.metadata = builder.metadata; + this.coords = builder.coords; + this.owner = builder.owner; + this.serial = builder.serial; + + } + + public static GadgetDTO.GadgetDTOBuilder builder() { + return new GadgetDTO.GadgetDTOBuilder(); + } + + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static class GadgetDTOBuilder { + + private MultipartFile payload; + private Object nothing; + private String id; + private Map metadata = new HashMap(); + private List coords = new ArrayList(); + private PersonDTO owner; + private String serial; + + public GadgetDTO.GadgetDTOBuilder payload(MultipartFile payload) { + this.payload = payload; + return this; + } + + public GadgetDTO.GadgetDTOBuilder nothing(Object nothing) { + this.nothing = nothing; + return this; + } + + public GadgetDTO.GadgetDTOBuilder id(String id) { + this.id = id; + return this; + } + + public GadgetDTO.GadgetDTOBuilder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public GadgetDTO.GadgetDTOBuilder metadataValue(String key, String value) { + this.metadata.put(key, value); + return this; + } + + public GadgetDTO.GadgetDTOBuilder coords(List coords) { + if (!coords.isEmpty()) { + this.coords.addAll(coords); + } + return this; + } + + public GadgetDTO.GadgetDTOBuilder coord(Object coord) { + if (Objects.nonNull(coord)) { + this.coords.add(coord); + } + return this; + } + + public GadgetDTO.GadgetDTOBuilder owner(PersonDTO owner) { + this.owner = owner; + return this; + } + + public GadgetDTO.GadgetDTOBuilder serial(String serial) { + this.serial = serial; + return this; + } + + public GadgetDTO build() { + GadgetDTO gadgetDTO = new GadgetDTO(this); + return gadgetDTO; + } + } + + @Schema(name = "payload", required = false) + public MultipartFile getPayload() { + return payload; + } + public void setPayload(MultipartFile payload) { + this.payload = payload; + } + + @Schema(name = "nothing", required = false) + public Object getNothing() { + return nothing; + } + public void setNothing(Object nothing) { + this.nothing = nothing; + } + + @Schema(name = "id", required = false, description = "The unique gadget identifier", example = "gadget-001") + public String getId() { + return id; + } + public void setId(String id) { + this.id = id; + } + + @Schema(name = "metadata", required = false) + public Map getMetadata() { + return metadata; + } + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + @Schema(name = "coords", required = false) + public List getCoords() { + return coords; + } + public void setCoords(List coords) { + this.coords = coords; + } + + @Schema(name = "owner", required = false) + public PersonDTO getOwner() { + return owner; + } + public void setOwner(PersonDTO owner) { + this.owner = owner; + } + + @Schema(name = "serial", required = false, example = "SN-12345") + public String getSerial() { + return serial; + } + public void setSerial(String serial) { + this.serial = serial; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + GadgetDTO gadgetDTO = (GadgetDTO) o; + return Objects.equals(this.payload, gadgetDTO.payload) && Objects.equals(this.nothing, gadgetDTO.nothing) && Objects.equals(this.id, gadgetDTO.id) && Objects.equals(this.metadata, gadgetDTO.metadata) && Objects.equals(this.coords, gadgetDTO.coords) && Objects.equals(this.owner, gadgetDTO.owner) && Objects.equals(this.serial, gadgetDTO.serial); + } + + @Override + public int hashCode() { + return Objects.hash(payload, nothing, id, metadata, coords, owner, serial); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("GadgetDTO{"); + sb.append(" payload:").append(payload).append(","); + sb.append(" nothing:").append(nothing).append(","); + sb.append(" id:").append(id).append(","); + sb.append(" metadata:").append(metadata).append(","); + sb.append(" coords:").append(coords).append(","); + sb.append(" owner:").append(owner).append(","); + sb.append(" serial:").append(serial); + sb.append("}"); + return sb.toString(); + } + + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/assets/PersonDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/assets/PersonDTO.java new file mode 100644 index 00000000..f410cff2 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testOpenApi31Completeness/assets/PersonDTO.java @@ -0,0 +1,76 @@ +package com.sngular.multifileplugin.openapi31completeness.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; + +@JsonDeserialize(builder = PersonDTO.PersonDTOBuilder.class) +public class PersonDTO { + + @JsonProperty(value ="name") + private String name; + + private PersonDTO(PersonDTOBuilder builder) { + this.name = builder.name; + + } + + public static PersonDTO.PersonDTOBuilder builder() { + return new PersonDTO.PersonDTOBuilder(); + } + + @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "") + public static class PersonDTOBuilder { + + private String name; + + public PersonDTO.PersonDTOBuilder name(String name) { + this.name = name; + return this; + } + + public PersonDTO build() { + PersonDTO personDTO = new PersonDTO(this); + return personDTO; + } + } + + @Schema(name = "name", required = false) + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PersonDTO personDTO = (PersonDTO) o; + return Objects.equals(this.name, personDTO.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("PersonDTO{"); + sb.append(" name:").append(name); + sb.append("}"); + return sb.toString(); + } + + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRefWithDescription/assets/OrderDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testRefWithDescription/assets/OrderDTO.java index 9e932363..cf0529bb 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testRefWithDescription/assets/OrderDTO.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testRefWithDescription/assets/OrderDTO.java @@ -64,7 +64,7 @@ public void setId(String id) { this.id = id; } - @Schema(name = "customer", required = false) + @Schema(name = "customer", required = false, description = "The customer that placed the order") public CustomerDTO getCustomer() { return customer; } @@ -72,7 +72,7 @@ public void setCustomer(CustomerDTO customer) { this.customer = customer; } - @Schema(name = "shippingAddress", required = false) + @Schema(name = "shippingAddress", required = false, description = "Where the order is shipped") public AddressDTO getShippingAddress() { return shippingAddress; } diff --git a/scs-multiapi-gradle-plugin/build.gradle b/scs-multiapi-gradle-plugin/build.gradle index 993a8fe3..bc031991 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.4.2' +version = '6.5.0' def SCSMultiApiPluginGroupId = group def SCSMultiApiPluginVersion = version @@ -31,7 +31,7 @@ dependencies { shadow localGroovy() shadow gradleApi() - implementation 'com.sngular:multiapi-engine:6.4.2' + implementation 'com.sngular:multiapi-engine:6.5.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.4.2' + implementation 'com.sngular:scs-multiapi-gradle-plugin:6.5.0' implementation 'org.assertj:assertj-core:3.24.2' } diff --git a/scs-multiapi-maven-plugin/pom.xml b/scs-multiapi-maven-plugin/pom.xml index 81ee251a..d70d56f0 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.4.2 + 6.5.0 maven-plugin AsyncApi - OpenApi Code Generator Maven Plugin @@ -271,7 +271,7 @@ com.sngular multiapi-engine - 6.4.2 + 6.5.0 org.apache.maven