From 76b7307a6ea8c41c31d8ef0699a608c43e0e06d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bresson?= Date: Thu, 24 May 2018 17:07:41 +0200 Subject: [PATCH] DefaultGenerator: ignore only form param schemas (#74) * Rewrite ModelUtils.getUnusedSchemas(OpenAPI) * Add ModelUtils.getAllUnusedSchemas(OpenAPI) * Add ModelUtils.getSchemasUsedOnlyInFormParam(OpenAPI) --- .../codegen/DefaultGenerator.java | 4 +- .../codegen/utils/ModelUtils.java | 175 ++++++++-- .../codegen/utils/ModelUtilsTest.java | 59 +++- .../2_0/globalProducesConsumesTest.yaml | 39 +++ .../src/test/resources/3_0/unusedSchemas.yaml | 309 ++++++++++++++++++ 5 files changed, 553 insertions(+), 33 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/2_0/globalProducesConsumesTest.yaml create mode 100644 modules/openapi-generator/src/test/resources/3_0/unusedSchemas.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index 8ead6e767841..3eb29c62dc8d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -778,9 +778,9 @@ public List generate() { List files = new ArrayList(); // models - List unusedSchemas = ModelUtils.getUnusedSchemas(openAPI); + List filteredSchemas = ModelUtils.getSchemasUsedOnlyInFormParam(openAPI); List allModels = new ArrayList(); - generateModels(files, allModels, unusedSchemas); + generateModels(files, allModels, filteredSchemas); // apis List allOperations = new ArrayList(); generateApis(files, allOperations, allModels); 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 3232ec96715b..1e2c46d9b42e 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 @@ -39,6 +39,7 @@ import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.media.UUIDSchema; +import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.parser.util.SchemaTypeUtil; @@ -52,6 +53,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; public class ModelUtils { @@ -85,40 +88,113 @@ public static CodegenModel getModelByName(final String name, final Map getAllUsedSchemas(OpenAPI openAPI) { + List allUsedSchemas = new ArrayList(); + visitOpenAPI(openAPI, (s, t) -> { + if(s.get$ref() != null) { + String ref = getSimpleRef(s.get$ref()); + if(!allUsedSchemas.contains(ref)) { + allUsedSchemas.add(ref); + } + } + }); + return allUsedSchemas; + } + + /** + * Return the list of unused schemas in the 'components/schemas' section of an openAPI specification + * @param openAPI specification + * @return schemas a list of unused schemas + */ public static List getUnusedSchemas(OpenAPI openAPI) { List unusedSchemas = new ArrayList(); - // operations - Map paths = openAPI.getPaths(); Map schemas = getSchemas(openAPI); + unusedSchemas.addAll(schemas.keySet()); + + visitOpenAPI(openAPI, (s, t) -> { + if(s.get$ref() != null) { + unusedSchemas.remove(getSimpleRef(s.get$ref())); + } + }); + return unusedSchemas; + } + + /** + * Return the list of schemas in the 'components/schemas' used only in a 'application/x-www-form-urlencoded' or 'multipart/form-data' mime time + * @param openAPI specification + * @return schemas a list of schemas + */ + public static List getSchemasUsedOnlyInFormParam(OpenAPI openAPI) { + List schemasUsedInFormParam = new ArrayList(); + List schemasUsedInOtherCases = new ArrayList(); + + visitOpenAPI(openAPI, (s, t) -> { + if(s.get$ref() != null) { + String ref = getSimpleRef(s.get$ref()); + if ("application/x-www-form-urlencoded".equalsIgnoreCase(t) || + "multipart/form-data".equalsIgnoreCase(t)) { + schemasUsedInFormParam.add(ref); + } else { + schemasUsedInOtherCases.add(ref); + } + } + }); + return schemasUsedInFormParam.stream().filter(n -> !schemasUsedInOtherCases.contains(n)).collect(Collectors.toList()); + } + + /** + * Private method used by several methods ({@link #getAllUsedSchemas(OpenAPI)}, + * {@link #getUnusedSchemas(OpenAPI)}, + * {@link #getSchemasUsedOnlyInFormParam(OpenAPI)}, ...) to traverse all paths of an + * OpenAPI instance and call the visitor functional interface when a schema is found. + * + * @param openAPI specification + * @param visitor functional interface (can be defined as a lambda) called each time a schema is found. + */ + private static void visitOpenAPI(OpenAPI openAPI, OpenAPISchemaVisitor visitor) { + Map paths = openAPI.getPaths(); if (paths != null) { - for (String pathname : paths.keySet()) { - PathItem path = paths.get(pathname); - Map operationMap = path.readOperationsMap(); - if (operationMap != null) { - for (PathItem.HttpMethod method : operationMap.keySet()) { - Operation operation = operationMap.get(method); - RequestBody requestBody = operation.getRequestBody(); - - if (requestBody == null) { - continue; + for (PathItem path : paths.values()) { + List allOperations = path.readOperations(); + if (allOperations != null) { + for (Operation operation : allOperations) { + //Params: + if(operation.getParameters() != null) { + for (Parameter p : operation.getParameters()) { + Parameter parameter = getReferencedParameter(openAPI, p); + if (parameter.getSchema() != null) { + visitor.visit(parameter.getSchema(), null); + } + } } - //LOGGER.info("debugging resolver: " + requestBody.toString()); - if (requestBody.getContent() == null) { - continue; + //RequestBody: + RequestBody requestBody = getReferencedRequestBody(openAPI, operation.getRequestBody()); + if (requestBody != null && requestBody.getContent() != null) { + for (Entry e : requestBody.getContent().entrySet()) { + if (e.getValue().getSchema() != null) { + visitor.visit(e.getValue().getSchema(), e.getKey()); + } + } } - // go through "content" - for (String mimeType : requestBody.getContent().keySet()) { - if ("application/x-www-form-urlencoded".equalsIgnoreCase(mimeType) || - "multipart/form-data".equalsIgnoreCase(mimeType)) { - // remove the schema that's automatically created by the parser - MediaType mediaType = requestBody.getContent().get(mimeType); - if (mediaType.getSchema().get$ref() != null) { - LOGGER.debug("mark schema (form parameters) as unused: " + getSimpleRef(mediaType.getSchema().get$ref())); - unusedSchemas.add(getSimpleRef(mediaType.getSchema().get$ref())); + //Responses: + if(operation.getResponses() != null) { + for (ApiResponse r : operation.getResponses().values()) { + ApiResponse apiResponse = getReferencedApiResponse(openAPI, r); + if (apiResponse != null && apiResponse.getContent() != null) { + for (Entry e : apiResponse.getContent().entrySet()) { + if (e.getValue().getSchema() != null) { + visitor.visit(e.getValue().getSchema(), e.getKey()); + } + } } } } @@ -126,8 +202,12 @@ public static List getUnusedSchemas(OpenAPI openAPI) { } } } + } - return unusedSchemas; + @FunctionalInterface + private static interface OpenAPISchemaVisitor { + + public void visit(Schema schema, String mimeType); } public static String getSimpleRef(String ref) { @@ -344,7 +424,7 @@ public static boolean isEmailSchema(Schema schema) { /** * If a Schema contains a reference to an other Schema with '$ref', returns the referenced Schema or the actual Schema in the other cases. - * @param openAPI + * @param openAPI specification being checked * @param schema potentially containing a '$ref' * @return schema without '$ref' */ @@ -373,7 +453,7 @@ public static Map getSchemas(OpenAPI openAPI) { /** * If a RequestBody contains a reference to an other RequestBody with '$ref', returns the referenced RequestBody or the actual RequestBody in the other cases. - * @param openAPI + * @param openAPI specification being checked * @param requestBody potentially containing a '$ref' * @return requestBody without '$ref' */ @@ -398,7 +478,7 @@ public static RequestBody getRequestBody(OpenAPI openAPI, String name) { /** * If a ApiResponse contains a reference to an other ApiResponse with '$ref', returns the referenced ApiResponse or the actual ApiResponse in the other cases. - * @param openAPI + * @param openAPI specification being checked * @param apiResponse potentially containing a '$ref' * @return apiResponse without '$ref' */ @@ -420,12 +500,46 @@ public static ApiResponse getApiResponse(OpenAPI openAPI, String name) { } return null; } - + /** + * If a Parameter contains a reference to an other Parameter with '$ref', returns the referenced Parameter or the actual Parameter in the other cases. + * @param openAPI specification being checked + * @param parameter potentially containing a '$ref' + * @return parameter without '$ref' + */ + public static Parameter getReferencedParameter(OpenAPI openAPI, Parameter parameter) { + if (parameter != null && StringUtils.isNotEmpty(parameter.get$ref())) { + String name = getSimpleRef(parameter.get$ref()); + return getParameter(openAPI, name); + } + return parameter; + } + + public static Parameter getParameter(OpenAPI openAPI, String name) { + if (name == null) { + return null; + } + + if (openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getRequestBodies() != null) { + return openAPI.getComponents().getParameters().get(name); + } + return null; + } + + /** + * Return the first defined Schema for a RequestBody + * @param requestBody request body of the operation + * @return firstSchema + */ public static Schema getSchemaFromRequestBody(RequestBody requestBody) { return getSchemaFromContent(requestBody.getContent()); } + /** + * Return the first defined Schema for a ApiResponse + * @param response api response of the operation + * @return firstSchema + */ public static Schema getSchemaFromResponse(ApiResponse response) { return getSchemaFromContent(response.getContent()); } @@ -434,6 +548,9 @@ private static Schema getSchemaFromContent(Content content) { if (content == null || content.isEmpty()) { return null; } + if(content.size() > 1) { + LOGGER.warn("Multiple schemas found, returning only the first one"); + } MediaType mediaType = content.values().iterator().next(); return mediaType.getSchema(); } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java index 83fd662f9fca..05cb91e83426 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java @@ -29,9 +29,64 @@ public class ModelUtilsTest { @Test - public void testEnsureNoDuplicateProduces() { - final OpenAPI openAPI = new OpenAPIParser().readLocation("src/test/resources/3_0/ping.yaml", null, new ParseOptions()).getOpenAPI(); + public void testGetAllUsedSchemas() { + final OpenAPI openAPI = new OpenAPIParser().readLocation("src/test/resources/3_0/unusedSchemas.yaml", null, new ParseOptions()).getOpenAPI(); + List allUsedSchemas = ModelUtils.getAllUsedSchemas(openAPI); + Assert.assertEquals(allUsedSchemas.size(), 12); + + Assert.assertTrue(allUsedSchemas.contains("SomeObjShared"), "contains 'SomeObjShared'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj1"), "contains 'UnusedObj1'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj2"), "contains 'SomeObj2'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj3"), "contains 'SomeObj3'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj6"), "contains 'SomeObj6'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj7"), "contains 'SomeObj7'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj8"), "contains 'SomeObj8'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj9A"), "contains 'SomeObj9A'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj9B"), "contains 'SomeObj9B'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj10A"), "contains 'SomeObj10A'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj10B"), "contains 'SomeObj10B'"); + Assert.assertTrue(allUsedSchemas.contains("SomeObj11"), "contains 'SomeObj11'"); + } + + @Test + public void testGetUnusedSchemas() { + final OpenAPI openAPI = new OpenAPIParser().readLocation("src/test/resources/3_0/unusedSchemas.yaml", null, new ParseOptions()).getOpenAPI(); List unusedSchemas = ModelUtils.getUnusedSchemas(openAPI); + Assert.assertEquals(unusedSchemas.size(), 4); + //UnusedObj is not used at all: + Assert.assertTrue(unusedSchemas.contains("UnusedObj1"), "contains 'UnusedObj1'"); + //SomeObjUnused is used in a request body that is not used. + Assert.assertTrue(unusedSchemas.contains("UnusedObj2"), "contains 'UnusedObj2'"); + //SomeObjUnused is used in a response that is not used. + Assert.assertTrue(unusedSchemas.contains("UnusedObj3"), "contains 'UnusedObj3'"); + //SomeObjUnused is used in a parameter that is not used. + Assert.assertTrue(unusedSchemas.contains("UnusedObj4"), "contains 'UnusedObj4'"); + } + + @Test + public void testSchemasUsedOnlyInFormParam() { + final OpenAPI openAPI = new OpenAPIParser().readLocation("src/test/resources/3_0/unusedSchemas.yaml", null, new ParseOptions()).getOpenAPI(); + List unusedSchemas = ModelUtils.getSchemasUsedOnlyInFormParam(openAPI); + Assert.assertEquals(unusedSchemas.size(), 3); + //SomeObj2 is only used in a 'application/x-www-form-urlencoded' request + Assert.assertTrue(unusedSchemas.contains("SomeObj2"), "contains 'SomeObj2'"); + //SomeObj3 is only used in a 'multipart/form-data' request + Assert.assertTrue(unusedSchemas.contains("SomeObj3"), "contains 'SomeObj3'"); + //SomeObj7 is only used in a 'application/x-www-form-urlencoded' request (with referenced request body) + Assert.assertTrue(unusedSchemas.contains("SomeObj7"), "contains 'SomeObj7'"); + } + + @Test + public void testNoComponentsSection() { + final OpenAPI openAPI = new OpenAPIParser().readLocation("src/test/resources/3_0/ping.yaml", null, new ParseOptions()).getOpenAPI(); + List unusedSchemas = ModelUtils.getSchemasUsedOnlyInFormParam(openAPI); + Assert.assertEquals(unusedSchemas.size(), 0); + } + + @Test + public void testGlobalProducesConsumes() { + final OpenAPI openAPI = new OpenAPIParser().readLocation("src/test/resources/2_0/globalProducesConsumesTest.yaml", null, new ParseOptions()).getOpenAPI(); + List unusedSchemas = ModelUtils.getSchemasUsedOnlyInFormParam(openAPI); Assert.assertEquals(unusedSchemas.size(), 0); } } diff --git a/modules/openapi-generator/src/test/resources/2_0/globalProducesConsumesTest.yaml b/modules/openapi-generator/src/test/resources/2_0/globalProducesConsumesTest.yaml new file mode 100644 index 000000000000..c735f5b66e27 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/2_0/globalProducesConsumesTest.yaml @@ -0,0 +1,39 @@ +swagger: '2.0' +info: + title: Test + description: Test API + version: 1.0.0 +host: some.example.com +basePath: /v1 +schemes: + - https + - http +consumes: + - application/json + - application/x-www-form-urlencoded +produces: + - application/json +paths: + /testMe: + post: + tags: + - db + operationId: testMeOp + parameters: + - in: body + name: body + required: false + schema: + $ref: '#/definitions/SomeObject' + responses: + '200': + description: Successful Operation +definitions: + SomeObject: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/unusedSchemas.yaml b/modules/openapi-generator/src/test/resources/3_0/unusedSchemas.yaml new file mode 100644 index 000000000000..1027aec67f20 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unusedSchemas.yaml @@ -0,0 +1,309 @@ +openapi: 3.0.1 +info: + title: Test + description: Test API + version: 1.0.0 +servers: +- url: http://some.example.com/v1 +paths: + /some/p1: + post: + operationId: p1 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SomeObj1' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SomeObj1' + required: false + responses: + 200: + description: Successful Operation + content: {} + /some/p2: + post: + operationId: p2 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SomeObj2' + required: false + responses: + 200: + description: Successful Operation + content: {} + /some/p3: + post: + operationId: p3 + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/SomeObj3' + required: false + responses: + 200: + description: Successful Operation + content: {} + /some/p4: + post: + operationId: p4 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SomeObjShared' + required: false + responses: + 200: + description: Successful Operation + content: {} + /some/p5: + post: + operationId: p5 + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SomeObjShared' + required: false + responses: + 200: + description: Successful Operation + content: {} + /some/p6: + post: + operationId: p6 + requestBody: + $ref: '#/components/requestBodies/Op6RequestBody' + responses: + 200: + description: Successful Operation + content: {} + /some/p7: + post: + operationId: p7 + requestBody: + $ref: '#/components/requestBodies/Op7RequestBody' + responses: + 200: + description: Successful Operation + content: {} + /some/p8: + post: + operationId: p8 + responses: + 200: + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/SomeObj8' + /some/p9: + post: + operationId: p9 + responses: + 200: + $ref: '#/components/responses/Rep9' + /some/p10/{someParam}: + post: + operationId: p10 + parameters: + - name: status + in: query + schema: + $ref: '#/components/schemas/SomeObj10A' + - name: someParam + in: path + description: selected value + required: true + schema: + $ref: '#/components/schemas/SomeObj10B' + responses: + 200: + description: Successful Operation + content: {} + /some/p11: + post: + operationId: p11 + parameters: + - $ref: '#/components/parameters/QueryParam11' + responses: + 200: + description: Successful Operation + content: {} +components: + schemas: + UnusedObj1: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + UnusedObj2: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + UnusedObj3: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + UnusedObj4: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + SomeObj1: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + SomeObj2: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + SomeObj3: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + SomeObj6: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + SomeObj7: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + SomeObj8: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + SomeObj9A: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + SomeObj9B: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + SomeObj10A: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + SomeObj10B: + type: string + enum: + - v1 + - v2 + default: v1 + SomeObj11: + type: string + enum: + - v1 + - v2 + default: v1 + SomeObjShared: + type: object + properties: + p1: + type: string + p2: + type: integer + format: int32 + requestBodies: + UnusedRequestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UnusedObj2' + required: false + Op6RequestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SomeObj6' + required: false + Op7RequestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SomeObj7' + responses: + UnusedResponse: + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/UnusedObj3' + Rep9: + description: Successful Operation + content: + application/json: + schema: + $ref: '#/components/schemas/SomeObj9A' + application/xml: + schema: + $ref: '#/components/schemas/SomeObj9B' + parameters: + UnusedParam: + name: id + in: query + schema: + $ref: '#/components/schemas/UnusedObj4' + QueryParam11: + name: id + in: query + schema: + $ref: '#/components/schemas/SomeObj11' \ No newline at end of file