From d97c152c140a5e4b5e0079485f031bd18b38be26 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Thu, 5 May 2022 16:22:52 +0800 Subject: [PATCH] Add new method to detect self-referencing in ModelUtils (#12294) * fix unaliasing object schema, add tests * add self reference check * add self reference check, add test --- .../codegen/utils/ModelUtils.java | 99 ++++++++++++++++++- .../codegen/DefaultCodegenTest.java | 25 +++++ .../resources/3_0/schema-unalias-test.yml | 43 ++++++++ 3 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/schema-unalias-test.yml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java index 8787b70867b..ac3ef1346b0 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java @@ -1065,6 +1065,96 @@ public class ModelUtils { return entry.getValue().getSchema(); } + /** + * Has self reference? + * + * @param openAPI OpenAPI spec. + * @param schema Schema + * @return boolean true if it has at least one self reference + */ + public static boolean hasSelfReference(OpenAPI openAPI, + Schema schema) { + return hasSelfReference(openAPI, schema, null); + } + + /** + * Has self reference? + * + * @param openAPI OpenAPI spec. + * @param schema Schema + * @param visitedSchemaNames A set of visited schema names + * @return boolean true if it has at least one self reference + */ + public static boolean hasSelfReference(OpenAPI openAPI, + Schema schema, + Set visitedSchemaNames) { + if (visitedSchemaNames == null) { + visitedSchemaNames = new HashSet(); + } + + if (schema.get$ref() != null) { + String ref = getSimpleRef(schema.get$ref()); + if (!visitedSchemaNames.contains(ref)) { + visitedSchemaNames.add(ref); + Schema referencedSchema = getSchemas(openAPI).get(ref); + if (referencedSchema != null) { + return hasSelfReference(openAPI, referencedSchema, visitedSchemaNames); + } else { + LOGGER.error("Failed to obtain schema from `{}` in self reference check", ref); + return false; + } + } else { + return true; + } + } + if (schema instanceof ComposedSchema) { + List oneOf = ((ComposedSchema) schema).getOneOf(); + if (oneOf != null) { + for (Schema s : oneOf) { + if (hasSelfReference(openAPI, s, visitedSchemaNames)) { + return true; + } + } + } + List allOf = ((ComposedSchema) schema).getAllOf(); + if (allOf != null) { + for (Schema s : allOf) { + if (hasSelfReference(openAPI, s, visitedSchemaNames)) { + return true; + } + } + } + List anyOf = ((ComposedSchema) schema).getAnyOf(); + if (anyOf != null) { + for (Schema s : anyOf) { + if (hasSelfReference(openAPI, s, visitedSchemaNames)) { + return true; + } + } + } + } else if (isArraySchema(schema)) { + Schema itemsSchema = ((ArraySchema) schema).getItems(); + if (itemsSchema != null) { + return hasSelfReference(openAPI, itemsSchema, visitedSchemaNames); + } + } else if (isMapSchema(schema)) { + Object additionalProperties = schema.getAdditionalProperties(); + if (additionalProperties instanceof Schema) { + return hasSelfReference(openAPI, (Schema) additionalProperties, visitedSchemaNames); + } + } else if (schema.getNot() != null) { + return hasSelfReference(openAPI, schema.getNot(), visitedSchemaNames); + } else if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { + // go through properties to see if there's any self-reference + for (Schema property : ((Map)schema.getProperties()).values()) { + if (hasSelfReference(openAPI, property, visitedSchemaNames)) { + return true; + } + } + } + return false; + } + /** * Get the actual schema from aliases. If the provided schema is not an alias, the schema itself will be returned. * @@ -1131,7 +1221,14 @@ public class ModelUtils { } } else if (isObjectSchema(ref)) { // model if (ref.getProperties() != null && !ref.getProperties().isEmpty()) { // has at least one property - return schema; + if (hasSelfReference(openAPI, ref)) { + // it's self referencing so returning itself instead + return schema; + } else { + // TODO we may revise below to return `ref` instead of schema + // which is the last reference to the actual model/object + return schema; + } } else { // free form object (type: object) return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), importMappings); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java index 11a445f7b26..982873161d0 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/DefaultCodegenTest.java @@ -4125,4 +4125,29 @@ public class DefaultCodegenTest { assertEquals(cp.baseName, "SchemaFor201ResponseBodyTextPlain"); assertTrue(cp.isString); } + + @Test + public void testUnalias() { + final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/schema-unalias-test.yml"); + final DefaultCodegen codegen = new DefaultCodegen(); + codegen.setOpenAPI(openAPI); + + Schema requestBodySchema = ModelUtils.getSchemaFromRequestBody( + openAPI.getPaths().get("/thingy/{date}").getPost().getRequestBody()); + Assert.assertEquals(requestBodySchema.get$ref(), "#/components/schemas/updatePetWithForm_request"); + Assert.assertEquals(ModelUtils.getSimpleRef(requestBodySchema.get$ref()), "updatePetWithForm_request"); + Assert.assertNotNull(openAPI.getComponents().getSchemas().get(ModelUtils.getSimpleRef(requestBodySchema.get$ref()))); + + Schema requestBodySchema2 = ModelUtils.unaliasSchema(openAPI, requestBodySchema); + // get$ref is not null as unaliasSchem returns the schema with the last $ref to the actual schema + Assert.assertNotNull(requestBodySchema2.get$ref()); + Assert.assertEquals(requestBodySchema2.get$ref(), "#/components/schemas/updatePetWithForm_request"); + + Schema requestBodySchema3 = ModelUtils.getReferencedSchema(openAPI, requestBodySchema); + CodegenParameter codegenParameter = codegen.fromFormProperty("visitDate", + (Schema) requestBodySchema3.getProperties().get("visitDate"), new HashSet<>()); + + Assert.assertEquals(codegenParameter.defaultValue, "1971-12-19T03:39:57-08:00"); + Assert.assertEquals(codegenParameter.getSchema(), null); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/schema-unalias-test.yml b/modules/openapi-generator/src/test/resources/3_0/schema-unalias-test.yml new file mode 100644 index 00000000000..ea757d8f08f --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/schema-unalias-test.yml @@ -0,0 +1,43 @@ +openapi: 3.0.3 +info: + description: Test schema unalias + types + title: Api Documentation + version: "1.0" +servers: +- url: / +paths: + /thingy/{date}: + post: + description: update with form data + operationId: updatePetWithForm + parameters: + - description: A date path parameter + explode: false + in: path + name: date + required: true + schema: + default: 1969-12-31T16:00:00.000+00:00 + example: 2021-01-01 + format: date + type: string + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/updatePetWithForm_request' + responses: + "405": + description: Invalid input +components: + schemas: + updatePetWithForm_request: + properties: + visitDate: + default: 1971-12-19T03:39:57-08:00 + description: Updated last vist timestamp + format: date-time + type: string + type: object