From 99c9751d2eaa192ca3ae6c1c1e52d46a15a8abbf Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 13 Oct 2025 19:08:16 +0200 Subject: [PATCH 01/16] Use Filter Parser and allow multiple filters --- docs/customization.md | 7 +- .../codegen/OpenAPINormalizer.java | 147 +++++++++++------- .../codegen/OpenAPINormalizerTest.java | 86 +++++----- ...nableKeepOnlyFirstTagInOperation_test.yaml | 14 +- 4 files changed, 162 insertions(+), 92 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index acb45936fd21..5023ab097468 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -645,7 +645,7 @@ java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generat - `FILTER` -The `FILTER` parameter allows selective inclusion of API operations based on specific criteria. It applies the `x-internal: true` property to operations that do **not** match the specified values, preventing them from being generated. +The `FILTER` parameter allows selective inclusion of API operations based on specific criteria. It applies the `x-internal: true` property to operations that do **not** match the specified values, preventing them from being generated. Multiple filters can be separated by a semi-column. ### Available Filters @@ -658,6 +658,9 @@ The `FILTER` parameter allows selective inclusion of API operations based on spe - **`tag`** When set to `tag:person|basic`, operations **not** tagged with `person` or `basic` will be marked as internal (`x-internal: true`), and will not be generated. +- **`path`** + When set to `path:/v1|/v2`, operations on paths **not** starting with `/v1` or with `/v2` will be marked as internal (`x-internal: true`), and will not be generated. + ### Example Usage ```sh @@ -665,7 +668,7 @@ java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generat -g java \ -i modules/openapi-generator/src/test/resources/3_0/petstore.yaml \ -o /tmp/java-okhttp/ \ - --openapi-normalizer FILTER="operationId:addPet|getPetById" + --openapi-normalizer FILTER="operationId:addPet|getPetById ; tag:store" ``` - `SET_CONTAINER_TO_NULLABLE`: When set to `array|set|map` (or just `array`) for example, it will set `nullable` in array, set and map to true. diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index a53d9655a74a..903681c58c0c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -49,7 +49,7 @@ public class OpenAPINormalizer { private TreeSet anyTypeTreeSet = new TreeSet<>(); - protected final Logger LOGGER = LoggerFactory.getLogger(OpenAPINormalizer.class); + protected static final Logger LOGGER = LoggerFactory.getLogger(OpenAPINormalizer.class); Set ruleNames = new TreeSet<>(); Set rulesDefaultToTrue = new TreeSet<>(); @@ -133,10 +133,7 @@ public class OpenAPINormalizer { // when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else final String FILTER = "FILTER"; - HashSet operationIdFilters = new HashSet<>(); - HashSet methodFilters = new HashSet<>(); - - HashSet tagFilters = new HashSet<>(); + Filter filter = new Filter(); // when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else final String SET_CONTAINER_TO_NULLABLE = "SET_CONTAINER_TO_NULLABLE"; @@ -275,29 +272,11 @@ public void processRules(Map inputRules) { if (inputRules.get(FILTER) != null) { rules.put(FILTER, true); - - String[] filterStrs = inputRules.get(FILTER).split(":"); - if (filterStrs.length != 2) { // only support operationId with : at the moment - LOGGER.error("FILTER rule must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3`: {}", inputRules.get(FILTER)); - } else { - if ("operationId".equals(filterStrs[0])) { - operationIdFilters = Arrays.stream(filterStrs[1].split("[|]")) - .filter(Objects::nonNull) - .map(String::trim) - .collect(Collectors.toCollection(HashSet::new)); - } else if ("method".equals(filterStrs[0])) { - methodFilters = Arrays.stream(filterStrs[1].split("[|]")) - .filter(Objects::nonNull) - .map(String::trim) - .collect(Collectors.toCollection(HashSet::new)); - } else if ("tag".equals(filterStrs[0])) { - tagFilters = Arrays.stream(filterStrs[1].split("[|]")) - .filter(Objects::nonNull) - .map(String::trim) - .collect(Collectors.toCollection(HashSet::new)); - } else { - LOGGER.error("FILTER rule must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3`: {}", inputRules.get(FILTER)); - } + String filters = inputRules.get(FILTER); + try { + filter = new Filter(filters); + } catch (Exception e) { + LOGGER.error("FILTER rule must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2` in {}", filters); } } @@ -405,15 +384,11 @@ protected void normalizePaths() { "trace", PathItem::getTrace ); - // Iterates over each HTTP method in methodMap, retrieves the corresponding Operation from the PathItem, - // and marks it as internal (`x-internal`) if the method is not in methodFilters. - methodMap.forEach((method, getter) -> { - Operation operation = getter.apply(path); - if (operation != null && !methodFilters.isEmpty()) { - LOGGER.info("operation `{}` marked internal only (x-internal: `{}`) by the method FILTER", operation.getOperationId(), !methodFilters.contains(method)); - operation.addExtension("x-internal", !methodFilters.contains(method)); - } - }); + if (filter.hasFilter()) { + // Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem, + // and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters. + filter.apply(pathsEntry.getKey(), path, methodMap); + } // Include callback operation as well for (Operation operation : path.readOperations()) { @@ -430,22 +405,6 @@ protected void normalizePaths() { normalizeParameters(path.getParameters()); for (Operation operation : operations) { - if (operationIdFilters.size() > 0) { - if (operationIdFilters.contains(operation.getOperationId())) { - operation.addExtension(X_INTERNAL, false); - } else { - LOGGER.info("operation `{}` marked as internal only (x-internal: true) by the operationId FILTER", operation.getOperationId()); - operation.addExtension(X_INTERNAL, true); - } - } else if (!tagFilters.isEmpty()) { - if (operation.getTags().stream().anyMatch(tagFilters::contains)) { - operation.addExtension(X_INTERNAL, false); - } else { - LOGGER.info("operation `{}` marked as internal only (x-internal: true) by the tag FILTER", operation.getOperationId()); - operation.addExtension(X_INTERNAL, true); - } - } - normalizeOperation(operation); normalizeRequestBody(operation); normalizeParameters(operation.getParameters()); @@ -1349,7 +1308,7 @@ protected Schema processSimplifyOneOfEnum(Schema schema) { * * @param schema Schema to modify * @param subSchemas List of sub-schemas to check - * @param schemaType Type of composed schema ("oneOf" or "anyOf") + * @param composedType Type of composed schema ("oneOf" or "anyOf") * @return Simplified schema */ protected Schema simplifyComposedSchemaWithEnums(Schema schema, List subSchemas, String composedType) { @@ -1818,4 +1777,84 @@ protected Schema processNormalize31Spec(Schema schema, Set visitedSchema } // ===================== end of rules ===================== + + static class Filter { + protected Set operationIdFilters = Collections.emptySet(); + protected Set methodFilters = Collections.emptySet(); + protected Set tagFilters = Collections.emptySet(); + protected Set pathStartingWithFilters = Collections.emptySet(); + + Filter() { + + } + + public Filter(String filters) { + for (String filter : filters.split(";")) { + filter = filter.trim(); + String[] filterStrs = filter.split(":"); + if (filterStrs.length != 2) { // only support operationId with : at the moment + throw new IllegalArgumentException("filter not supported :[" + filter + "]"); + } else { + String filterKey = filterStrs[0].trim(); + String filterValue = filterStrs[1]; + Set parsedFilters = Arrays.stream(filterValue.split("[|]")) + .filter(Objects::nonNull) + .map(String::trim) + .collect(Collectors.toCollection(HashSet::new)); + if ("operationId".equals(filterKey)) { + operationIdFilters = parsedFilters; + } else if ("method".equals(filterKey)) { + methodFilters = parsedFilters; + } else if ("tag".equals(filterKey)) { + tagFilters = parsedFilters; + } else if ("path".equals(filterKey)) { + pathStartingWithFilters = parsedFilters; + } else { + throw new IllegalArgumentException("filter not supported :[" + filter + "]"); + } + } + } + } + + public boolean hasFilter() { + return !operationIdFilters.isEmpty() || !methodFilters.isEmpty() || !tagFilters.isEmpty() || !pathStartingWithFilters.isEmpty (); + } + + public void apply(String path, PathItem pathItem, Map> methodMap) { + methodMap.forEach((method, getter) -> { + Operation operation = getter.apply(pathItem); + if (operation != null) { + boolean found = false; + found |= hasMatch("path", operation, hasPathStarting(path)); + found |= hasMatch("tag", operation, hasTag(operation)); + found |= hasMatch("operationId", operation, hasOperationId(operation)); + found |= hasMatch("method", operation, hasMethod(method)); + operation.addExtension(X_INTERNAL, !found); + } + }); + } + + private boolean hasMatch(String filterName, Operation operation, boolean filterMatched) { + if (filterMatched) { + OpenAPINormalizer.LOGGER.info("operation `{}` marked as internal only (x-internal: true) by the {} FILTER", operation.getOperationId(), filterName); + } + return filterMatched; + } + + private boolean hasPathStarting(String path) { + return pathStartingWithFilters.stream().anyMatch(filter -> path.startsWith(filter)); + } + + private boolean hasTag( Operation operation) { + return operation.getTags() != null && operation.getTags().stream().anyMatch(tagFilters::contains); + } + + private boolean hasOperationId(Operation operation) { + return operationIdFilters.contains(operation.getOperationId()); + } + + private boolean hasMethod(String method) { + return methodFilters.contains(method); + } + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 3ea7f1ce8727..c0bc941af362 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -630,27 +630,9 @@ public void testOperationIdFilter() { OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); openAPINormalizer.normalize(); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions().get(X_INTERNAL), false); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), false); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true); - } - - @Test - public void testOperationIdFilterWithTrim() { - OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml"); - - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions(), null); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null); - - Map options = new HashMap<>(); - options.put("FILTER", "operationId:\n\t\t\t\tdelete|\n\t\tlist"); - OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); - openAPINormalizer.normalize(); - - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions().get(X_INTERNAL), false); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), false); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions().get("x-internal"), false); + assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get("x-internal"), false); + assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get("x-internal"), true); } @Test @@ -670,22 +652,56 @@ public void testFilterWithMethod() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true); } - @Test - public void testFilterWithMethodWithTrim() { - OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml"); - - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions(), null); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null); - Map options = new HashMap<>(); - options.put("FILTER", "method:\n\t\t\t\tget"); - OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); - openAPINormalizer.normalize(); + @Test + public void testFilterParsing() { + OpenAPINormalizer.Filter filter; + + // default + filter = new OpenAPINormalizer.Filter(); + assertFalse(filter.hasFilter()); + + // no filter + filter = new OpenAPINormalizer.Filter(); + assertFalse(filter.hasFilter()); + + // invalid filter + assertThrows(IllegalArgumentException.class, () -> + new OpenAPINormalizer.Filter("operationId:")); + + assertThrows(IllegalArgumentException.class, () -> + new OpenAPINormalizer.Filter("invalid:invalid:")); + + // extra spaces are trimmed + filter = new OpenAPINormalizer.Filter("method:\n\t\t\t\tget"); + assertTrue(filter.hasFilter()); + assertEquals(filter.methodFilters, Set.of("get")); + assertTrue(filter.operationIdFilters.isEmpty()); + assertTrue(filter.tagFilters.isEmpty()); + assertTrue(filter.pathStartingWithFilters.isEmpty()); + + // multiple values separated by pipe + filter = new OpenAPINormalizer.Filter("operationId:\n\t\t\t\tdelete|\n\t\tlist\t"); + assertTrue(filter.hasFilter()); + assertTrue(filter.methodFilters.isEmpty()); + assertEquals(filter.operationIdFilters, Set.of("delete", "list")); + assertTrue(filter.tagFilters.isEmpty()); + assertTrue(filter.pathStartingWithFilters.isEmpty()); + + // multiple filters + filter = new OpenAPINormalizer.Filter("operationId:delete|list;path:/v1"); + assertTrue(filter.hasFilter()); + assertTrue(filter.methodFilters.isEmpty()); + assertEquals(filter.operationIdFilters, Set.of("delete", "list")); + assertTrue(filter.tagFilters.isEmpty()); + assertEquals(filter.pathStartingWithFilters, Set.of("/v1")); + } - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions().get(X_INTERNAL), false); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true); + @Test + public void testMultiFilterParsing() { + OpenAPINormalizer.Filter filter = new OpenAPINormalizer.Filter("operationId: delete| list ; tag : testA |testB "); + assertEquals(filter.operationIdFilters, Set.of("delete", "list")); + assertEquals(filter.tagFilters, Set.of("testA", "testB")); } @Test diff --git a/modules/openapi-generator/src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml b/modules/openapi-generator/src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml index 04abd51fc985..5031ce39440c 100644 --- a/modules/openapi-generator/src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml @@ -7,6 +7,18 @@ info: servers: - url: http://api.example.xyz/v1 paths: + /v1/person: + get: + operationId: list + responses: + '200': + description: OK + /v2/person: + get: + operationId: list + responses: + '200': + description: OK /person/display/{personId}: get: tags: @@ -83,4 +95,4 @@ components: type: object properties: test: - type: string \ No newline at end of file + type: string From 23d660592eaafa354f921d7359e1dceb44e7cdae Mon Sep 17 00:00:00 2001 From: jpfinne Date: Tue, 14 Oct 2025 08:24:44 +0200 Subject: [PATCH 02/16] OpenAPINormalizer fails for invalid FILTER syntax --- .../java/org/openapitools/codegen/OpenAPINormalizer.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 903681c58c0c..72b46f2f01af 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -275,8 +275,11 @@ public void processRules(Map inputRules) { String filters = inputRules.get(FILTER); try { filter = new Filter(filters); - } catch (Exception e) { + } catch (RuntimeException e) { LOGGER.error("FILTER rule must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2` in {}", filters); + // rethrow the exception. This is a breaking change compared to pre 7.16.0 + // Workaround: fix the syntax! + throw e; } } @@ -1792,7 +1795,7 @@ public Filter(String filters) { for (String filter : filters.split(";")) { filter = filter.trim(); String[] filterStrs = filter.split(":"); - if (filterStrs.length != 2) { // only support operationId with : at the moment + if (filterStrs.length != 2) { // only support filter with : at the moment throw new IllegalArgumentException("filter not supported :[" + filter + "]"); } else { String filterKey = filterStrs[0].trim(); From d9f3ce115dec77377db3917caffe9ecf7dba3c35 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Wed, 15 Oct 2025 09:14:32 +0200 Subject: [PATCH 03/16] Fix typo --- docs/customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/customization.md b/docs/customization.md index 5023ab097468..5cc0e972d158 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -645,7 +645,7 @@ java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generat - `FILTER` -The `FILTER` parameter allows selective inclusion of API operations based on specific criteria. It applies the `x-internal: true` property to operations that do **not** match the specified values, preventing them from being generated. Multiple filters can be separated by a semi-column. +The `FILTER` parameter allows selective inclusion of API operations based on specific criteria. It applies the `x-internal: true` property to operations that do **not** match the specified values, preventing them from being generated. Multiple filters can be separated by a semicolon. ### Available Filters From aa3a282c6cc8a5cf3f81c647b61a122669f3f8c4 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Wed, 15 Oct 2025 09:15:23 +0200 Subject: [PATCH 04/16] Use constants for filter keys. Improve exception handling and tests. --- .../codegen/OpenAPINormalizer.java | 32 ++++++++++------- .../codegen/OpenAPINormalizerTest.java | 35 ++++++++----------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 72b46f2f01af..3de4d0c0a2ee 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -276,10 +276,11 @@ public void processRules(Map inputRules) { try { filter = new Filter(filters); } catch (RuntimeException e) { - LOGGER.error("FILTER rule must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2` in {}", filters); - // rethrow the exception. This is a breaking change compared to pre 7.16.0 + String message = String.format("FILTER rule [%s] must be in the form of `%s:name1|name2|name3` or `%s:get|post|put` or `%s:tag1|tag2|tag3` or `%s:/v1|/v2`. Error: %s", + filters, Filter.OPERATION_ID, Filter.METHOD, Filter.TAG, Filter.PATH, e.getMessage()); + // throw an exception. This is a breaking change compared to pre 7.16.0 // Workaround: fix the syntax! - throw e; + throw new IllegalArgumentException(message); } } @@ -1782,6 +1783,10 @@ protected Schema processNormalize31Spec(Schema schema, Set visitedSchema // ===================== end of rules ===================== static class Filter { + public static final String OPERATION_ID = "operationId"; + public static final String METHOD = "method"; + public static final String TAG = "tag"; + public static final String PATH = "path"; protected Set operationIdFilters = Collections.emptySet(); protected Set methodFilters = Collections.emptySet(); protected Set tagFilters = Collections.emptySet(); @@ -1796,7 +1801,7 @@ public Filter(String filters) { filter = filter.trim(); String[] filterStrs = filter.split(":"); if (filterStrs.length != 2) { // only support filter with : at the moment - throw new IllegalArgumentException("filter not supported :[" + filter + "]"); + throw new IllegalArgumentException("filter not supported :[" + filters + "]"); } else { String filterKey = filterStrs[0].trim(); String filterValue = filterStrs[1]; @@ -1804,16 +1809,17 @@ public Filter(String filters) { .filter(Objects::nonNull) .map(String::trim) .collect(Collectors.toCollection(HashSet::new)); - if ("operationId".equals(filterKey)) { + if (OPERATION_ID.equals(filterKey)) { operationIdFilters = parsedFilters; - } else if ("method".equals(filterKey)) { + } else if (METHOD.equals(filterKey)) { + methodFilters = parsedFilters; - } else if ("tag".equals(filterKey)) { + } else if (TAG.equals(filterKey)) { tagFilters = parsedFilters; - } else if ("path".equals(filterKey)) { + } else if (PATH.equals(filterKey)) { pathStartingWithFilters = parsedFilters; } else { - throw new IllegalArgumentException("filter not supported :[" + filter + "]"); + throw new IllegalArgumentException("filter not supported :[" + filters + "]"); } } } @@ -1828,10 +1834,10 @@ public void apply(String path, PathItem pathItem, Map options = new HashMap<>(); - options.put("REMOVE_X_INTERNAL", "true"); + Map options = Map.of("REMOVE_X_INTERNAL", "true"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); openAPINormalizer.normalize(); @@ -625,8 +624,7 @@ public void testOperationIdFilter() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null); - Map options = new HashMap<>(); - options.put("FILTER", "operationId:delete|list"); + Map options = Map.of("FILTER", "operationId:delete|list"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); openAPINormalizer.normalize(); @@ -643,8 +641,7 @@ public void testFilterWithMethod() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null); - Map options = new HashMap<>(); - options.put("FILTER", "method:get"); + Map options = Map.of("FILTER", "method:get"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); openAPINormalizer.normalize(); @@ -712,8 +709,7 @@ public void testFilterWithTag() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null); - Map options = new HashMap<>(); - options.put("FILTER", "tag:basic"); + Map options = Map.of("FILTER", "tag:basic"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); openAPINormalizer.normalize(); @@ -721,24 +717,21 @@ public void testFilterWithTag() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true); } + @Test - public void testFilterWithTagWithTrim() { + public void testFilterInvalidDoesThrow() { OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml"); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions(), null); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null); - - Map options = new HashMap<>(); - options.put("FILTER", "tag:basic"); - OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); - openAPINormalizer.normalize(); - - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions().get(X_INTERNAL), false); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true); + Map options = Map.of("FILTER", "tag ; invalid"); + try { + new OpenAPINormalizer(openAPI, options); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "FILTER rule [tag ; invalid] must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2`. Error: filter not supported :[tag ; invalid]"); + } } + @Test public void testComposedSchemaDoesNotThrow() { OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/composed-schema.yaml"); From cf8992a18bea66cabd69ad635c94bb340610d69c Mon Sep 17 00:00:00 2001 From: jpfinne Date: Wed, 15 Oct 2025 12:58:26 +0200 Subject: [PATCH 05/16] Fix format missing Locale.ROOT --- .../main/java/org/openapitools/codegen/OpenAPINormalizer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 3de4d0c0a2ee..90848f5718cc 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -276,7 +276,7 @@ public void processRules(Map inputRules) { try { filter = new Filter(filters); } catch (RuntimeException e) { - String message = String.format("FILTER rule [%s] must be in the form of `%s:name1|name2|name3` or `%s:get|post|put` or `%s:tag1|tag2|tag3` or `%s:/v1|/v2`. Error: %s", + String message = String.format(Locale.ROOT, "FILTER rule [%s] must be in the form of `%s:name1|name2|name3` or `%s:get|post|put` or `%s:tag1|tag2|tag3` or `%s:/v1|/v2`. Error: %s", filters, Filter.OPERATION_ID, Filter.METHOD, Filter.TAG, Filter.PATH, e.getMessage()); // throw an exception. This is a breaking change compared to pre 7.16.0 // Workaround: fix the syntax! From 4835072a3d74278d40a20d2f587917c7f25500ec Mon Sep 17 00:00:00 2001 From: jpfinne Date: Thu, 16 Oct 2025 10:32:08 +0200 Subject: [PATCH 06/16] Make Filter extensible --- .../codegen/OpenAPINormalizer.java | 134 ++++++++++++++---- .../codegen/OpenAPINormalizerTest.java | 74 +++++++--- ...nableKeepOnlyFirstTagInOperation_test.yaml | 1 + 3 files changed, 166 insertions(+), 43 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 22d597e7ea8b..018fad4f955c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -133,7 +133,6 @@ public class OpenAPINormalizer { // when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else final String FILTER = "FILTER"; - Filter filter = new Filter(); // when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else final String SET_CONTAINER_TO_NULLABLE = "SET_CONTAINER_TO_NULLABLE"; @@ -272,16 +271,7 @@ public void processRules(Map inputRules) { if (inputRules.get(FILTER) != null) { rules.put(FILTER, true); - String filters = inputRules.get(FILTER); - try { - filter = new Filter(filters); - } catch (RuntimeException e) { - String message = String.format(Locale.ROOT, "FILTER rule [%s] must be in the form of `%s:name1|name2|name3` or `%s:get|post|put` or `%s:tag1|tag2|tag3` or `%s:/v1|/v2`. Error: %s", - filters, Filter.OPERATION_ID, Filter.METHOD, Filter.TAG, Filter.PATH, e.getMessage()); - // throw an exception. This is a breaking change compared to pre 7.16.0 - // Workaround: fix the syntax! - throw new IllegalArgumentException(message); - } + // actual parsing is delayed to allow customization of the Filter processing } if (inputRules.get(SET_CONTAINER_TO_NULLABLE) != null) { @@ -327,6 +317,19 @@ public void processRules(Map inputRules) { } } + /** + * Create the filter to process the FILTER normalizer. + * Override this to create a custom filter normalizer. + * + * @param openApi Contract used in the filtering (could be used for customization). + * @param filters full FILTER value + * + * @return a Filter containing the parsed filters. + */ + protected Filter createFilter(OpenAPI openApi, String filters) { + return new Filter(filters); + } + /** * Normalizes the OpenAPI input, which may not perfectly conform to * the specification. @@ -388,7 +391,10 @@ protected void normalizePaths() { "trace", PathItem::getTrace ); - if (filter.hasFilter()) { + if (Boolean.TRUE.equals(getRule(FILTER))) { + String filters = inputRules.get(FILTER); + Filter filter = createFilter(this.openAPI, filters); + filter.parse(); // Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem, // and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters. filter.apply(pathsEntry.getKey(), path, methodMap); @@ -1796,33 +1802,52 @@ protected Schema processNormalize31Spec(Schema schema, Set visitedSchema // ===================== end of rules ===================== - static class Filter { + protected static class Filter { public static final String OPERATION_ID = "operationId"; public static final String METHOD = "method"; public static final String TAG = "tag"; public static final String PATH = "path"; + private final String filters; protected Set operationIdFilters = Collections.emptySet(); protected Set methodFilters = Collections.emptySet(); protected Set tagFilters = Collections.emptySet(); protected Set pathStartingWithFilters = Collections.emptySet(); - Filter() { + protected Filter(String filters) { + this.filters = filters.trim(); + } + /** + * Perform the parsing of the filter string. + * + * @return true if filters need to be processed + */ + public boolean parse() { + if (StringUtils.isEmpty(filters)) { + return false; + } + try { + doParse(); + return hasFilter(); + } catch (RuntimeException e) { + String message = String.format(Locale.ROOT, "FILTER rule [%s] must be in the form of `%s:name1|name2|name3` or `%s:get|post|put` or `%s:tag1|tag2|tag3` or `%s:/v1|/v2`. Error: %s", + filters, Filter.OPERATION_ID, Filter.METHOD, Filter.TAG, Filter.PATH, e.getMessage()); + // throw an exception. This is a breaking change compared to pre 7.16.0 + // Workaround: fix the syntax! + throw new IllegalArgumentException(message); + } } - public Filter(String filters) { + private void doParse() { for (String filter : filters.split(";")) { filter = filter.trim(); String[] filterStrs = filter.split(":"); if (filterStrs.length != 2) { // only support filter with : at the moment - throw new IllegalArgumentException("filter not supported :[" + filters + "]"); + throw new IllegalArgumentException("filter with no value not supported :[" + filter + "]"); } else { String filterKey = filterStrs[0].trim(); String filterValue = filterStrs[1]; - Set parsedFilters = Arrays.stream(filterValue.split("[|]")) - .filter(Objects::nonNull) - .map(String::trim) - .collect(Collectors.toCollection(HashSet::new)); + Set parsedFilters = splitByPipe(filterValue); if (OPERATION_ID.equals(filterKey)) { operationIdFilters = parsedFilters; } else if (METHOD.equals(filterKey)) { @@ -1833,12 +1858,56 @@ public Filter(String filters) { } else if (PATH.equals(filterKey)) { pathStartingWithFilters = parsedFilters; } else { - throw new IllegalArgumentException("filter not supported :[" + filters + "]"); + parse(filterKey, filterValue); } } } } + /** + * Split the filterValue by pipe. + * + * @return the split values. + */ + protected Set splitByPipe(String filterValue) { + return Arrays.stream(filterValue.split("[|]")) + .filter(Objects::nonNull) + .map(String::trim) + .collect(Collectors.toCollection(HashSet::new)); + } + + /** + * Parse non default filters. + * + * Override this method to add custom parsing logic. + * + * By default throws IllegalArgumentException. + * + * @param filterName name of the filter + * @param filterValue value of the filter + */ + protected void parse(String filterName, String filterValue) { + parseFails(filterName, filterValue); + } + + protected void parseFails(String filterName, String filterValue) { + throw new IllegalArgumentException("filter not supported :[" + filterName + ":" + filterValue + "]"); + } + + /** + * Test if the OpenAPI contract match an extra filter. + * + * Override this method to add custom logic. + * + * @param operation Openapi Operation + * @param path Path of the operation + * + * @return true if the operation of path match the filter + */ + protected boolean hasCustomFilterMatch(String path, Operation operation) { + return false; + } + public boolean hasFilter() { return !operationIdFilters.isEmpty() || !methodFilters.isEmpty() || !tagFilters.isEmpty() || !pathStartingWithFilters.isEmpty (); } @@ -1848,22 +1917,32 @@ public void apply(String path, PathItem pathItem, Map path.startsWith(filter)); } @@ -1879,5 +1958,6 @@ private boolean hasOperationId(Operation operation) { private boolean hasMethod(String method) { return methodFilters.contains(method); } + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index e32aec489ccb..78fdc3ca4948 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -17,6 +17,7 @@ package org.openapitools.codegen; import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; import io.swagger.v3.oas.models.media.*; import io.swagger.v3.oas.models.parameters.Parameter; @@ -622,7 +623,6 @@ public void testOperationIdFilter() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions(), null); assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null); Map options = Map.of("FILTER", "operationId:delete|list"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); @@ -639,7 +639,6 @@ public void testFilterWithMethod() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions(), null); assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null); Map options = Map.of("FILTER", "method:get"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); @@ -650,27 +649,29 @@ public void testFilterWithMethod() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true); } + static OpenAPINormalizer.Filter parseFilter(String filters) { + OpenAPINormalizer.Filter filter = new OpenAPINormalizer.Filter(filters); + filter.parse(); + return filter; + } + @Test public void testFilterParsing() { OpenAPINormalizer.Filter filter; - // default - filter = new OpenAPINormalizer.Filter(); - assertFalse(filter.hasFilter()); - // no filter - filter = new OpenAPINormalizer.Filter(); + filter = parseFilter(" "); assertFalse(filter.hasFilter()); // invalid filter assertThrows(IllegalArgumentException.class, () -> - new OpenAPINormalizer.Filter("operationId:")); + parseFilter("operationId:")); assertThrows(IllegalArgumentException.class, () -> - new OpenAPINormalizer.Filter("invalid:invalid:")); + parseFilter("invalid:invalid:")); // extra spaces are trimmed - filter = new OpenAPINormalizer.Filter("method:\n\t\t\t\tget"); + filter = parseFilter("method:\n\t\t\t\tget"); assertTrue(filter.hasFilter()); assertEquals(filter.methodFilters, Set.of("get")); assertTrue(filter.operationIdFilters.isEmpty()); @@ -678,7 +679,7 @@ public void testFilterParsing() { assertTrue(filter.pathStartingWithFilters.isEmpty()); // multiple values separated by pipe - filter = new OpenAPINormalizer.Filter("operationId:\n\t\t\t\tdelete|\n\t\tlist\t"); + filter = parseFilter("operationId:\n\t\t\t\tdelete|\n\t\tlist\t"); assertTrue(filter.hasFilter()); assertTrue(filter.methodFilters.isEmpty()); assertEquals(filter.operationIdFilters, Set.of("delete", "list")); @@ -686,7 +687,7 @@ public void testFilterParsing() { assertTrue(filter.pathStartingWithFilters.isEmpty()); // multiple filters - filter = new OpenAPINormalizer.Filter("operationId:delete|list;path:/v1"); + filter = parseFilter("operationId:delete|list;path:/v1"); assertTrue(filter.hasFilter()); assertTrue(filter.methodFilters.isEmpty()); assertEquals(filter.operationIdFilters, Set.of("delete", "list")); @@ -696,7 +697,7 @@ public void testFilterParsing() { @Test public void testMultiFilterParsing() { - OpenAPINormalizer.Filter filter = new OpenAPINormalizer.Filter("operationId: delete| list ; tag : testA |testB "); + OpenAPINormalizer.Filter filter = parseFilter("operationId: delete| list ; tag : testA |testB "); assertEquals(filter.operationIdFilters, Set.of("delete", "list")); assertEquals(filter.tagFilters, Set.of("testA", "testB")); } @@ -707,7 +708,6 @@ public void testFilterWithTag() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions(), null); assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); - assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null); Map options = Map.of("FILTER", "tag:basic"); OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); @@ -718,16 +718,57 @@ public void testFilterWithTag() { assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true); } + @Test + public void testCustomRoleFilter() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml"); + + Map options = Map.of("FILTER", "role:admin"); + OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options) { + @Override + protected Filter createFilter(OpenAPI openApi, String filters) { + return new CustomRoleFilter(filters); + } + }; + openAPINormalizer.normalize(); + + assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true); + assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), false); + } + + private class CustomRoleFilter extends OpenAPINormalizer.Filter { + private Set filteredRoles; + + public CustomRoleFilter(String filters) { + super(filters); + } + + + @Override + protected void parse(String filterName, String filterValue) { + if ("role".equals(filterName)) { + this.filteredRoles = splitByPipe(filterValue); + } else { + parseFails(filterName, filterValue); + } + } + + @Override + protected boolean hasCustomFilterMatch(String path, Operation operation) { + return operation.getExtensions() != null && filteredRoles.contains(operation.getExtensions().get("x-role")); + } + } + @Test public void testFilterInvalidDoesThrow() { OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml"); Map options = Map.of("FILTER", "tag ; invalid"); try { - new OpenAPINormalizer(openAPI, options); + new OpenAPINormalizer(openAPI, options).normalize(); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { - assertEquals(e.getMessage(), "FILTER rule [tag ; invalid] must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2`. Error: filter not supported :[tag ; invalid]"); + assertEquals(e.getMessage(), "FILTER rule [tag ; invalid] must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2`. Error: filter with no value not supported :[tag]"); } } @@ -1213,4 +1254,5 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { return super.normalizeSchema(schema, visitedSchemas); } } + } diff --git a/modules/openapi-generator/src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml b/modules/openapi-generator/src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml index 5031ce39440c..823504bf6f5e 100644 --- a/modules/openapi-generator/src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml @@ -59,6 +59,7 @@ paths: schema: $ref: "#/components/schemas/Person" put: + x-role: admin tags: - person parameters: From 5078277faea70b175852fb7fe65b962586397b73 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Fri, 17 Oct 2025 13:29:33 +0200 Subject: [PATCH 07/16] Additional unit test for invalid filter --- .../openapitools/codegen/OpenAPINormalizer.java | 14 ++++++++------ .../codegen/OpenAPINormalizerTest.java | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 018fad4f955c..432ffd1bb29c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -394,10 +394,11 @@ protected void normalizePaths() { if (Boolean.TRUE.equals(getRule(FILTER))) { String filters = inputRules.get(FILTER); Filter filter = createFilter(this.openAPI, filters); - filter.parse(); - // Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem, - // and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters. - filter.apply(pathsEntry.getKey(), path, methodMap); + if (filter.parse()) { + // Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem, + // and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters. + filter.apply(pathsEntry.getKey(), path, methodMap); + } } // Include callback operation as well @@ -1812,6 +1813,7 @@ protected static class Filter { protected Set methodFilters = Collections.emptySet(); protected Set tagFilters = Collections.emptySet(); protected Set pathStartingWithFilters = Collections.emptySet(); + private boolean hasFilter; protected Filter(String filters) { this.filters = filters.trim(); @@ -1848,10 +1850,10 @@ private void doParse() { String filterKey = filterStrs[0].trim(); String filterValue = filterStrs[1]; Set parsedFilters = splitByPipe(filterValue); + hasFilter = true; if (OPERATION_ID.equals(filterKey)) { operationIdFilters = parsedFilters; } else if (METHOD.equals(filterKey)) { - methodFilters = parsedFilters; } else if (TAG.equals(filterKey)) { tagFilters = parsedFilters; @@ -1909,7 +1911,7 @@ protected boolean hasCustomFilterMatch(String path, Operation operation) { } public boolean hasFilter() { - return !operationIdFilters.isEmpty() || !methodFilters.isEmpty() || !tagFilters.isEmpty() || !pathStartingWithFilters.isEmpty (); + return hasFilter; } public void apply(String path, PathItem pathItem, Map> methodMap) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 78fdc3ca4948..a2567b78606c 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -743,7 +743,6 @@ public CustomRoleFilter(String filters) { super(filters); } - @Override protected void parse(String filterName, String filterValue) { if ("role".equals(filterName)) { @@ -760,7 +759,7 @@ protected boolean hasCustomFilterMatch(String path, Operation operation) { } @Test - public void testFilterInvalidDoesThrow() { + public void testFilterInvalidSyntaxDoesThrow() { OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml"); Map options = Map.of("FILTER", "tag ; invalid"); @@ -772,6 +771,19 @@ public void testFilterInvalidDoesThrow() { } } + @Test + public void testFilterInvalidFilterDoesThrow() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml"); + + Map options = Map.of("FILTER", "method:get ; unknown:test"); + try { + new OpenAPINormalizer(openAPI, options).normalize(); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + assertEquals(e.getMessage(), "FILTER rule [method:get ; unknown:test] must be in the form of `operationId:name1|name2|name3` or `method:get|post|put` or `tag:tag1|tag2|tag3` or `path:/v1|/v2`. Error: filter not supported :[unknown:test]"); + } + } + @Test public void testComposedSchemaDoesNotThrow() { From b82a63abf6725febea2b32f62402fb4d1fb7f640 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 25 Oct 2025 14:29:48 +0200 Subject: [PATCH 08/16] Implementation of RemoveFilter in OpenaAPINormalizer --- .../codegen/OpenAPINormalizer.java | 598 ++++++++++++++++-- .../codegen/OpenAPINormalizerTest.java | 71 +++ .../3_0/allOf_composition_discriminator.yaml | 9 + .../custom_remove_filter.yaml | 203 ++++++ .../custom_remove_filter_config.yaml | 5 + .../custom_remove_filter_response.yaml | 81 +++ .../inline_x_internal_test.yaml | 24 + .../inline_x_internal_test_config.yaml | 4 + .../inline_x_internal_test_removed.yaml | 14 + .../openapi_normalizer/petstore.yaml | 528 ++++++++++++++++ .../petstore_removeDeprecated.yaml | 427 +++++++++++++ .../petstore_removeDeprecated_config.yaml | 4 + .../petstore_removeTags_all.yaml | 511 +++++++++++++++ .../petstore_removeTags_all_config.yaml | 4 + .../petstore_removeUnused.yaml | 251 ++++++++ ...removeUnused_REMOVE_X_INTERNAL_config.yaml | 6 + .../petstore_removeUnused_config.yaml | 5 + 17 files changed, 2692 insertions(+), 53 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_config.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_response.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_config.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_removed.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_config.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all_config.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_REMOVE_X_INTERNAL_config.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_config.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 432ffd1bb29c..e472e12b10f5 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -17,6 +17,7 @@ package org.openapitools.codegen; +import io.swagger.models.Response; import io.swagger.v3.oas.models.*; import io.swagger.v3.oas.models.callbacks.Callback; import io.swagger.v3.oas.models.headers.Header; @@ -27,6 +28,7 @@ import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.tags.Tag; import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.utils.ModelUtils; import org.slf4j.Logger; @@ -71,7 +73,7 @@ public class OpenAPINormalizer { final String REF_AS_PARENT_IN_ALLOF = "REF_AS_PARENT_IN_ALLOF"; // when set to true, only keep the first tag in operation if there are more than one tag defined. - final String KEEP_ONLY_FIRST_TAG_IN_OPERATION = "KEEP_ONLY_FIRST_TAG_IN_OPERATION"; + static final String KEEP_ONLY_FIRST_TAG_IN_OPERATION = "KEEP_ONLY_FIRST_TAG_IN_OPERATION"; // when set to true, complex composed schemas (a mix of oneOf/anyOf/anyOf and properties) with // oneOf/anyOf containing only `required` and no properties (these are properties inter-dependency rules) @@ -128,8 +130,7 @@ public class OpenAPINormalizer { final String NORMALIZE_31SPEC = "NORMALIZE_31SPEC"; // when set to true, remove x-internal: true from models, operations - final String REMOVE_X_INTERNAL = "REMOVE_X_INTERNAL"; - boolean removeXInternal; + static final String REMOVE_X_INTERNAL = "REMOVE_X_INTERNAL"; // when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else final String FILTER = "FILTER"; @@ -149,6 +150,9 @@ public class OpenAPINormalizer { boolean updateNumberToNullable; boolean updateBooleanToNullable; + final String REMOVE_FILTER = "REMOVE_FILTER"; + RemoveFilter removeFilter = new RemoveFilter(); + // ============= end of rules ============= /** @@ -206,6 +210,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map inputRules) { ruleNames.add(SET_CONTAINER_TO_NULLABLE); ruleNames.add(SET_PRIMITIVE_TYPES_TO_NULLABLE); ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM); + ruleNames.add(REMOVE_FILTER); // rules that are default to true rules.put(SIMPLIFY_ONEOF_ANYOF, true); @@ -315,6 +320,26 @@ public void processRules(Map inputRules) { if (bearerAuthSecuritySchemeName != null) { rules.put(SET_BEARER_AUTH_FOR_NAME, true); } + + String filter = inputRules.get(REMOVE_FILTER); + // for backward compatibility + if (getRule(REMOVE_X_INTERNAL)) { + filter = filter == null? REMOVE_X_INTERNAL : filter + " ; " + REMOVE_X_INTERNAL; + } + // for backward compatibility + if (getRule(KEEP_ONLY_FIRST_TAG_IN_OPERATION)) { + filter = filter == null? KEEP_ONLY_FIRST_TAG_IN_OPERATION : filter + " ; " + KEEP_ONLY_FIRST_TAG_IN_OPERATION; + } + if (filter != null) { + removeFilter = createRemoveFilter(filter); + removeFilter.parse(); + rules.put(REMOVE_FILTER, true); + } + + } + + protected RemoveFilter createRemoveFilter(String filter) { + return new RemoveFilter(filter); } /** @@ -352,6 +377,7 @@ protected void normalize() { normalizeComponentsSecuritySchemes(); normalizeComponentsSchemas(); normalizeComponentsResponses(); + processRemoveItems(); } /** @@ -367,6 +393,161 @@ protected void normalizeInfo() { } } + /** + * Processes the removal of unused items based on the specified options in the {@code removeUnused} configuration. + * This method performs distinct actions to remove various types of unused elements. + * + * It evaluates each specific removal option. If a particular option is enabled, the associated processing + * sub-method is invoked to perform the cleanup operation. The following operations may be performed: + * + * - Removal of internal operations (via {@code processRemoveInternal()}). + * - Removal of unused schemas (via {@code processRemoveUnusedSchemas()}). + * - Removal of internal schemas (via {@code processRemoveInternalSchemas()}). + * - Removal of unused tags (via {@code processRemoveUnusedTags()}). + * + * Each sub-method is specifically tasked with cleaning its respective category of unused items. + * This modular approach ensures that only operations matching enabled options are executed. + * + * This method is designed to facilitate cleanup operations for unused elements, + * improving overall efficiency and maintainability by reducing unnecessary elements. + */ + + protected void processRemoveItems() { + if (!removeFilter.hasFilter) { + return; + } + processRemoveEmptyPath(); + processRemoveUnusedRequestBodies(); + processRemoveUnusedResponses(); + processRemoveUnusedParameters(); + processRemoveSchemas(); + if (removeFilter.unusedSchemas) { + processRemoveUnusedSchemas(); + } + processRemoveUnusedTags(); + } + + /** + * remove schemas not used anyware + */ + protected void processRemoveUnusedSchemas() { + List unusedSchemas = ModelUtils.getUnusedSchemas(openAPI); + if (!unusedSchemas.isEmpty()) { + openAPI.getComponents().getSchemas().keySet().removeAll(unusedSchemas); + } + } + + /** + * remove paths with no operations + */ + protected void processRemoveEmptyPath() { + Paths paths = openAPI.getPaths(); + if (paths != null) { + // remove empty pathItems + paths.keySet().removeIf(path -> paths.get(path).readOperations().isEmpty()); + } + } + + protected void processRemoveUnusedRequestBodies() { + if (removeFilter.unusedRequestBodies) { + if (openAPI.getComponents() != null && openAPI.getComponents().getRequestBodies() != null) { + Set usedRequestBodies = openAPI.getPaths().values().stream() + .flatMap(pathItem -> pathItem.readOperations().stream()) + .map(Operation::getRequestBody) + .filter(Objects::nonNull) + .map(RequestBody::get$ref) + .filter(Objects::nonNull) + .map(ModelUtils::getSimpleRef) + .collect(Collectors.toSet()); + + openAPI.getComponents().getRequestBodies().keySet().removeIf(ref -> !usedRequestBodies.contains(ref)); + if (openAPI.getComponents().getRequestBodies().isEmpty()) { + openAPI.getComponents().setRequestBodies(null); + } + } + } + } + + protected void processRemoveUnusedResponses() { + if (removeFilter.unusedResponses) { + if (openAPI.getComponents() != null && openAPI.getComponents().getResponses() != null) { + Set unusedResponses = openAPI.getPaths().values().stream() + .flatMap(pathItem -> pathItem.readOperations().stream()) + .map(Operation::getResponses) + .filter(Objects::nonNull) + .map(ApiResponses::values) + .flatMap(responses-> responses.stream()) + .filter(Objects::nonNull) + .map(ApiResponse::get$ref) + .filter(Objects::nonNull) + .map(ModelUtils::getSimpleRef) + .collect(Collectors.toSet()); + + openAPI.getComponents().getResponses().keySet().removeIf(ref -> !unusedResponses.contains(ref)); + if (openAPI.getComponents().getResponses().isEmpty()) { + openAPI.getComponents().setResponses(null); + } + } + } + } + + protected void processRemoveUnusedParameters() { + if (removeFilter.unusedParameters) { + if (openAPI.getComponents() != null && openAPI.getComponents().getParameters() != null) { + Set usedParameters = openAPI.getPaths().values().stream() + .flatMap(pathItem -> pathItem.readOperations().stream()) + .map(Operation::getParameters) + .filter(Objects::nonNull) + .flatMap(List::stream) + .map(Parameter::get$ref) + .filter(Objects::nonNull) + .map(ModelUtils::getSimpleRef) + .collect(Collectors.toSet()); + + openAPI.getComponents().getParameters().keySet().removeIf(ref -> !usedParameters.contains(ref)); + if (openAPI.getComponents().getParameters().isEmpty()) { + openAPI.getComponents().setParameters(null); + } + } + } + } + + /** + * remove the configured schemas + *

+ * for example: marked as x-internal=true) + */ + protected void processRemoveSchemas() { + Map schemas = Optional.ofNullable(openAPI.getComponents()).map(Components::getSchemas).orElse(null); + if (schemas != null) { + schemas.values().removeIf(schema -> removeFilter.matchSchema(schema)); + } + } + + /** + * Processes the removal of unused tags. + *

+ * This method identifies and cleans up tags that are no longer used or referenced. + */ + protected void processRemoveUnusedTags() { + Paths paths = openAPI.getPaths(); + if (paths != null) { + Set usedTags = paths.values().stream() + .map(PathItem::readOperations) + .flatMap(List::stream) + .map(Operation::getTags) + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(Collectors.toSet()); + if (openAPI.getTags() != null) { + openAPI.getTags().removeIf(tag -> removeFilter.matchTag(tag, usedTags)); + if (openAPI.getTags().isEmpty()) { + openAPI.setTags(null); + } + } + } + } + /** * Normalizes inline models in Paths */ @@ -413,12 +594,26 @@ protected void normalizePaths() { } // normalize PathItem common parameters - normalizeParameters(path.getParameters()); + normalizeParameters(pathsEntry.getKey(), path, null, path.getParameters()); + if (path.getParameters() != null && path.getParameters().isEmpty()) { + path.setParameters(null); + } + if (removeFilter.hasFilter()) { + Map httpMethodOperationMap = path.readOperationsMap(); + for (Map.Entry operationEntry: httpMethodOperationMap.entrySet()) { + if (removeFilter.matchOperation(pathsEntry.getKey(), operationEntry.getKey(), operationEntry.getValue())) { + path.operation(operationEntry.getKey(), null); + } + } + } for (Operation operation : operations) { normalizeOperation(operation); normalizeRequestBody(operation); - normalizeParameters(operation.getParameters()); + normalizeParameters(pathsEntry.getKey(), path, operation, operation.getParameters()); + if (operation.getParameters() != null && operation.getParameters().isEmpty()) { + operation.setParameters(null); + } normalizeResponses(operation); } } @@ -431,7 +626,9 @@ protected void normalizePaths() { */ protected void normalizeOperation(Operation operation) { processRemoveXInternalFromOperation(operation); + processRemoveVendorExtensions(operation); + processRemoveTagsInOperation(operation); processKeepOnlyFirstTagInOperation(operation); processSetTagsForAllOperations(operation); @@ -491,6 +688,23 @@ protected void normalizeRequestBody(Operation operation) { normalizeContent(requestBody.getContent()); } + protected void normalizeParameters(String path, PathItem pathItem, Operation operation, List parameters) { + if (parameters == null) { + return; + } + if (removeFilter.hasFilter()) { + for (Iterator it = parameters.iterator(); it.hasNext(); ) { + Parameter parameter = it.next(); + if (operation == null && removeFilter.matchParameter(path, pathItem, parameter)) { + it.remove(); + } else if (removeFilter.matchParameter(path, pathItem, operation, parameter)) { + it.remove(); + } + } + } + normalizeParameters(parameters); + } + /** * Normalizes schemas in parameters * @@ -604,11 +818,13 @@ protected void normalizeComponentsSchemas() { if (schema == null) { LOGGER.warn("{} not found in openapi/components/schemas.", schemaName); } else { - // remove x-internal if needed - if (schema.getExtensions() != null && getRule(REMOVE_X_INTERNAL)) { - if (Boolean.parseBoolean(String.valueOf(schema.getExtensions().get(X_INTERNAL)))) { - schema.getExtensions().remove(X_INTERNAL); - } + if (removeFilter.hasFilter && schema.getExtensions() != null && schema.getExtensions().size() > 0) { + Set extensionKeysToRemove = ((Map)schema.getExtensions()) + .entrySet().stream() + .filter(entry -> removeFilter.matchVendorExtension(schema, entry.getKey(), entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + extensionKeysToRemove.forEach(key -> schema.getExtensions().remove(key)); } // auto fix self reference schema to avoid stack overflow @@ -620,6 +836,18 @@ protected void normalizeComponentsSchemas() { } } + public static boolean isInternal(Map extensions) { + if (extensions != null) { + Object xInternalValue = extensions.get(X_INTERNAL); + if (xInternalValue instanceof Boolean) { + return (Boolean) xInternalValue; + } else if (xInternalValue instanceof String) { + return Boolean.parseBoolean((String) xInternalValue); + } + } + return false; + } + /** * Normalizes schemas in component's responses. */ @@ -823,27 +1051,48 @@ protected void normalizeProperties(Map properties, Set v if (properties == null) { return; } + if (this.removeFilter.hasFilter()) { + Set propertiesToRemove = properties.entrySet().stream() + .filter(entry -> this.removeFilter.matchInlineProperties(entry.getValue(), entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + if (!properties.isEmpty()) { + propertiesToRemove.forEach(p -> properties.remove(p) ); + } + } for (Map.Entry propertiesEntry : properties.entrySet()) { Schema property = propertiesEntry.getValue(); - + // remove x-internal if needed (same logic as normalizeComponentsSchemas) - if (property.getExtensions() != null && getRule(REMOVE_X_INTERNAL)) { - Object xInternalValue = property.getExtensions().get(X_INTERNAL); - boolean isInternal = false; - if (xInternalValue instanceof Boolean) { - isInternal = (Boolean) xInternalValue; - } else if (xInternalValue instanceof String) { - isInternal = Boolean.parseBoolean((String) xInternalValue); - } - if (isInternal) { - property.getExtensions().remove(X_INTERNAL); - } + if (property.getExtensions() != null) { + processRemoveVendorExtensions(property, property.getExtensions()); } Schema newProperty = normalizeSchema(property, new HashSet<>()); propertiesEntry.setValue(newProperty); } } + protected void processRemoveVendorExtensions(Schema property, Map extensions) { + if (removeFilter.hasFilter()) { + Set extensionKeysToRemove = extensions + .entrySet().stream() + .filter(entry -> removeFilter.matchVendorExtension(property, entry.getKey(), entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + extensions.keySet().removeIf(key -> extensionKeysToRemove.contains(key)); + } + } + + protected void processRemoveVendorExtensions(Operation operation) { + if (operation.getExtensions() != null) { + Set extensionKeysToRemove = operation.getExtensions().entrySet().stream() + .filter(entry -> removeFilter.matchVendorExtension(operation, entry.getKey(), entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + operation.getExtensions().keySet().removeIf(key -> extensionKeysToRemove.contains(key)); + } + } + protected void refactorAllOfWithMetadataOnlySchemas(Schema schema) { if (schema.getAllOf() == null) { return; @@ -1090,17 +1339,28 @@ protected void processUseAllOfRefAsParent(Schema schema) { * * @param operation Operation */ + @Deprecated protected void processRemoveXInternalFromOperation(Operation operation) { - if (!getRule(REMOVE_X_INTERNAL)) { - return; - } - - if (operation.getExtensions() == null) { - return; - } + } - if (Boolean.parseBoolean(String.valueOf(operation.getExtensions().get(X_INTERNAL)))) { - operation.getExtensions().remove(X_INTERNAL); + /** + * Remove tag if requested. + * + * @param operation Operation + */ + protected void processRemoveTagsInOperation(Operation operation) { + if (removeFilter.hasFilter && operation.getTags() != null) { + List tags = operation.getTags(); + Set names = new HashSet<>(); + for (int i=0; i < tags.size(); i++) { + if (removeFilter.matchTag(operation, i, tags.get(i))) { + names.add(tags.get(i)); + } + } + operation.getTags().removeIf(tag -> names.contains(tag)); + if (operation.getTags().isEmpty()) { + operation.setTags(null); + } } } @@ -1110,17 +1370,9 @@ protected void processRemoveXInternalFromOperation(Operation operation) { * * @param operation Operation */ + @Deprecated protected void processKeepOnlyFirstTagInOperation(Operation operation) { - if (!getRule(KEEP_ONLY_FIRST_TAG_IN_OPERATION)) { - return; - } - - if (operation.getTags() != null && !operation.getTags().isEmpty() && operation.getTags().size() > 1) { - // has more than 1 tag - String firstTag = operation.getTags().get(0); - operation.setTags(null); - operation.addTagsItem(firstTag); - } + // kept for backward compatibility } /** @@ -1866,18 +2118,6 @@ private void doParse() { } } - /** - * Split the filterValue by pipe. - * - * @return the split values. - */ - protected Set splitByPipe(String filterValue) { - return Arrays.stream(filterValue.split("[|]")) - .filter(Objects::nonNull) - .map(String::trim) - .collect(Collectors.toCollection(HashSet::new)); - } - /** * Parse non default filters. * @@ -1896,6 +2136,11 @@ protected void parseFails(String filterName, String filterValue) { throw new IllegalArgumentException("filter not supported :[" + filterName + ":" + filterValue + "]"); } + protected Set splitByPipe(String filterValue) { + return OpenAPINormalizer.splitByPipe(filterValue); + } + + /** * Test if the OpenAPI contract match an extra filter. * @@ -1962,4 +2207,251 @@ private boolean hasMethod(String method) { } } + + public static class RemoveFilter { + private final String filters; + private boolean hasFilter; + + boolean removeXInternal; + boolean removeAllVendorExtensions; + Set removeExtensions = Collections.emptySet(); + + boolean internalOperations; + boolean internalSchemas; + boolean internalParameters; + boolean internalProperties; + + boolean unusedSchemas; + boolean unusedParameters; + boolean unusedTags; + boolean unusedRequestBodies; + boolean unusedResponses; + + boolean keepOnlyFirstTagInOperation; + boolean removeAllTags; + Set tags = Collections.emptySet(); + private boolean deprecated; + + RemoveFilter() { + this.filters = null; + } + + protected RemoveFilter(String filters) { + this.filters = filters.trim(); + } + + /** + * Perform the parsing of the filter string. + * + * @return true if filters need to be processed + */ + public boolean parse() { + if (StringUtils.isEmpty(filters)) { + return false; + } + try { + doParse(); + return hasFilter(); + } catch (RuntimeException e) { + String message = String.format(Locale.ROOT, "FILTER rule [%s] `. Error: %s", + filters, e.getMessage()); + // throw an exception. This is a breaking change compared to pre 7.16.0 + // Workaround: fix the syntax! + throw new IllegalArgumentException(message); + } + } + + public boolean hasFilter() { + return hasFilter; + } + + private void doParse() { + if ("true".equals(filters)) { + allTrue(); + return; + } + + for (String filter : filters.split(";")) { + filter = filter.trim(); + String[] filterStrs = filter.split(":"); + if (filterStrs.length==1) { + filterStrs = new String[]{filterStrs[0], "true"}; + } + hasFilter = true; + String filterKey = filterStrs[0].trim(); + String filterValue = filterStrs[1]; + Set set = splitByPipe(filterValue); + boolean isTrue = Boolean.valueOf(filterValue); + boolean isBoolean = isTrue || "false".equals(filterValue); + if ("internal".equals(filterKey)) { + if (isBoolean) { + internalOperations = isTrue; + internalSchemas = isTrue; + internalProperties = isTrue; + internalParameters = isTrue; + } else { + internalOperations = set.contains("operations"); + internalSchemas = set.contains("schemas"); + internalProperties = set.contains("properties"); + internalParameters = set.contains("parameters"); + } + } else if ("deprecated".equals(filterKey)) { + this.deprecated = isTrue; + } else if ("unused".equals(filterKey)) { + if (isBoolean) { + unusedSchemas = isTrue; + unusedTags = isTrue; + unusedRequestBodies = isTrue; + unusedResponses = isTrue; + unusedParameters = isTrue; + } else { + unusedSchemas = set.contains("schemas"); + unusedTags = set.contains("tags"); + unusedRequestBodies = set.contains("requestBodies"); + unusedResponses = set.contains("responses"); + unusedParameters = set.contains("parameters"); + } + } else if (REMOVE_X_INTERNAL.equals(filterKey)) { + removeXInternal = isTrue; + } else if ("vendorExtensions".equals(filterKey)) { + if (isBoolean) { + removeXInternal = isTrue; + removeAllVendorExtensions = isTrue; + } else { + removeExtensions = set; + } + } else if (KEEP_ONLY_FIRST_TAG_IN_OPERATION.equals(filterKey)) { + keepOnlyFirstTagInOperation = isTrue; + } else if ("tags".equals(filterKey)) { + if ("keepOnlyFirstTag".equals(filterValue)) { + keepOnlyFirstTagInOperation = true; + } else { + if (isBoolean) { + removeAllTags = isTrue; + } else { + tags = set; + } + } + } else { + parse(filterKey, filterValue); + } + } + } + + protected void parse(String filterName, String filterValue) { + parseFails(filterName, filterValue); + } + + protected void parseFails(String filterName, String filterValue) { + throw new IllegalArgumentException("filter not supported :[" + filterName + ":" + filterValue + "]"); + } + + private void allTrue() { + hasFilter = true; + + internalOperations = true; + internalSchemas = true; + internalProperties = true; + internalParameters = true; + + unusedSchemas = true; + unusedParameters = true; + unusedTags = true; + unusedRequestBodies = true; + } + + protected boolean matchTag(Operation operation, int index, String tag) { + if (removeAllTags) { + return true; + } + if (keepOnlyFirstTagInOperation) { + return index > 0; + } + return tags.contains(tag); + } + + protected boolean matchVendorExtension(Schema schema, String key, Object value) { + return matchVendorExtension(key); + } + + protected boolean matchVendorExtension(Operation operation, String key, Object value) { + return matchVendorExtension(key); + } + + protected boolean matchOperation(String path, PathItem.HttpMethod method, Operation operation) { + if (internalOperations && isInternal(operation.getExtensions())) { + return true; + } + if (deprecated && Boolean.TRUE.equals(operation.getDeprecated())) { + return true; + } + return false; + } + + protected boolean matchInlineProperties(Schema schema, Schema property) { + if (internalProperties && isInternal(property.getExtensions())) { + return true; + } + if (deprecated && Boolean.TRUE.equals(property.getDeprecated())) { + return true; + } + return false; + } + + protected boolean matchSchema(Schema schema) { + if (internalSchemas && isInternal(schema.getExtensions())) { + return true; + } + if (deprecated && Boolean.TRUE.equals(schema.getDeprecated())) { + return true; + } + return false; + } + + protected boolean matchTag(Tag tag, Set usedTags) { + return unusedTags && !usedTags.contains(tag.getName()); + } + + protected boolean matchParameter(String path, PathItem pathItem, Parameter parameter) { + return matchParameter(parameter); + } + + protected boolean matchParameter(String path, PathItem pathItem, Operation operation, Parameter parameter) { + return matchParameter(parameter); + } + + protected boolean matchVendorExtension(String key) { + if (removeAllVendorExtensions) { + return true; + } + // remove x-internal if needed + if (removeXInternal && X_INTERNAL.equals(key)) { + return true; + } + + return removeExtensions.contains(key); + } + + protected boolean matchParameter(Parameter parameter) { + if (internalParameters && isInternal(parameter.getExtensions())) { + return true; + } + if (deprecated && Boolean.TRUE.equals(parameter.getDeprecated())) { + return true; + } + return false; + } + } + + /** + * Split the filterValue by pipe. + * + * @return the split values. + */ + public static Set splitByPipe(String filterValue) { + return Arrays.stream(filterValue.split("[|]")) + .filter(Objects::nonNull) + .map(String::trim) + .collect(Collectors.toCollection(HashSet::new)); + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index a2567b78606c..5b865d52aa6b 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -16,6 +16,8 @@ package org.openapitools.codegen; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.PathItem; @@ -23,9 +25,15 @@ import io.swagger.v3.oas.models.parameters.Parameter; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.security.SecurityScheme; +import lombok.Data; +import org.openapitools.codegen.serializer.SerializerUtils; import org.openapitools.codegen.utils.ModelUtils; +import org.opentest4j.AssertionFailedError; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import java.io.*; +import java.net.URL; import java.util.*; import static org.testng.Assert.*; @@ -1267,4 +1275,67 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { } } + /** + * get a list of all files matching openapi_normalizer/*_config.yaml. + */ + @DataProvider(name="testConfigs") + public Object[][] getTestConfigs() { + URL path = getClass().getClassLoader().getResource("openapi_normalizer"); + File[] files = new File(path.getFile()) + .listFiles(file -> file.getName().endsWith("_config.yaml")); + return Arrays.stream(files) + .map(file -> new Object[]{"src/test/resources/openapi_normalizer/" +file.getName()}) + .toArray(Object[][]::new); + } + + @Test(dataProvider = "testConfigs") + public void executeAllTests(String specPath) { + OpenapiNormalizerTestConfig config = OpenapiNormalizerTestConfig.fromFile(specPath); + OpenAPI openAPI = TestUtils.parseSpec(config.inputSpec); + OpenAPINormalizer openAPINormalizer = OpenAPINormalizer.createNormalizer(openAPI, config.inputRules); + openAPINormalizer.normalize(); + String expected = SerializerUtils.toYamlString(TestUtils.parseSpec(config.normalizedSpec)); + String after = SerializerUtils.toYamlString(openAPI); + if (!expected.equals(after)) { + throw new AssertionFailedError("Unexpected normalized result for\n" + config, expected, after); + } + } + + /** + * Custom remove filter used by custom_remove_filter_config.yaml + */ + public static class CustomRemoveFilter extends OpenAPINormalizer { + public CustomRemoveFilter(OpenAPI openAPI, Map inputRules) { + super(openAPI, inputRules); + } + + @Override + protected RemoveFilter createRemoveFilter(String filter) { + return new RemoveFilter(filter) { + protected boolean matchOperation(String path, PathItem.HttpMethod method, Operation operation) { + return operation.getExtensions() != null && "admin".equals(operation.getExtensions().get("x-role")); + } + }; + } + } + + /** + * Normalizer configuration in yaml files. + */ + @Data + static class OpenapiNormalizerTestConfig { + Map inputRules; + String inputSpec; + String normalizedSpec; + + static OpenapiNormalizerTestConfig fromFile(String specPath) { + ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory()); + try (InputStream inputStream = new FileInputStream(new File(specPath))) { + return objectMapper.readValue(inputStream, OpenapiNormalizerTestConfig.class); + } catch (IOException e) { + throw new RuntimeException("Unable to read from " + specPath, e); + } + } + } + } diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml index efeaa5fd02e9..c08f897e791d 100644 --- a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml @@ -30,6 +30,15 @@ paths: application/json: schema: $ref: '#/components/schemas/MyPets' + /a: + get: + responses: + '200': + description: desc + content: + application/json: + schema: + $ref: '#/components/schemas/A' components: schemas: Pet: diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter.yaml new file mode 100644 index 000000000000..a57da8a466bd --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter.yaml @@ -0,0 +1,203 @@ +openapi: 3.0.0 +info: + version: 1.0.0 +servers: + - url: http://petstore.swagger.io/v2 +paths: + /pet: + post: + x-role: admin + tags: + - pet + operationId: addPet + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + requestBody: + $ref: '#/components/requestBodies/Pet' + '/pet/{petId}': + get: + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' +components: + responses: + Order: + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + Order: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + schemas: + Order: + title: Pet Order + description: An order for a pets from the pet store + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + Category: + title: Pet category + description: A category for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + User: + title: a User + description: A User who is purchasing from the pet store + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + Tag: + title: Pet Tag + description: A tag for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + Pet: + title: a Pet + description: A pet for sale in the pet store + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + items: + type: string + tags: + type: array + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + deprecated: true + enum: + - available + - pending + - sold + ApiResponse: + title: An uploaded response + description: Describes the result of uploading an image resource + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + parameters: + orderId: + name: orderId + in: path + required: true + schema: + type: string + offset: + in: query + name: offset + required: false + x-internal: true + deprecated: true + schema: + type: integer diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_config.yaml new file mode 100644 index 000000000000..3e8641f257ac --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_config.yaml @@ -0,0 +1,5 @@ +inputSpec: src/test/resources/openapi_normalizer/custom_remove_filter.yaml +normalizedSpec: src/test/resources/openapi_normalizer/custom_remove_filter_response.yaml +inputRules: + NORMALIZER_CLASS: org.openapitools.codegen.OpenAPINormalizerTest$CustomRemoveFilter + REMOVE_FILTER: 'unused' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_response.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_response.yaml new file mode 100644 index 000000000000..bcf2ad30098e --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_response.yaml @@ -0,0 +1,81 @@ +openapi: 3.0.0 +info: + version: 1.0.0 +servers: + - url: http://petstore.swagger.io/v2 +paths: + /pet/{petId}: + get: + operationId: getPetById + parameters: + - description: ID of pet to return + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation +components: + schemas: + Category: + description: A category for a pet + properties: + id: + format: int64 + type: integer + name: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + title: Pet category + type: object + Tag: + description: A tag for a pet + properties: + id: + format: int64 + type: integer + name: + type: string + title: Pet Tag + type: object + Pet: + description: A pet for sale in the pet store + properties: + id: + format: int64 + type: integer + category: + $ref: "#/components/schemas/Category" + name: + example: doggie + type: string + photoUrls: + items: + type: string + type: array + tags: + items: + $ref: "#/components/schemas/Tag" + type: array + status: + deprecated: true + description: pet status in the store + enum: + - available + - pending + - sold + type: string + required: + - name + - photoUrls + title: a Pet + type: object diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test.yaml new file mode 100644 index 000000000000..d5a07461f1d5 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test.yaml @@ -0,0 +1,24 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Test inline x-internal +components: + schemas: + ParentSchema: + description: Schema with inline x-internal property + type: object + properties: + normalProperty: + type: string + description: A normal property without x-internal + inlineXInternalProperty: + x-internal: true + description: Inline object property marked as x-internal + type: object + properties: + nestedField: + type: string + description: A field inside the inline x-internal object + nestedNumber: + type: integer + description: Another field inside the inline object diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_config.yaml new file mode 100644 index 000000000000..5586f3e5f920 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_config.yaml @@ -0,0 +1,4 @@ +inputSpec: src/test/resources/openapi_normalizer/inline_x_internal_test.yaml +normalizedSpec: src/test/resources/openapi_normalizer/inline_x_internal_test_removed.yaml +inputRules: + REMOVE_FILTER: 'internal: properties' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_removed.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_removed.yaml new file mode 100644 index 000000000000..f5365ec954c6 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_removed.yaml @@ -0,0 +1,14 @@ +openapi: 3.0.0 +info: + version: 1.0.0 + title: Test inline x-internal +components: + schemas: + ParentSchema: + description: Schema with inline x-internal property + type: object + properties: + normalProperty: + type: string + description: A normal property without x-internal + diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml new file mode 100644 index 000000000000..cb4ba40598cc --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml @@ -0,0 +1,528 @@ +openapi: 3.0.0 +info: + description: >- + This is a sample server Petstore server. + version: 1.0.0 +servers: + - url: http://petstore.swagger.io/v2 +tags: + - name: pet + description: Everything about your Pets + - name: store + description: Access to Petstore orders + - name: user + description: Operations about user +paths: + /pet: + post: + tags: + - pet + operationId: addPet + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '405': + description: Invalid input + requestBody: + $ref: '#/components/requestBodies/Pet' + put: + tags: + - pet + operationId: updatePet + externalDocs: + url: "http://petstore.swagger.io/v2/doc/updatePet" + description: "API documentation for the updatePet operation" + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + requestBody: + $ref: '#/components/requestBodies/Pet' + /pet/findByStatus: + parameters: + - in: header + name: role + required: false + deprecated: true + x-internal: true + schema: + type: string + get: + tags: + - pet + operationId: findPetsByStatus + deprecated: true + parameters: + - in: query + name: limit + required: false + deprecated: true + x-internal: true + schema: + type: integer + - name: status + in: query + description: Status values that need to be considered for filter + required: true + style: form + explode: false + deprecated: true + schema: + type: array + items: + type: string + enum: + - available + - pending + - sold + default: available + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid status value + /pet/findByTags: + get: + tags: + - pet + operationId: findPetsByTags + parameters: + - name: tags + in: query + description: Tags to filter by + required: true + style: form + explode: false + schema: + type: array + items: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid tag value + deprecated: true + '/pet/{petId}': + get: + tags: + - pet + operationId: getPetById + parameters: + - name: petId + in: path + description: ID of pet to return + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + '400': + description: Invalid ID supplied + '404': + description: Pet not found + post: + tags: + - pet + operationId: updatePetWithForm + parameters: + - name: petId + in: path + description: ID of pet that needs to be updated + required: true + schema: + type: integer + format: int64 + responses: + '405': + description: Invalid input + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + delete: + tags: + - pet + summary: Deletes a pet + description: '' + operationId: deletePet + parameters: + - name: api_key + in: header + required: false + schema: + type: string + - name: petId + in: path + description: Pet id to delete + required: true + schema: + type: integer + format: int64 + responses: + '400': + description: Invalid pet value + /store/inventory: + get: + deprecated: true + tags: + - store + operationId: getInventory + responses: + '200': + description: successful operation + content: + application/json: + schema: + type: object + additionalProperties: + type: integer + format: int32 + /store/order: + post: + tags: + - store + operationId: placeOrder + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + '400': + description: Invalid Order + requestBody: + $ref: '#/components/requestBodies/Order' + '/store/order/{orderId}': + get: + tags: + - store + operationId: getOrderById + parameters: + - $ref: '#/components/parameters/orderId' + responses: + '200': + $ref: '#/components/responses/Order' + + delete: + tags: + - store + operationId: deleteOrder + parameters: + - $ref: '#/components/parameters/orderId' + /user: + post: + tags: + - user + operationId: createUser + responses: + default: + description: successful operation + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Created user object + required: true + /user/createWithList: + post: + tags: + - user + operationId: createUsersWithListInput + requestBody: + $ref: '#/components/requestBodies/UserArray' + /user/login: + get: + tags: + - user + operationId: loginUser + parameters: + - name: username + in: query + description: The user name for login + required: true + schema: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + - name: password + in: query + description: The password for login in clear text + required: true + schema: + type: string + responses: + '200': + description: successful operation + headers: + X-Rate-Limit: + x-internal: true + description: calls per hour allowed by the user + schema: + type: integer + format: int32 + content: + application/json: + schema: + type: string + '400': + description: Invalid username/password supplied + /user/logout: + get: + tags: + - user + operationId: logoutUser + '/user/{username}': + get: + tags: + - user + operationId: getUserByName + parameters: + - name: username + in: path + description: The name that needs to be fetched. Use user1 for testing. + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/User' + put: + tags: + - user + operationId: updateUser + parameters: + - name: username + in: path + description: name that need to be deleted + required: true + schema: + type: string + responses: + '400': + description: Invalid user supplied + '404': + description: User not found + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + description: Updated user object + required: true + delete: + tags: + - user + operationId: deleteUser + parameters: + - name: username + in: path + description: The name that needs to be deleted + required: true + schema: + type: string +externalDocs: + description: Find out more about Swagger + url: 'http://swagger.io' +components: + responses: + Order: + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + requestBodies: + UserArray: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + description: Pet object that needs to be added to the store + required: true + Order: + content: + application/json: + schema: + $ref: '#/components/schemas/Order' + description: order placed for purchasing the pet + required: true + schemas: + Order: + title: Pet Order + description: An order for a pets from the pet store + type: object + properties: + id: + type: integer + format: int64 + petId: + type: integer + format: int64 + quantity: + type: integer + format: int32 + shipDate: + type: string + format: date-time + status: + type: string + description: Order Status + enum: + - placed + - approved + - delivered + complete: + type: boolean + default: false + Category: + title: Pet category + description: A category for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' + User: + title: a User + description: A User who is purchasing from the pet store + type: object + properties: + id: + type: integer + format: int64 + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + type: integer + format: int32 + description: User Status + Tag: + title: Pet Tag + description: A tag for a pet + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + Pet: + title: a Pet + description: A pet for sale in the pet store + type: object + required: + - name + - photoUrls + properties: + id: + type: integer + format: int64 + category: + $ref: '#/components/schemas/Category' + name: + type: string + example: doggie + photoUrls: + type: array + items: + type: string + tags: + type: array + items: + $ref: '#/components/schemas/Tag' + status: + type: string + description: pet status in the store + deprecated: true + enum: + - available + - pending + - sold + ApiResponse: + title: An uploaded response + description: Describes the result of uploading an image resource + type: object + properties: + code: + type: integer + format: int32 + type: + type: string + message: + type: string + parameters: + orderId: + name: orderId + in: path + required: true + schema: + type: string + offset: + in: query + name: offset + required: false + x-internal: true + deprecated: true + schema: + type: integer diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated.yaml new file mode 100644 index 000000000000..1481e19d5c6a --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated.yaml @@ -0,0 +1,427 @@ +openapi: 3.0.0 +info: + description: This is a sample server Petstore server. + version: 1.0.0 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: + - url: http://petstore.swagger.io/v2 +tags: + - description: Everything about your Pets + name: pet + - description: Access to Petstore orders + name: store + - description: Operations about user + name: user +paths: + /pet: + post: + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "405": + description: Invalid input + tags: + - pet + put: + externalDocs: + description: API documentation for the updatePet operation + url: http://petstore.swagger.io/v2/doc/updatePet + operationId: updatePet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + tags: + - pet + /pet/{petId}: + delete: + description: "" + operationId: deletePet + parameters: + - explode: false + in: header + name: api_key + required: false + schema: + type: string + style: simple + - description: Pet id to delete + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "400": + description: Invalid pet value + summary: Deletes a pet + tags: + - pet + get: + operationId: getPetById + parameters: + - description: ID of pet to return + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "400": + description: Invalid ID supplied + "404": + description: Pet not found + tags: + - pet + post: + operationId: updatePetWithForm + parameters: + - description: ID of pet that needs to be updated + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + schema: + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + type: object + responses: + "405": + description: Invalid input + tags: + - pet + /store/order: + post: + operationId: placeOrder + requestBody: + $ref: "#/components/requestBodies/Order" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: successful operation + "400": + description: Invalid Order + tags: + - store + /store/order/{orderId}: + delete: + operationId: deleteOrder + parameters: + - $ref: "#/components/parameters/orderId" + tags: + - store + get: + operationId: getOrderById + parameters: + - $ref: "#/components/parameters/orderId" + responses: + "200": + $ref: "#/components/responses/Order" + tags: + - store + /user: + post: + operationId: createUser + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Created user object + required: true + responses: + default: + description: successful operation + tags: + - user + /user/createWithList: + post: + operationId: createUsersWithListInput + requestBody: + $ref: "#/components/requestBodies/UserArray" + tags: + - user + /user/login: + get: + operationId: loginUser + parameters: + - description: The user name for login + explode: true + in: query + name: username + required: true + schema: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + style: form + - description: The password for login in clear text + explode: true + in: query + name: password + required: true + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + type: string + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + explode: false + schema: + format: int32 + type: integer + style: simple + x-internal: true + "400": + description: Invalid username/password supplied + tags: + - user + /user/logout: + get: + operationId: logoutUser + tags: + - user + /user/{username}: + delete: + operationId: deleteUser + parameters: + - description: The name that needs to be deleted + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + tags: + - user + get: + operationId: getUserByName + parameters: + - description: The name that needs to be fetched. Use user1 for testing. + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: successful operation + tags: + - user + put: + operationId: updateUser + parameters: + - description: name that need to be deleted + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Updated user object + required: true + responses: + "400": + description: Invalid user supplied + "404": + description: User not found + tags: + - user +components: + parameters: + orderId: + explode: false + in: path + name: orderId + required: true + schema: + type: string + style: simple + requestBodies: + UserArray: + content: + application/json: + schema: + items: + $ref: "#/components/schemas/User" + type: array + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + Order: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: order placed for purchasing the pet + required: true + responses: + Order: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: successful operation + schemas: + Order: + description: An order for a pets from the pet store + properties: + id: + format: int64 + type: integer + petId: + format: int64 + type: integer + quantity: + format: int32 + type: integer + shipDate: + format: date-time + type: string + status: + description: Order Status + enum: + - placed + - approved + - delivered + type: string + complete: + default: false + type: boolean + title: Pet Order + type: object + Category: + description: A category for a pet + properties: + id: + format: int64 + type: integer + name: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + title: Pet category + type: object + User: + description: A User who is purchasing from the pet store + properties: + id: + format: int64 + type: integer + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + description: User Status + format: int32 + type: integer + title: a User + type: object + Tag: + description: A tag for a pet + properties: + id: + format: int64 + type: integer + name: + type: string + title: Pet Tag + type: object + Pet: + description: A pet for sale in the pet store + properties: + id: + format: int64 + type: integer + category: + $ref: "#/components/schemas/Category" + name: + example: doggie + type: string + photoUrls: + items: + type: string + type: array + tags: + items: + $ref: "#/components/schemas/Tag" + type: array + required: + - name + - photoUrls + title: a Pet + type: object diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_config.yaml new file mode 100644 index 000000000000..7a327536c7d2 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_config.yaml @@ -0,0 +1,4 @@ +inputSpec: src/test/resources/openapi_normalizer/petstore.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeDeprecated.yaml +inputRules: + REMOVE_FILTER: 'deprecated ; unused' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all.yaml new file mode 100644 index 000000000000..21b8cb33f5fe --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all.yaml @@ -0,0 +1,511 @@ +openapi: 3.0.0 +info: + description: This is a sample server Petstore server. + version: 1.0.0 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: + - url: http://petstore.swagger.io/v2 +paths: + /pet: + post: + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "405": + description: Invalid input + put: + externalDocs: + description: API documentation for the updatePet operation + url: http://petstore.swagger.io/v2/doc/updatePet + operationId: updatePet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + /pet/findByStatus: + get: + deprecated: true + operationId: findPetsByStatus + parameters: + - deprecated: true + explode: true + in: query + name: limit + required: false + schema: + type: integer + style: form + x-internal: true + - deprecated: true + description: Status values that need to be considered for filter + explode: false + in: query + name: status + required: true + schema: + items: + default: available + enum: + - available + - pending + - sold + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + "400": + description: Invalid status value + parameters: + - deprecated: true + explode: false + in: header + name: role + required: false + schema: + type: string + style: simple + x-internal: true + /pet/findByTags: + get: + deprecated: true + operationId: findPetsByTags + parameters: + - description: Tags to filter by + explode: false + in: query + name: tags + required: true + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + "400": + description: Invalid tag value + /pet/{petId}: + delete: + description: "" + operationId: deletePet + parameters: + - explode: false + in: header + name: api_key + required: false + schema: + type: string + style: simple + - description: Pet id to delete + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "400": + description: Invalid pet value + summary: Deletes a pet + get: + operationId: getPetById + parameters: + - description: ID of pet to return + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "400": + description: Invalid ID supplied + "404": + description: Pet not found + post: + operationId: updatePetWithForm + parameters: + - description: ID of pet that needs to be updated + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + schema: + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + type: object + responses: + "405": + description: Invalid input + /store/inventory: + get: + deprecated: true + operationId: getInventory + responses: + "200": + content: + application/json: + schema: + additionalProperties: + format: int32 + type: integer + type: object + description: successful operation + /store/order: + post: + operationId: placeOrder + requestBody: + $ref: "#/components/requestBodies/Order" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: successful operation + "400": + description: Invalid Order + /store/order/{orderId}: + delete: + operationId: deleteOrder + parameters: + - $ref: "#/components/parameters/orderId" + get: + operationId: getOrderById + parameters: + - $ref: "#/components/parameters/orderId" + responses: + "200": + $ref: "#/components/responses/Order" + /user: + post: + operationId: createUser + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Created user object + required: true + responses: + default: + description: successful operation + /user/createWithList: + post: + operationId: createUsersWithListInput + requestBody: + $ref: "#/components/requestBodies/UserArray" + /user/login: + get: + operationId: loginUser + parameters: + - description: The user name for login + explode: true + in: query + name: username + required: true + schema: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + style: form + - description: The password for login in clear text + explode: true + in: query + name: password + required: true + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + type: string + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + explode: false + schema: + format: int32 + type: integer + style: simple + x-internal: true + "400": + description: Invalid username/password supplied + /user/logout: + get: + operationId: logoutUser + /user/{username}: + delete: + operationId: deleteUser + parameters: + - description: The name that needs to be deleted + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + get: + operationId: getUserByName + parameters: + - description: The name that needs to be fetched. Use user1 for testing. + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: successful operation + put: + operationId: updateUser + parameters: + - description: name that need to be deleted + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Updated user object + required: true + responses: + "400": + description: Invalid user supplied + "404": + description: User not found +components: + parameters: + orderId: + explode: false + in: path + name: orderId + required: true + schema: + type: string + style: simple + offset: + deprecated: true + explode: true + in: query + name: offset + required: false + schema: + type: integer + style: form + x-internal: true + requestBodies: + UserArray: + content: + application/json: + schema: + items: + $ref: "#/components/schemas/User" + type: array + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + Order: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: order placed for purchasing the pet + required: true + responses: + Order: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: successful operation + schemas: + Order: + description: An order for a pets from the pet store + properties: + id: + format: int64 + type: integer + petId: + format: int64 + type: integer + quantity: + format: int32 + type: integer + shipDate: + format: date-time + type: string + status: + description: Order Status + enum: + - placed + - approved + - delivered + type: string + complete: + default: false + type: boolean + title: Pet Order + type: object + Category: + description: A category for a pet + properties: + id: + format: int64 + type: integer + name: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + title: Pet category + type: object + User: + description: A User who is purchasing from the pet store + properties: + id: + format: int64 + type: integer + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + description: User Status + format: int32 + type: integer + title: a User + type: object + Tag: + description: A tag for a pet + properties: + id: + format: int64 + type: integer + name: + type: string + title: Pet Tag + type: object + Pet: + description: A pet for sale in the pet store + properties: + id: + format: int64 + type: integer + category: + $ref: "#/components/schemas/Category" + name: + example: doggie + type: string + photoUrls: + items: + type: string + type: array + tags: + items: + $ref: "#/components/schemas/Tag" + type: array + status: + deprecated: true + description: pet status in the store + enum: + - available + - pending + - sold + type: string + required: + - name + - photoUrls + title: a Pet + type: object + ApiResponse: + description: Describes the result of uploading an image resource + properties: + code: + format: int32 + type: integer + type: + type: string + message: + type: string + title: An uploaded response + type: object diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all_config.yaml new file mode 100644 index 000000000000..6cad52747a9f --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all_config.yaml @@ -0,0 +1,4 @@ +inputSpec: src/test/resources/openapi_normalizer/petstore.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeTags_all.yaml +inputRules: + REMOVE_FILTER: 'unused: tags ; tags' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused.yaml new file mode 100644 index 000000000000..2aba5a9875fc --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused.yaml @@ -0,0 +1,251 @@ +openapi: 3.0.0 +info: + description: This is a sample server Petstore server. + version: 1.0.0 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: + - url: http://petstore.swagger.io/v2 +tags: + - description: Everything about your Pets + name: pet +paths: + /pet: + post: + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "405": + description: Invalid input + tags: + - pet + put: + externalDocs: + description: API documentation for the updatePet operation + url: http://petstore.swagger.io/v2/doc/updatePet + operationId: updatePet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + tags: + - pet + /pet/findByStatus: + get: + deprecated: true + operationId: findPetsByStatus + parameters: + - deprecated: true + description: Status values that need to be considered for filter + explode: false + in: query + name: status + required: true + schema: + items: + default: available + enum: + - available + - pending + - sold + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + "400": + description: Invalid status value + tags: + - pet + /pet/findByTags: + get: + deprecated: true + operationId: findPetsByTags + parameters: + - description: Tags to filter by + explode: false + in: query + name: tags + required: true + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + "400": + description: Invalid tag value + tags: + - pet + /pet/{petId}: + delete: + description: "" + operationId: deletePet + parameters: + - explode: false + in: header + name: api_key + required: false + schema: + type: string + style: simple + - description: Pet id to delete + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "400": + description: Invalid pet value + summary: Deletes a pet + tags: + - pet + get: + operationId: getPetById + parameters: + - description: ID of pet to return + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "400": + description: Invalid ID supplied + "404": + description: Pet not found + tags: + - pet + post: + operationId: updatePetWithForm + parameters: + - description: ID of pet that needs to be updated + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + schema: + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + type: object + responses: + "405": + description: Invalid input + tags: + - pet +components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + schemas: + Category: + description: A category for a pet + properties: + id: + format: int64 + type: integer + name: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + title: Pet category + type: object + Tag: + description: A tag for a pet + properties: + id: + format: int64 + type: integer + name: + type: string + title: Pet Tag + type: object + Pet: + description: A pet for sale in the pet store + properties: + id: + format: int64 + type: integer + category: + $ref: "#/components/schemas/Category" + name: + example: doggie + type: string + photoUrls: + items: + type: string + type: array + tags: + items: + $ref: "#/components/schemas/Tag" + type: array + status: + deprecated: true + description: pet status in the store + enum: + - available + - pending + - sold + type: string + required: + - name + - photoUrls + title: a Pet + type: object diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_REMOVE_X_INTERNAL_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_REMOVE_X_INTERNAL_config.yaml new file mode 100644 index 000000000000..fde9ba7d375c --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_REMOVE_X_INTERNAL_config.yaml @@ -0,0 +1,6 @@ +inputSpec: src/test/resources/openapi_normalizer/petstore.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeUnused.yaml +inputRules: + FILTER: 'tag: pet' + REMOVE_FILTER: 'internal ; unused' + REMOVE_X_INTERNAL: true diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_config.yaml new file mode 100644 index 000000000000..d6f96bf5ccbd --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_config.yaml @@ -0,0 +1,5 @@ +inputSpec: src/test/resources/openapi_normalizer/petstore.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeUnused.yaml +inputRules: + FILTER: 'tag: pet' + REMOVE_FILTER: 'internal ; unused ; vendorExtensions: x-internal' From b0ed007650b13a3e4ea506f5dcde02fa74a98703 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 25 Oct 2025 15:53:30 +0200 Subject: [PATCH 09/16] Improve documentation --- docs/customization.md | 33 ++++++++++++++++++- .../codegen/OpenAPINormalizer.java | 21 ------------ .../codegen/OpenAPINormalizerTest.java | 4 +++ 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 5cc0e972d158..5b2c58b7fd2b 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -647,7 +647,7 @@ java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generat The `FILTER` parameter allows selective inclusion of API operations based on specific criteria. It applies the `x-internal: true` property to operations that do **not** match the specified values, preventing them from being generated. Multiple filters can be separated by a semicolon. -### Available Filters +### Available FILTER filters - **`operationId`** When set to `operationId:addPet|getPetById`, operations **not** matching `addPet` or `getPetById` will be marked as internal (`x-internal: true`), and excluded from generation. Matching operations will have `x-internal: false`. @@ -715,3 +715,34 @@ Into this securityScheme: scheme: bearer type: http ``` + +- `REMOVE_FILTER` + +The `REMOVE_FILTER` parameter allows the removal of elements in an openAPI document. A semicolon can separate multiple filters. + +### Available REMOVE_FILTER filters + +- **`internal`** + When specified as `internal` or set to `internal:true`, all operations, schemas, properties and parameters marked with x-internal: true are removed from the document. Optionally set individual options like in `internal:operations|schemas|properties|parameters` + +- **`deprecated`** + When specified as `deprecated` or set to `deprecated:true`, all operations, schemas, properties and parameters marked with deprecated: true are removed. + +- **`tags`** + When specified as `tags` or set to `tags:true`, all tags are removed. + When set to `tags:store|user` all tags whose name is store or user are removed. + When set to `tags:keepOnlyFirstTag` perform the KEEP_ONLY_FIRST_TAG_IN_OPERATION normalization + +- **`vendorExtensions`** + When specified as `vendorExtensions` or set to `vendorExtensions:true`, remove all vendorExtensions (including x-internal). + When set to `vendorExtensions:x-role|x-groups`, remove all `x-role` and `x-groups` vendorExtensions. When set to `vendorExtensions:x-internal`, perform the equivalent of REMOVE_X_INTERNAL normalization. + +- **`unused`** + When specified as `unused` or set to `unused:true`, remove all unused schemas, tags, requestBodies, responses and parameters. + Optionally set individual options like in `unused:schemas|tags|requestBodies|responses|parameters` + +Example: +``` +java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g openapi -i modules/openapi-generator/src/test/resources/3_1/java/petstore.yaml -o /tmp/openapi/ --openapi-normalizer FILTER=tag:pet --openapi-normalizer REMOVE_FILTER=internal;unused +``` +generates an openapi.json without the store and user operations. diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index e472e12b10f5..ca08bf099860 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -2255,8 +2255,6 @@ public boolean parse() { } catch (RuntimeException e) { String message = String.format(Locale.ROOT, "FILTER rule [%s] `. Error: %s", filters, e.getMessage()); - // throw an exception. This is a breaking change compared to pre 7.16.0 - // Workaround: fix the syntax! throw new IllegalArgumentException(message); } } @@ -2266,11 +2264,6 @@ public boolean hasFilter() { } private void doParse() { - if ("true".equals(filters)) { - allTrue(); - return; - } - for (String filter : filters.split(";")) { filter = filter.trim(); String[] filterStrs = filter.split(":"); @@ -2346,20 +2339,6 @@ protected void parseFails(String filterName, String filterValue) { throw new IllegalArgumentException("filter not supported :[" + filterName + ":" + filterValue + "]"); } - private void allTrue() { - hasFilter = true; - - internalOperations = true; - internalSchemas = true; - internalProperties = true; - internalParameters = true; - - unusedSchemas = true; - unusedParameters = true; - unusedTags = true; - unusedRequestBodies = true; - } - protected boolean matchTag(Operation operation, int index, String tag) { if (removeAllTags) { return true; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 5b865d52aa6b..a7a3b7af26f9 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -1283,6 +1283,10 @@ public Object[][] getTestConfigs() { URL path = getClass().getClassLoader().getResource("openapi_normalizer"); File[] files = new File(path.getFile()) .listFiles(file -> file.getName().endsWith("_config.yaml")); + + if (files == null || files.length == 0) { + throw new AssertionError("No test configs found in openapi_normalizer directory."); + } return Arrays.stream(files) .map(file -> new Object[]{"src/test/resources/openapi_normalizer/" +file.getName()}) .toArray(Object[][]::new); From 91ef655c1cfcbfbd953e829308e6cc0ac8e764ea Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 25 Oct 2025 15:59:35 +0200 Subject: [PATCH 10/16] Improve documentation --- .../main/java/org/openapitools/codegen/OpenAPINormalizer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index ca08bf099860..b4c216f83413 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -150,6 +150,7 @@ public class OpenAPINormalizer { boolean updateNumberToNullable; boolean updateBooleanToNullable; + // when set (e.g. internal:unused), remove the x-internalt:true elements and remove the unused elements final String REMOVE_FILTER = "REMOVE_FILTER"; RemoveFilter removeFilter = new RemoveFilter(); From d8a8fe8bba381d97f47773732a6ccdd2346260be Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 25 Oct 2025 18:27:49 +0200 Subject: [PATCH 11/16] rollback --- .../resources/3_0/allOf_composition_discriminator.yaml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml index c08f897e791d..efeaa5fd02e9 100644 --- a/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/allOf_composition_discriminator.yaml @@ -30,15 +30,6 @@ paths: application/json: schema: $ref: '#/components/schemas/MyPets' - /a: - get: - responses: - '200': - description: desc - content: - application/json: - schema: - $ref: '#/components/schemas/A' components: schemas: Pet: From 1979461b97503266e2249655d001b8fa8649cac0 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 25 Oct 2025 18:32:32 +0200 Subject: [PATCH 12/16] Ensure backward compatibility. Do not remove parameters if not configured --- .../openapitools/codegen/OpenAPINormalizer.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index c00491274be8..8287be5a5c82 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -597,8 +597,11 @@ protected void normalizePaths() { // normalize PathItem common parameters normalizeParameters(pathsEntry.getKey(), path, null, path.getParameters()); - if (path.getParameters() != null && path.getParameters().isEmpty()) { - path.setParameters(null); + if (removeFilter.hasFilter()) { + // for backward compatibility, keep empty parameters if not removeFilter + if (path.getParameters() != null && path.getParameters().isEmpty()) { + path.setParameters(null); + } } if (removeFilter.hasFilter()) { @@ -613,8 +616,11 @@ protected void normalizePaths() { normalizeOperation(operation); normalizeRequestBody(operation); normalizeParameters(pathsEntry.getKey(), path, operation, operation.getParameters()); - if (operation.getParameters() != null && operation.getParameters().isEmpty()) { - operation.setParameters(null); + if (removeFilter.hasFilter()) { + // just for backward compatibility + if (operation.getParameters() != null && operation.getParameters().isEmpty()) { + operation.setParameters(null); + } } normalizeResponses(operation); } From 733c79cdd14bdf1dda132d6f5fd8304f45897a36 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sun, 26 Oct 2025 12:19:39 +0100 Subject: [PATCH 13/16] add x- and tags remove filters --- docs/customization.md | 25 +- .../codegen/OpenAPINormalizer.java | 197 ++++++++++-- .../custom_remove_filter_config.yaml | 5 +- ...l => custom_remove_filter_normalized.yaml} | 27 +- .../inline_x_internal_test_config.yaml | 3 +- ...=> inline_x_internal_test_normalized.yaml} | 0 .../openapi_normalizer/petstore.yaml | 1 + .../petstore_removeDeprecated_config.yaml | 3 +- ...petstore_removeDeprecated_normalized.yaml} | 0 .../petstore_removeTags_all_config.yaml | 5 +- ...ml => petstore_removeTags_normalized.yaml} | 1 + ...removeUnused_REMOVE_X_INTERNAL_config.yaml | 2 +- .../petstore_removeUnused_config.yaml | 5 +- ... => petstore_removeUnused_normalized.yaml} | 0 .../petstore_tags_config.yaml | 5 + .../petstore_tags_normalized.yaml | 282 ++++++++++++++++++ .../petstore_with_role_admin_config.yaml | 5 + .../petstore_with_role_admin_normalized.yaml | 53 ++++ .../petstore_with_role_any_config.yaml | 5 + .../petstore_with_role_any_normalized.yaml | 53 ++++ ...e_filter.yaml => petstore_with_roles.yaml} | 42 +-- 21 files changed, 622 insertions(+), 97 deletions(-) rename modules/openapi-generator/src/test/resources/openapi_normalizer/{custom_remove_filter_response.yaml => custom_remove_filter_normalized.yaml} (79%) rename modules/openapi-generator/src/test/resources/openapi_normalizer/{inline_x_internal_test_removed.yaml => inline_x_internal_test_normalized.yaml} (100%) rename modules/openapi-generator/src/test/resources/openapi_normalizer/{petstore_removeDeprecated.yaml => petstore_removeDeprecated_normalized.yaml} (100%) rename modules/openapi-generator/src/test/resources/openapi_normalizer/{petstore_removeTags_all.yaml => petstore_removeTags_normalized.yaml} (99%) rename modules/openapi-generator/src/test/resources/openapi_normalizer/{petstore_removeUnused.yaml => petstore_removeUnused_normalized.yaml} (100%) create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_config.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_normalized.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_admin_config.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_admin_normalized.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_any_config.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_any_normalized.yaml rename modules/openapi-generator/src/test/resources/openapi_normalizer/{custom_remove_filter.yaml => petstore_with_roles.yaml} (81%) diff --git a/docs/customization.md b/docs/customization.md index f07d94492115..df0ccc91cc09 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -724,19 +724,26 @@ The `REMOVE_FILTER` parameter allows the removal of elements in an openAPI docum ### Available REMOVE_FILTER filters - **`internal`** - When specified as `internal` or set to `internal:true`, all operations, schemas, properties and parameters marked with x-internal: true are removed from the document. Optionally set individual options like in `internal:operations|schemas|properties|parameters` + When specified as `internal` or set to `internal:true`, all operations, schemas, properties and parameters marked with `x-internal: true` are removed from the document. Optionally set individual options like in `internal:operations|schemas|properties|parameters` - **`deprecated`** - When specified as `deprecated` or set to `deprecated:true`, all operations, schemas, properties and parameters marked with deprecated: true are removed. + When specified as `deprecated` or set to `deprecated:true`, all operations, schemas, properties and parameters marked with `deprecated: true` are removed. -- **`tags`** - When specified as `tags` or set to `tags:true`, all tags are removed. - When set to `tags:store|user` all tags whose name is store or user are removed. - When set to `tags:keepOnlyFirstTag` perform the KEEP_ONLY_FIRST_TAG_IN_OPERATION normalization +- **`removeTags`** + When specified as `removeTags` or set to `removeTags:true`, all tags are removed from the operations. + When set to `removeTags:store|user` all tags whose name is `store` or `user` are removed. + When set to `removeTags:keepOnlyFirstTag` perform the KEEP_ONLY_FIRST_TAG_IN_OPERATION normalization -- **`vendorExtensions`** - When specified as `vendorExtensions` or set to `vendorExtensions:true`, remove all vendorExtensions (including x-internal). - When set to `vendorExtensions:x-role|x-groups`, remove all `x-role` and `x-groups` vendorExtensions. When set to `vendorExtensions:x-internal`, perform the equivalent of REMOVE_X_INTERNAL normalization. +- **`removeVendorExtensions`** + When specified as `removeVendorExtensions` or set to `removeVendorExtensions:true`, remove all vendorExtensions (including x-internal). + When set to `removeVendorExtensions:x-role|x-groups`, remove all `x-role` and `x-groups` vendorExtensions. When set to `removeVendorExtensions:x-internal`, perform the REMOVE_X_INTERNAL normalization. + +- **`x-`** + When specified as `x-role`, remove operations, schemas, properties and parameters having a vendor extension `x-role`. + When set to `x-role:admin|superuser`, remove operations, schemas, properties and parameters marked with vendorExtension `x-role:admin` or `x-role:superuser`. + +- **`tags`** + When set as `tags:user|store`, remove operations marked with tags: user or store. - **`unused`** When specified as `unused` or set to `unused:true`, remove all unused schemas, tags, requestBodies, responses and parameters. diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 8287be5a5c82..6090b287200a 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -17,7 +17,6 @@ package org.openapitools.codegen; -import io.swagger.models.Response; import io.swagger.v3.oas.models.*; import io.swagger.v3.oas.models.callbacks.Callback; import io.swagger.v3.oas.models.headers.Header; @@ -542,7 +541,7 @@ protected void processRemoveUnusedTags() { .flatMap(List::stream) .collect(Collectors.toSet()); if (openAPI.getTags() != null) { - openAPI.getTags().removeIf(tag -> removeFilter.matchTag(tag, usedTags)); + openAPI.getTags().removeIf(tag -> removeFilter.matchTagToRemove(tag, usedTags)); if (openAPI.getTags().isEmpty()) { openAPI.setTags(null); } @@ -703,6 +702,9 @@ protected void normalizeParameters(String path, PathItem pathItem, Operation ope if (removeFilter.hasFilter()) { for (Iterator it = parameters.iterator(); it.hasNext(); ) { Parameter parameter = it.next(); + if (parameter.get$ref() != null) { + parameter = ModelUtils.getReferencedParameter(openAPI, parameter); + } if (operation == null && removeFilter.matchParameter(path, pathItem, parameter)) { it.remove(); } else if (removeFilter.matchParameter(path, pathItem, operation, parameter)) { @@ -829,7 +831,7 @@ protected void normalizeComponentsSchemas() { if (removeFilter.hasFilter && schema.getExtensions() != null && schema.getExtensions().size() > 0) { Set extensionKeysToRemove = ((Map)schema.getExtensions()) .entrySet().stream() - .filter(entry -> removeFilter.matchVendorExtension(schema, entry.getKey(), entry.getValue())) + .filter(entry -> removeFilter.matchVendorExtensionToRemove(schema, entry.getKey(), entry.getValue())) .map(Map.Entry::getKey) .collect(Collectors.toSet()); extensionKeysToRemove.forEach(key -> schema.getExtensions().remove(key)); @@ -1084,7 +1086,7 @@ protected void processRemoveVendorExtensions(Schema property, Map extensionKeysToRemove = extensions .entrySet().stream() - .filter(entry -> removeFilter.matchVendorExtension(property, entry.getKey(), entry.getValue())) + .filter(entry -> removeFilter.matchVendorExtensionToRemove(property, entry.getKey(), entry.getValue())) .map(Map.Entry::getKey) .collect(Collectors.toSet()); extensions.keySet().removeIf(key -> extensionKeysToRemove.contains(key)); @@ -1094,7 +1096,7 @@ protected void processRemoveVendorExtensions(Schema property, Map extensionKeysToRemove = operation.getExtensions().entrySet().stream() - .filter(entry -> removeFilter.matchVendorExtension(operation, entry.getKey(), entry.getValue())) + .filter(entry -> removeFilter.matchVendorExtensionToRemove(operation, entry.getKey(), entry.getValue())) .map(Map.Entry::getKey) .collect(Collectors.toSet()); operation.getExtensions().keySet().removeIf(key -> extensionKeysToRemove.contains(key)); @@ -1361,7 +1363,7 @@ protected void processRemoveTagsInOperation(Operation operation) { List tags = operation.getTags(); Set names = new HashSet<>(); for (int i=0; i < tags.size(); i++) { - if (removeFilter.matchTag(operation, i, tags.get(i))) { + if (removeFilter.matchTagToRemove(operation, i, tags.get(i))) { names.add(tags.get(i)); } } @@ -2217,11 +2219,11 @@ private boolean hasMethod(String method) { } public static class RemoveFilter { + private final static Set ANYSET = new HashSet<>(); private final String filters; private boolean hasFilter; boolean removeXInternal; - boolean removeAllVendorExtensions; Set removeExtensions = Collections.emptySet(); boolean internalOperations; @@ -2236,9 +2238,10 @@ public static class RemoveFilter { boolean unusedResponses; boolean keepOnlyFirstTagInOperation; - boolean removeAllTags; + Set removeTags = Collections.emptySet(); + boolean deprecated; Set tags = Collections.emptySet(); - private boolean deprecated; + Map> vendorExtensions = new HashMap<>(); RemoveFilter() { this.filters = null; @@ -2314,57 +2317,113 @@ private void doParse() { } } else if (REMOVE_X_INTERNAL.equals(filterKey)) { removeXInternal = isTrue; - } else if ("vendorExtensions".equals(filterKey)) { + } else if ("removeVendorExtensions".equals(filterKey)) { if (isBoolean) { removeXInternal = isTrue; - removeAllVendorExtensions = isTrue; + removeExtensions = ANYSET; } else { removeExtensions = set; } } else if (KEEP_ONLY_FIRST_TAG_IN_OPERATION.equals(filterKey)) { keepOnlyFirstTagInOperation = isTrue; - } else if ("tags".equals(filterKey)) { + } else if ("removeTags".equals(filterKey)) { if ("keepOnlyFirstTag".equals(filterValue)) { keepOnlyFirstTagInOperation = true; } else { if (isBoolean) { - removeAllTags = isTrue; + removeTags = getAnySet(isTrue); } else { - tags = set; + removeTags = set; } } + } else if ("tags".equals(filterKey)) { + tags = set; + } else if (filterKey.startsWith("x-")) { + if (isBoolean) { + vendorExtensions.put(filterKey, getAnySet(isTrue)); + } else { + vendorExtensions.put(filterKey, set); + } } else { parse(filterKey, filterValue); } } } + + /** + * Parse unknown filter. + *

> + * Override this method to add custom parsing logic. + * Default is to fail. + * + * @param filterName name of the filter + * @param filterValue value of the filter + */ protected void parse(String filterName, String filterValue) { parseFails(filterName, filterValue); } + /** + * Fails the parsing with an IllegalArgumentException. + */ protected void parseFails(String filterName, String filterValue) { throw new IllegalArgumentException("filter not supported :[" + filterName + ":" + filterValue + "]"); } - protected boolean matchTag(Operation operation, int index, String tag) { - if (removeAllTags) { + /** + * Evaluate if a tag match the filters. + * + * @param operation owner of the tag + * @param index 0 indexed index in the tag list + * @param tagvalue vendorExtension value + * + * @return true if the vendorExtension needs to be removed + */ + protected boolean matchTagToRemove(Operation operation, int index, String tag) { + if (removeTags == ANYSET) { return true; } if (keepOnlyFirstTagInOperation) { return index > 0; } - return tags.contains(tag); + return removeTags.contains(tag); } - protected boolean matchVendorExtension(Schema schema, String key, Object value) { - return matchVendorExtension(key); + /** + * Evaluate if a vendorExtension match the filters. + * + * @param schema owner of the vendorExtension + * @param key vendorExtension key + * @param value vendorExtension value + * + * @return true if the vendorExtension needs to be removed + */ + protected boolean matchVendorExtensionToRemove(Schema schema, String key, Object value) { + return matchVendorExtensionToRemove(key); } - protected boolean matchVendorExtension(Operation operation, String key, Object value) { - return matchVendorExtension(key); + /** + * Evaluate if a vendorExtension match the filters. + * + * @param operation owner of the vendorExtension + * @param key vendorExtension key + * @param value vendorExtension value + * + * @return true if the vendorExtension needs to be removed + */ + protected boolean matchVendorExtensionToRemove(Operation operation, String key, Object value) { + return matchVendorExtensionToRemove(key); } + /** + * Evaluate if an operation matchs the filters. + * + * @param path endpoint + * @param method parent of the property (get, post, put, etc) + * @param operation operation to evaluate + * @return true if the operation needs to be removed + */ protected boolean matchOperation(String path, PathItem.HttpMethod method, Operation operation) { if (internalOperations && isInternal(operation.getExtensions())) { return true; @@ -2372,9 +2431,30 @@ protected boolean matchOperation(String path, PathItem.HttpMethod method, Operat if (deprecated && Boolean.TRUE.equals(operation.getDeprecated())) { return true; } - return false; + + return matchExtension(operation.getExtensions()) || (operation.getTags() != null && matchTag(operation.getTags())); } + /** + * Evaluate if the operation with tags need to be removed. + * + * @param operationTags tags of the operation + * @return true if the operation needs to be removed + */ + private boolean matchTag(List operationTags) { + if (this.tags.isEmpty()) { + return false; + } + return this.tags.stream().anyMatch(operationTags::contains); + } + + /** + * Evaluate if a property matchs the filters. + * + * @param schema parent of the property + * @param property + * @return true if the property needs to be removed + */ protected boolean matchInlineProperties(Schema schema, Schema property) { if (internalProperties && isInternal(property.getExtensions())) { return true; @@ -2382,9 +2462,15 @@ protected boolean matchInlineProperties(Schema schema, Schema property) { if (deprecated && Boolean.TRUE.equals(property.getDeprecated())) { return true; } - return false; + return matchExtension(property.getExtensions()); } + /** + * Evaluate if a schema matchs the filters. + * + * @param schema + * @return true if the schema needs to be removed + */ protected boolean matchSchema(Schema schema) { if (internalSchemas && isInternal(schema.getExtensions())) { return true; @@ -2392,23 +2478,47 @@ protected boolean matchSchema(Schema schema) { if (deprecated && Boolean.TRUE.equals(schema.getDeprecated())) { return true; } - return false; + return matchExtension(schema.getExtensions()); } - protected boolean matchTag(Tag tag, Set usedTags) { + /** + * Evaluate if the tag matchs the filters. + * + * @param tag + * @param usedTags the names of the tags used by the document + * @return true if the parameter needs to be removed + */ + protected boolean matchTagToRemove(Tag tag, Set usedTags) { return unusedTags && !usedTags.contains(tag.getName()); } + /** + * Evaluate if the parameter matchs the filters. + * + * @param path path of the endpoint + * @param pathItem path item of the endpoint + * @param parameter parameter of the endpoint + * @return true if the parameter needs to be removed + */ protected boolean matchParameter(String path, PathItem pathItem, Parameter parameter) { return matchParameter(parameter); } + /** + * Evaluate if the parameter matchs the filters. + * + * @param path path of the endpoint + * @param pathItem path item of the endpoint + * @param operation operation of the endpoint + * @param parameter parameter of the endpoint + * @return true if the parameter needs to be removed + */ protected boolean matchParameter(String path, PathItem pathItem, Operation operation, Parameter parameter) { return matchParameter(parameter); } - protected boolean matchVendorExtension(String key) { - if (removeAllVendorExtensions) { + protected boolean matchVendorExtensionToRemove(String key) { + if (removeExtensions == ANYSET) { return true; } // remove x-internal if needed @@ -2419,6 +2529,12 @@ protected boolean matchVendorExtension(String key) { return removeExtensions.contains(key); } + /** + * Evaluate if the parameter matchs the filters. + * + * @param parameter + * @return true if the parameter needs to be removed + */ protected boolean matchParameter(Parameter parameter) { if (internalParameters && isInternal(parameter.getExtensions())) { return true; @@ -2426,7 +2542,32 @@ protected boolean matchParameter(Parameter parameter) { if (deprecated && Boolean.TRUE.equals(parameter.getDeprecated())) { return true; } - return false; + + return matchExtension(parameter.getExtensions()); + } + + private boolean matchExtension(Map extensions) { + if (extensions == null || extensions.isEmpty() || vendorExtensions.isEmpty()) { + return false; + } + return extensions.entrySet() + .stream() + .anyMatch(this::matchExtension); + } + + protected boolean matchExtension(Map.Entry entry) { + Set set = vendorExtensions.get(entry.getKey()); + if (set == null) { + return false; + } + if (set == ANYSET) { + return true; + } + return set.contains(entry.getValue()); + } + + private Set getAnySet(boolean isTrue) { + return isTrue ? ANYSET : Collections.emptySet(); } } diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_config.yaml index 3e8641f257ac..ba9781e6d9d1 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_config.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_config.yaml @@ -1,5 +1,6 @@ -inputSpec: src/test/resources/openapi_normalizer/custom_remove_filter.yaml -normalizedSpec: src/test/resources/openapi_normalizer/custom_remove_filter_response.yaml +# test custom remove filter +inputSpec: src/test/resources/openapi_normalizer/petstore_with_roles.yaml +normalizedSpec: src/test/resources/openapi_normalizer/custom_remove_filter_normalized.yaml inputRules: NORMALIZER_CLASS: org.openapitools.codegen.OpenAPINormalizerTest$CustomRemoveFilter REMOVE_FILTER: 'unused' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_response.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_normalized.yaml similarity index 79% rename from modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_response.yaml rename to modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_normalized.yaml index bcf2ad30098e..06d2987f547e 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_response.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter_normalized.yaml @@ -24,7 +24,19 @@ paths: schema: $ref: "#/components/schemas/Pet" description: successful operation + put: + requestBody: + $ref: "#/components/requestBodies/Pet" + x-role: superuser components: + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true schemas: Category: description: A category for a pet @@ -33,7 +45,6 @@ components: format: int64 type: integer name: - pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" type: string title: Pet category type: object @@ -56,24 +67,12 @@ components: category: $ref: "#/components/schemas/Category" name: - example: doggie type: string - photoUrls: - items: - type: string - type: array tags: items: $ref: "#/components/schemas/Tag" type: array - status: - deprecated: true - description: pet status in the store - enum: - - available - - pending - - sold - type: string + x-role: admin required: - name - photoUrls diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_config.yaml index 5586f3e5f920..e687b1188936 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_config.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_config.yaml @@ -1,4 +1,5 @@ +# remove the elements having x-internal: true inputSpec: src/test/resources/openapi_normalizer/inline_x_internal_test.yaml -normalizedSpec: src/test/resources/openapi_normalizer/inline_x_internal_test_removed.yaml +normalizedSpec: src/test/resources/openapi_normalizer/inline_x_internal_test_normalized.yaml inputRules: REMOVE_FILTER: 'internal: properties' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_removed.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_normalized.yaml similarity index 100% rename from modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_removed.yaml rename to modules/openapi-generator/src/test/resources/openapi_normalizer/inline_x_internal_test_normalized.yaml diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml index cb4ba40598cc..100458491d6d 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml @@ -67,6 +67,7 @@ paths: x-internal: true schema: type: integer + - $ref: '#/components/parameters/offset' - name: status in: query description: Status values that need to be considered for filter diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_config.yaml index 7a327536c7d2..389eb0bd5848 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_config.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_config.yaml @@ -1,4 +1,5 @@ +# remove the elements having deprecated: true inputSpec: src/test/resources/openapi_normalizer/petstore.yaml -normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeDeprecated.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeDeprecated_normalized.yaml inputRules: REMOVE_FILTER: 'deprecated ; unused' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_normalized.yaml similarity index 100% rename from modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated.yaml rename to modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_normalized.yaml diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all_config.yaml index 6cad52747a9f..60c9a8e3ad3d 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all_config.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all_config.yaml @@ -1,4 +1,5 @@ +# remove all the tags inputSpec: src/test/resources/openapi_normalizer/petstore.yaml -normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeTags_all.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeTags_normalized.yaml inputRules: - REMOVE_FILTER: 'unused: tags ; tags' + REMOVE_FILTER: 'unused: tags ; removeTags' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_normalized.yaml similarity index 99% rename from modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all.yaml rename to modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_normalized.yaml index 21b8cb33f5fe..d2e9ff06866d 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_all.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_normalized.yaml @@ -50,6 +50,7 @@ paths: type: integer style: form x-internal: true + - $ref: "#/components/parameters/offset" - deprecated: true description: Status values that need to be considered for filter explode: false diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_REMOVE_X_INTERNAL_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_REMOVE_X_INTERNAL_config.yaml index fde9ba7d375c..2358d596d860 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_REMOVE_X_INTERNAL_config.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_REMOVE_X_INTERNAL_config.yaml @@ -1,5 +1,5 @@ inputSpec: src/test/resources/openapi_normalizer/petstore.yaml -normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeUnused.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeUnused_normalized.yaml inputRules: FILTER: 'tag: pet' REMOVE_FILTER: 'internal ; unused' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_config.yaml index d6f96bf5ccbd..18bc12907b16 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_config.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_config.yaml @@ -1,5 +1,6 @@ +# test a combination of FILTER and REMOVE_FILTER inputSpec: src/test/resources/openapi_normalizer/petstore.yaml -normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeUnused.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeUnused_normalized.yaml inputRules: FILTER: 'tag: pet' - REMOVE_FILTER: 'internal ; unused ; vendorExtensions: x-internal' + REMOVE_FILTER: 'internal ; unused ; removeVendorExtensions: x-internal' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_normalized.yaml similarity index 100% rename from modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused.yaml rename to modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_normalized.yaml diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_config.yaml new file mode 100644 index 000000000000..4bc3c8695f6b --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_config.yaml @@ -0,0 +1,5 @@ +# remove the operations having tag name user or store +inputSpec: src/test/resources/openapi_normalizer/petstore.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_tags_normalized.yaml +inputRules: + REMOVE_FILTER: 'unused; tags:user|store' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_normalized.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_normalized.yaml new file mode 100644 index 000000000000..d8a2b36cc696 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_normalized.yaml @@ -0,0 +1,282 @@ +openapi: 3.0.0 +info: + description: This is a sample server Petstore server. + version: 1.0.0 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: + - url: http://petstore.swagger.io/v2 +tags: + - description: Everything about your Pets + name: pet +paths: + /pet: + post: + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "405": + description: Invalid input + tags: + - pet + put: + externalDocs: + description: API documentation for the updatePet operation + url: http://petstore.swagger.io/v2/doc/updatePet + operationId: updatePet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + tags: + - pet + /pet/findByStatus: + get: + deprecated: true + operationId: findPetsByStatus + parameters: + - deprecated: true + explode: true + in: query + name: limit + required: false + schema: + type: integer + style: form + x-internal: true + - $ref: "#/components/parameters/offset" + - deprecated: true + description: Status values that need to be considered for filter + explode: false + in: query + name: status + required: true + schema: + items: + default: available + enum: + - available + - pending + - sold + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + "400": + description: Invalid status value + tags: + - pet + parameters: + - deprecated: true + explode: false + in: header + name: role + required: false + schema: + type: string + style: simple + x-internal: true + /pet/findByTags: + get: + deprecated: true + operationId: findPetsByTags + parameters: + - description: Tags to filter by + explode: false + in: query + name: tags + required: true + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + "400": + description: Invalid tag value + tags: + - pet + /pet/{petId}: + delete: + description: "" + operationId: deletePet + parameters: + - explode: false + in: header + name: api_key + required: false + schema: + type: string + style: simple + - description: Pet id to delete + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "400": + description: Invalid pet value + summary: Deletes a pet + tags: + - pet + get: + operationId: getPetById + parameters: + - description: ID of pet to return + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "400": + description: Invalid ID supplied + "404": + description: Pet not found + tags: + - pet + post: + operationId: updatePetWithForm + parameters: + - description: ID of pet that needs to be updated + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + schema: + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + type: object + responses: + "405": + description: Invalid input + tags: + - pet +components: + parameters: + offset: + deprecated: true + explode: true + in: query + name: offset + required: false + schema: + type: integer + style: form + x-internal: true + requestBodies: + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + schemas: + Category: + description: A category for a pet + properties: + id: + format: int64 + type: integer + name: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + title: Pet category + type: object + Tag: + description: A tag for a pet + properties: + id: + format: int64 + type: integer + name: + type: string + title: Pet Tag + type: object + Pet: + description: A pet for sale in the pet store + properties: + id: + format: int64 + type: integer + category: + $ref: "#/components/schemas/Category" + name: + example: doggie + type: string + photoUrls: + items: + type: string + type: array + tags: + items: + $ref: "#/components/schemas/Tag" + type: array + status: + deprecated: true + description: pet status in the store + enum: + - available + - pending + - sold + type: string + required: + - name + - photoUrls + title: a Pet + type: object diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_admin_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_admin_config.yaml new file mode 100644 index 000000000000..4bec7d75ba17 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_admin_config.yaml @@ -0,0 +1,5 @@ +# remove elements having x-role:admin or x-role:superuser +inputSpec: src/test/resources/openapi_normalizer/petstore_with_roles.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_with_role_admin_normalized.yaml +inputRules: + REMOVE_FILTER: 'x-role:admin|superuser ; unused' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_admin_normalized.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_admin_normalized.yaml new file mode 100644 index 000000000000..e08626c06777 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_admin_normalized.yaml @@ -0,0 +1,53 @@ +openapi: 3.0.0 +info: + version: 1.0.0 +servers: + - url: http://petstore.swagger.io/v2 +paths: + /pet/{petId}: + get: + operationId: getPetById + parameters: + - description: ID of pet to return + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation +components: + schemas: + Category: + description: A category for a pet + properties: + id: + format: int64 + type: integer + name: + type: string + title: Pet category + type: object + Pet: + description: A pet for sale in the pet store + properties: + id: + format: int64 + type: integer + category: + $ref: "#/components/schemas/Category" + name: + type: string + required: + - name + - photoUrls + title: a Pet + type: object diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_any_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_any_config.yaml new file mode 100644 index 000000000000..d32c33c7084e --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_any_config.yaml @@ -0,0 +1,5 @@ +# remove elements having x-role vendor extension +inputSpec: src/test/resources/openapi_normalizer/petstore_with_roles.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_with_role_any_normalized.yaml +inputRules: + REMOVE_FILTER: 'x-role ; unused' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_any_normalized.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_any_normalized.yaml new file mode 100644 index 000000000000..e08626c06777 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_role_any_normalized.yaml @@ -0,0 +1,53 @@ +openapi: 3.0.0 +info: + version: 1.0.0 +servers: + - url: http://petstore.swagger.io/v2 +paths: + /pet/{petId}: + get: + operationId: getPetById + parameters: + - description: ID of pet to return + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation +components: + schemas: + Category: + description: A category for a pet + properties: + id: + format: int64 + type: integer + name: + type: string + title: Pet category + type: object + Pet: + description: A pet for sale in the pet store + properties: + id: + format: int64 + type: integer + category: + $ref: "#/components/schemas/Category" + name: + type: string + required: + - name + - photoUrls + title: a Pet + type: object diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_roles.yaml similarity index 81% rename from modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter.yaml rename to modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_roles.yaml index a57da8a466bd..9f9e463b757f 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/custom_remove_filter.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_with_roles.yaml @@ -37,6 +37,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Pet' + put: + x-role: superuser + requestBody: + $ref: '#/components/requestBodies/Pet' components: responses: Order: @@ -81,19 +85,6 @@ components: petId: type: integer format: int64 - quantity: - type: integer - format: int32 - shipDate: - type: string - format: date-time - status: - type: string - description: Order Status - enum: - - placed - - approved - - delivered complete: type: boolean default: false @@ -107,7 +98,6 @@ components: format: int64 name: type: string - pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$' User: title: a User description: A User who is purchasing from the pet store @@ -122,16 +112,6 @@ components: type: string lastName: type: string - email: - type: string - password: - type: string - phone: - type: string - userStatus: - type: integer - format: int32 - description: User Status Tag: title: Pet Tag description: A tag for a pet @@ -157,23 +137,11 @@ components: $ref: '#/components/schemas/Category' name: type: string - example: doggie - photoUrls: - type: array - items: - type: string tags: + x-role: admin type: array items: $ref: '#/components/schemas/Tag' - status: - type: string - description: pet status in the store - deprecated: true - enum: - - available - - pending - - sold ApiResponse: title: An uploaded response description: Describes the result of uploading an image resource From 528e74266648c1a5e0a2838653537349b30bb4d0 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sat, 8 Nov 2025 19:10:45 +0100 Subject: [PATCH 14/16] Fix filter logging Add handling of x- filter --- docs/customization.md | 3 + .../codegen/OpenAPINormalizer.java | 281 ++++++--- .../codegen/OpenAPINormalizerTest.java | 2 +- .../openapi_normalizer/petstore.yaml | 4 + .../petstore_filter_role_guest.yaml | 578 ++++++++++++++++++ .../petstore_filter_role_guest_config.yaml | 5 + .../petstore_removeDeprecated_normalized.yaml | 2 + .../petstore_removeTags_normalized.yaml | 4 + .../petstore_removeUnused_normalized.yaml | 4 + .../petstore_tags_normalized.yaml | 4 + 10 files changed, 787 insertions(+), 100 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_filter_role_guest.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_filter_role_guest_config.yaml diff --git a/docs/customization.md b/docs/customization.md index a5db02f154f9..fc695c6b22a6 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -669,6 +669,9 @@ The `FILTER` parameter allows selective inclusion of API operations based on spe - **`path`** When set to `path:/v1|/v2`, operations on paths **not** starting with `/v1` or with `/v2` will be marked as internal (`x-internal: true`), and will not be generated. +- **`x-`** + When set to `x-role:admin|superuser`, operations or parameters having vendorExtension `x-role` with a value **not** in [`admin`,`superuser`] will be marked as internal (`x-internal: true`), and will not be generated. + ### Example Usage ```sh diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 7aac715a0d50..e8c820243ad8 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -35,9 +35,9 @@ import java.lang.reflect.Constructor; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; +import static org.apache.commons.lang3.ObjectUtils.isEmpty; import static org.openapitools.codegen.CodegenConstants.X_INTERNAL; import static org.openapitools.codegen.CodegenConstants.X_PARENT; import static org.openapitools.codegen.utils.ModelUtils.simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema; @@ -69,38 +69,38 @@ public class OpenAPINormalizer { // when set to true, $ref in allOf is treated as parent so that x-parent: true will be added // to the schema in $ref (if x-parent is not present) - final String REF_AS_PARENT_IN_ALLOF = "REF_AS_PARENT_IN_ALLOF"; + static final String REF_AS_PARENT_IN_ALLOF = "REF_AS_PARENT_IN_ALLOF"; // when set to true, only keep the first tag in operation if there are more than one tag defined. - final String KEEP_ONLY_FIRST_TAG_IN_OPERATION = "KEEP_ONLY_FIRST_TAG_IN_OPERATION"; + static final String KEEP_ONLY_FIRST_TAG_IN_OPERATION = "KEEP_ONLY_FIRST_TAG_IN_OPERATION"; // when set to true, complex composed schemas (a mix of oneOf/anyOf/anyOf and properties) with // oneOf/anyOf containing only `required` and no properties (these are properties inter-dependency rules) // are removed as most generators cannot handle such case at the moment - final String REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY = "REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY"; + static final String REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY = "REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY"; // when set to true, oneOf/anyOf with either string or enum string as sub schemas will be simplified // to just string - final String SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING = "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING"; + static final String SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING = "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING"; // when set to true, oneOf/anyOf schema with only one sub-schema is simplified to just the sub-schema // and if sub-schema contains "null", remove it and set nullable to true instead // and if sub-schema contains enum of "null", remove it and set nullable to true instead - final String SIMPLIFY_ONEOF_ANYOF = "SIMPLIFY_ONEOF_ANYOF"; + static final String SIMPLIFY_ONEOF_ANYOF = "SIMPLIFY_ONEOF_ANYOF"; // when set to true, boolean enum will be converted to just boolean - final String SIMPLIFY_BOOLEAN_ENUM = "SIMPLIFY_BOOLEAN_ENUM"; + static final String SIMPLIFY_BOOLEAN_ENUM = "SIMPLIFY_BOOLEAN_ENUM"; // when set to true, oneOf/anyOf with enum sub-schemas containing single values will be converted to a single enum - final String SIMPLIFY_ONEOF_ANYOF_ENUM = "SIMPLIFY_ONEOF_ANYOF_ENUM"; + static final String SIMPLIFY_ONEOF_ANYOF_ENUM = "SIMPLIFY_ONEOF_ANYOF_ENUM"; // when set to a string value, tags in all operations will be reset to the string value provided - final String SET_TAGS_FOR_ALL_OPERATIONS = "SET_TAGS_FOR_ALL_OPERATIONS"; + static final String SET_TAGS_FOR_ALL_OPERATIONS = "SET_TAGS_FOR_ALL_OPERATIONS"; String setTagsForAllOperations; // when set to true, tags in all operations will be set to operationId or "default" if operationId // is empty - final String SET_TAGS_TO_OPERATIONID = "SET_TAGS_TO_OPERATIONID"; + static final String SET_TAGS_TO_OPERATIONID = "SET_TAGS_TO_OPERATIONID"; String setTagsToOperationId; // when set to a string value, tags will be set to the value of the provided vendor extension @@ -119,33 +119,34 @@ public class OpenAPINormalizer { // when set to true, auto fix integer with maximum value 4294967295 (2^32-1) or long with 18446744073709551615 (2^64-1) // by adding x-unsigned to the schema - final String ADD_UNSIGNED_TO_INTEGER_WITH_INVALID_MAX_VALUE = "ADD_UNSIGNED_TO_INTEGER_WITH_INVALID_MAX_VALUE"; + static final String ADD_UNSIGNED_TO_INTEGER_WITH_INVALID_MAX_VALUE = "ADD_UNSIGNED_TO_INTEGER_WITH_INVALID_MAX_VALUE"; // when set to true, refactor schema with allOf and properties in the same level to a schema with allOf only and // the allOf contains a new schema containing the properties in the top level - final String REFACTOR_ALLOF_WITH_PROPERTIES_ONLY = "REFACTOR_ALLOF_WITH_PROPERTIES_ONLY"; + static final String REFACTOR_ALLOF_WITH_PROPERTIES_ONLY = "REFACTOR_ALLOF_WITH_PROPERTIES_ONLY"; // when set to true, remove the "properties" of a schema with type other than "object" - final String REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT = "REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT"; + static final String REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT = "REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT"; // when set to true, normalize OpenAPI 3.1 spec to make it work with the generator - final String NORMALIZE_31SPEC = "NORMALIZE_31SPEC"; + static final String NORMALIZE_31SPEC = "NORMALIZE_31SPEC"; // when set to true, remove x-internal: true from models, operations static final String REMOVE_X_INTERNAL = "REMOVE_X_INTERNAL"; // when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else final String FILTER = "FILTER"; + Filter filter = new Filter(); // when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else - final String SET_CONTAINER_TO_NULLABLE = "SET_CONTAINER_TO_NULLABLE"; + static final String SET_CONTAINER_TO_NULLABLE = "SET_CONTAINER_TO_NULLABLE"; HashSet setContainerToNullable = new HashSet<>(); boolean updateArrayToNullable; boolean updateSetToNullable; boolean updateMapToNullable; // when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else - final String SET_PRIMITIVE_TYPES_TO_NULLABLE = "SET_PRIMITIVE_TYPES_TO_NULLABLE"; + static final String SET_PRIMITIVE_TYPES_TO_NULLABLE = "SET_PRIMITIVE_TYPES_TO_NULLABLE"; HashSet setPrimitiveTypesToNullable = new HashSet<>(); boolean updateStringToNullable; boolean updateIntegerToNullable; @@ -153,7 +154,7 @@ public class OpenAPINormalizer { boolean updateBooleanToNullable; // when set (e.g. internal:unused), remove the x-internalt:true elements and remove the unused elements - final String REMOVE_FILTER = "REMOVE_FILTER"; + static final String REMOVE_FILTER = "REMOVE_FILTER"; RemoveFilter removeFilter = new RemoveFilter(); // ============= end of rules ============= @@ -326,23 +327,35 @@ public void processRules(Map inputRules) { rules.put(SET_BEARER_AUTH_FOR_NAME, true); } - String filter = inputRules.get(REMOVE_FILTER); + if (Boolean.TRUE.equals(getRule(FILTER))) { + String filters = inputRules.get(FILTER); + this.filter = createFilter(this.openAPI, filters); + this.filter.parse(); + } + + String removeFilter = inputRules.get(REMOVE_FILTER); // for backward compatibility if (getRule(REMOVE_X_INTERNAL)) { - filter = filter == null? REMOVE_X_INTERNAL : filter + " ; " + REMOVE_X_INTERNAL; + removeFilter = removeFilter == null? REMOVE_X_INTERNAL : removeFilter + " ; " + REMOVE_X_INTERNAL; } // for backward compatibility if (getRule(KEEP_ONLY_FIRST_TAG_IN_OPERATION)) { - filter = filter == null? KEEP_ONLY_FIRST_TAG_IN_OPERATION : filter + " ; " + KEEP_ONLY_FIRST_TAG_IN_OPERATION; + removeFilter = removeFilter == null? KEEP_ONLY_FIRST_TAG_IN_OPERATION : removeFilter + " ; " + KEEP_ONLY_FIRST_TAG_IN_OPERATION; } - if (filter != null) { - removeFilter = createRemoveFilter(filter); - removeFilter.parse(); + if (removeFilter != null) { + this.removeFilter = createRemoveFilter(removeFilter); + this.removeFilter.parse(); rules.put(REMOVE_FILTER, true); } - } + /** + * create the remove filter to process the REMOVE_FILTER normalizer. + * Ovveride this to create a custom remove filter normalizer. + * + * @param filter full filter value, including REMOVE_X_INTERNAL and KEEP_ONLY_FIRST_TAG_IN_OPERATION + * @return a remove filter containing the parsed filters. + */ protected RemoveFilter createRemoveFilter(String filter) { return new RemoveFilter(filter); } @@ -566,25 +579,11 @@ protected void normalizePaths() { PathItem path = pathsEntry.getValue(); List operations = new ArrayList<>(path.readOperations()); - Map> methodMap = Map.of( - "get", PathItem::getGet, - "put", PathItem::getPut, - "head", PathItem::getHead, - "post", PathItem::getPost, - "delete", PathItem::getDelete, - "patch", PathItem::getPatch, - "options", PathItem::getOptions, - "trace", PathItem::getTrace - ); - - if (Boolean.TRUE.equals(getRule(FILTER))) { - String filters = inputRules.get(FILTER); - Filter filter = createFilter(this.openAPI, filters); - if (filter.parse()) { - // Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem, - // and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters. - filter.apply(pathsEntry.getKey(), path, methodMap); - } + + if (filter.hasFilter()) { + // Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem, + // and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters. + filter.apply(pathsEntry.getKey(), path, path.readOperationsMap()); } // Include callback operation as well @@ -599,7 +598,7 @@ protected void normalizePaths() { } // normalize PathItem common parameters - normalizeParameters(pathsEntry.getKey(), path, null, path.getParameters()); + normalizeParameters(pathsEntry.getKey(), path, null, null, path.getParameters()); if (removeFilter.hasFilter()) { // for backward compatibility, keep empty parameters if not removeFilter if (path.getParameters() != null && path.getParameters().isEmpty()) { @@ -615,10 +614,12 @@ protected void normalizePaths() { } } } - for (Operation operation : operations) { + for (Map.Entry operationEntry : path.readOperationsMap().entrySet()) { + Operation operation = operationEntry.getValue(); + PathItem.HttpMethod method = operationEntry.getKey(); normalizeOperation(operation); normalizeRequestBody(operation); - normalizeParameters(pathsEntry.getKey(), path, operation, operation.getParameters()); + normalizeParameters(pathsEntry.getKey(), path, method, operation, operation.getParameters()); if (removeFilter.hasFilter()) { // just for backward compatibility if (operation.getParameters() != null && operation.getParameters().isEmpty()) { @@ -699,10 +700,19 @@ protected void normalizeRequestBody(Operation operation) { normalizeContent(requestBody.getContent()); } - protected void normalizeParameters(String path, PathItem pathItem, Operation operation, List parameters) { + protected void normalizeParameters(String path, PathItem pathItem, PathItem.HttpMethod method, Operation operation, List parameters) { if (parameters == null) { return; } + if (filter.hasFilter()) { + for (Iterator it = parameters.iterator(); it.hasNext(); ) { + Parameter parameter = it.next(); + if (parameter.get$ref() != null) { + parameter = ModelUtils.getReferencedParameter(openAPI, parameter); + } + filter.applyParameter(path, pathItem, method, operation, parameter); + } + } if (removeFilter.hasFilter()) { for (Iterator it = parameters.iterator(); it.hasNext(); ) { Parameter parameter = it.next(); @@ -2078,11 +2088,17 @@ protected static class Filter { public static final String PATH = "path"; private final String filters; protected Set operationIdFilters = Collections.emptySet(); - protected Set methodFilters = Collections.emptySet(); + protected Set methodFilters = Collections.emptySet(); protected Set tagFilters = Collections.emptySet(); protected Set pathStartingWithFilters = Collections.emptySet(); + protected Map> vendorExtensions = new HashMap<>(); private boolean hasFilter; + Filter() { + this.filters = null; + this.hasFilter = false; + } + protected Filter(String filters) { this.filters = filters.trim(); } @@ -2122,11 +2138,16 @@ private void doParse() { if (OPERATION_ID.equals(filterKey)) { operationIdFilters = parsedFilters; } else if (METHOD.equals(filterKey)) { - methodFilters = parsedFilters; + methodFilters = parsedFilters.stream() + .map(String::toUpperCase) + .map(PathItem.HttpMethod::valueOf) + .collect(Collectors.toSet()); } else if (TAG.equals(filterKey)) { tagFilters = parsedFilters; } else if (PATH.equals(filterKey)) { pathStartingWithFilters = parsedFilters; + } else if (filterKey.startsWith("x-")) { + vendorExtensions.put(filterKey, parsedFilters); } else { parse(filterKey, filterValue); } @@ -2174,31 +2195,86 @@ public boolean hasFilter() { return hasFilter; } - public void apply(String path, PathItem pathItem, Map> methodMap) { - methodMap.forEach((method, getter) -> { - Operation operation = getter.apply(pathItem); - if (operation != null) { - boolean found = false; - found |= logIfMatch(PATH, operation, hasPathStarting(path)); - found |= logIfMatch(TAG, operation, hasTag(operation)); - found |= logIfMatch(OPERATION_ID, operation, hasOperationId(operation)); - found |= logIfMatch(METHOD, operation, hasMethod(method)); - found |= hasCustomFilterMatch(path, operation); + public void apply(String path, PathItem pathItem, Map methodMap) { + methodMap.forEach((method, operation) -> { - operation.addExtension(X_INTERNAL, !found); + String operationInfo = getOperationInfo(path, pathItem, method, operation); + boolean found = false; + found |= logIfMatch(PATH, operationInfo, hasPathStarting(path)); + found |= logIfMatch(TAG, operationInfo, hasTag(operation)); + found |= logIfMatch(OPERATION_ID, operationInfo, hasOperationId(operation)); + found |= logIfMatch(METHOD, operationInfo, hasMethod(method)); + found |= hasCustomFilterMatch(path, operation); + found |= useVendorExtensions(operation) && logIfMatch(this.vendorExtensions, operationInfo, matchVendorExtension(path, method, operation)); + + if (!found) { + logMismatch(operationInfo); } + operation.addExtension(X_INTERNAL, !found); }); } - protected boolean logIfMatch(String filterName, Operation operation, boolean filterMatched) { + private String getOperationInfo(String path, PathItem pathItem, PathItem.HttpMethod method, Operation operation) { + return String.format("%s %s (operationId: %s)", method, path, operation.getOperationId()); + } + + protected boolean useVendorExtensions(Operation operation) { + return !isEmpty(this.vendorExtensions);// && !isEmpty(operation.getExtensions()); + } + + protected boolean matchVendorExtension(String path, PathItem.HttpMethod method, Operation operation) { + return matchExtension(operation.getExtensions()); + } + + public void applyParameter(String path, PathItem pathItem, PathItem.HttpMethod method, Operation operation, Parameter parameter) { + Map parameterExtensions = parameter.getExtensions(); + if (this.vendorExtensions == null || isEmpty(parameterExtensions)) { + return; + } + if (operation != null) { + if (!matchExtension(parameterExtensions)) { + parameter.addExtension(X_INTERNAL, true); + String operationInfo = getOperationInfo(path, pathItem, method, operation); + getLogger().info("Parameter `{}` in `{}` marked as internal (x-internal: true) by the {} FILTER", parameter.getName(), operationInfo, this.vendorExtensions); + } + } else { + if (!matchExtension(parameterExtensions)) { + parameter.addExtension(X_INTERNAL, true); + getLogger().info("Parameter `{}` in `{}` marked as internal (x-internal: true) by the {} FILTER", parameter.getName(), path, this.vendorExtensions); + } + } + } + + protected boolean logIfMatch(Object filterName, String operation, boolean filterMatched) { if (filterMatched) { - logMatch(filterName, operation); + logIfMatch(filterName, operation); } return filterMatched; } - protected void logMatch(String filterName, Operation operation) { - getLogger().info("operation `{}` marked as internal only (x-internal: true) by the {} FILTER", operation.getOperationId(), filterName); + protected void logMismatch(String operation) { + getLogger().info("operation `{}` marked as internal only (x-internal: true) by FILTER", operation); + } + + + protected void logIfMatch(Object filterName, String operation) { + getLogger().info("operation `{}` kept (x-internal: false) by the {} FILTER", operation, filterName); + } + + protected boolean matchExtension(Map extensions) { + if ( isEmpty(extensions)) { + return true; + } + for (Map.Entry> entry : this.vendorExtensions.entrySet()) { + String key = entry.getKey(); + if (extensions.containsKey(key)) { + Set filter = entry.getValue(); + if (!filter.contains(extensions.get(key))) { + return false; + } + } + } + return true; } protected Logger getLogger() { @@ -2217,9 +2293,10 @@ private boolean hasOperationId(Operation operation) { return operationIdFilters.contains(operation.getOperationId()); } - private boolean hasMethod(String method) { + private boolean hasMethod(PathItem.HttpMethod method) { return methodFilters.contains(method); } + } /** @@ -2245,25 +2322,25 @@ public static class RemoveFilter { private final String filters; private boolean hasFilter; - boolean removeXInternal; - Set removeExtensions = Collections.emptySet(); + protected boolean removeXInternal; + protected Set removeExtensions = Collections.emptySet(); - boolean internalOperations; - boolean internalSchemas; - boolean internalParameters; - boolean internalProperties; + protected boolean internalOperations; + protected boolean internalSchemas; + protected boolean internalParameters; + protected boolean internalProperties; - boolean unusedSchemas; - boolean unusedParameters; - boolean unusedTags; - boolean unusedRequestBodies; - boolean unusedResponses; + protected boolean unusedSchemas; + protected boolean unusedParameters; + protected boolean unusedTags; + protected boolean unusedRequestBodies; + protected boolean unusedResponses; - boolean keepOnlyFirstTagInOperation; - Set removeTags = Collections.emptySet(); - boolean deprecated; - Set tags = Collections.emptySet(); - Map> vendorExtensions = new HashMap<>(); + protected boolean keepOnlyFirstTagInOperation; + protected Set removeTags = Collections.emptySet(); + protected boolean deprecated; + protected Set tags = Collections.emptySet(); + protected Map> vendorExtensions = new HashMap<>(); RemoveFilter() { this.filters = null; @@ -2300,15 +2377,26 @@ private void doParse() { for (String filter : filters.split(";")) { filter = filter.trim(); String[] filterStrs = filter.split(":"); - if (filterStrs.length==1) { - filterStrs = new String[]{filterStrs[0], "true"}; + boolean isTrue = false; + boolean isBoolean = false; + boolean isAny = filterStrs.length == 1; + Set set; + String filterKey = filterStrs[0].trim(); + String filterValue; + if (isAny) { + filterStrs = new String[] { filterStrs[0], "true" }; + isTrue = true; + isBoolean = true; + set = Collections.emptySet(); + filterValue = null; + } else { + filterValue = filterStrs[1]; + set = splitByPipe(filterValue); + isTrue = Boolean.parseBoolean(filterValue); + isBoolean = "false".equals(filterValue) || isTrue; } hasFilter = true; - String filterKey = filterStrs[0].trim(); - String filterValue = filterStrs[1]; - Set set = splitByPipe(filterValue); - boolean isTrue = Boolean.valueOf(filterValue); - boolean isBoolean = isTrue || "false".equals(filterValue); + if ("internal".equals(filterKey)) { if (isBoolean) { internalOperations = isTrue; @@ -2340,9 +2428,9 @@ private void doParse() { } else if (REMOVE_X_INTERNAL.equals(filterKey)) { removeXInternal = isTrue; } else if ("removeVendorExtensions".equals(filterKey)) { - if (isBoolean) { + if (isAny) { removeXInternal = isTrue; - removeExtensions = ANYSET; + removeExtensions = getAnySet(isTrue); } else { removeExtensions = set; } @@ -2352,7 +2440,7 @@ private void doParse() { if ("keepOnlyFirstTag".equals(filterValue)) { keepOnlyFirstTagInOperation = true; } else { - if (isBoolean) { + if (isAny) { removeTags = getAnySet(isTrue); } else { removeTags = set; @@ -2361,18 +2449,13 @@ private void doParse() { } else if ("tags".equals(filterKey)) { tags = set; } else if (filterKey.startsWith("x-")) { - if (isBoolean) { - vendorExtensions.put(filterKey, getAnySet(isTrue)); - } else { - vendorExtensions.put(filterKey, set); - } + vendorExtensions.put(filterKey, isAny ? getAnySet(isTrue) : set); } else { parse(filterKey, filterValue); } } } - /** * Parse unknown filter. *

> @@ -2402,14 +2485,14 @@ protected void parseFails(String filterName, String filterValue) { * * @return true if the vendorExtension needs to be removed */ - protected boolean matchTagToRemove(Operation operation, int index, String tag) { + protected boolean matchTagToRemove(Operation operation, int index, String tagvalue) { if (removeTags == ANYSET) { return true; } if (keepOnlyFirstTagInOperation) { return index > 0; } - return removeTags.contains(tag); + return removeTags.contains(tagvalue); } /** diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index b6423c5b2db6..bae509b6b287 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -702,7 +702,7 @@ public void testFilterParsing() { // extra spaces are trimmed filter = parseFilter("method:\n\t\t\t\tget"); assertTrue(filter.hasFilter()); - assertEquals(filter.methodFilters, Set.of("get")); + assertEquals(filter.methodFilters, Set.of(PathItem.HttpMethod.GET)); assertTrue(filter.operationIdFilters.isEmpty()); assertTrue(filter.tagFilters.isEmpty()); assertTrue(filter.pathStartingWithFilters.isEmpty()); diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml index 100458491d6d..dc8f876992be 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore.yaml @@ -69,6 +69,7 @@ paths: type: integer - $ref: '#/components/parameters/offset' - name: status + x-role: admin in: query description: Status values that need to be considered for filter required: true @@ -100,6 +101,7 @@ paths: tags: - pet operationId: findPetsByTags + x-role: guest parameters: - name: tags in: query @@ -151,6 +153,7 @@ paths: tags: - pet operationId: updatePetWithForm + x-role: admin parameters: - name: petId in: path @@ -180,6 +183,7 @@ paths: summary: Deletes a pet description: '' operationId: deletePet + x-role: admin parameters: - name: api_key in: header diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_filter_role_guest.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_filter_role_guest.yaml new file mode 100644 index 000000000000..5caa5309ddaa --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_filter_role_guest.yaml @@ -0,0 +1,578 @@ +openapi: 3.0.0 +info: + description: This is a sample server Petstore server. + version: 1.0.0 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: + - url: http://petstore.swagger.io/v2 +tags: + - description: Everything about your Pets + name: pet + - description: Access to Petstore orders + name: store + - description: Operations about user + name: user +paths: + /pet: + post: + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "405": + description: Invalid input + tags: + - pet + x-internal: false + put: + externalDocs: + description: API documentation for the updatePet operation + url: http://petstore.swagger.io/v2/doc/updatePet + operationId: updatePet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + tags: + - pet + x-internal: false + /pet/findByStatus: + get: + deprecated: true + operationId: findPetsByStatus + parameters: + - deprecated: true + explode: true + in: query + name: limit + required: false + schema: + type: integer + style: form + x-internal: true + - $ref: "#/components/parameters/offset" + - deprecated: true + description: Status values that need to be considered for filter + explode: false + in: query + name: status + required: true + schema: + items: + default: available + enum: + - available + - pending + - sold + type: string + type: array + style: form + x-role: admin + x-internal: true + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + "400": + description: Invalid status value + tags: + - pet + x-internal: false + parameters: + - deprecated: true + explode: false + in: header + name: role + required: false + schema: + type: string + style: simple + x-internal: true + /pet/findByTags: + get: + deprecated: true + operationId: findPetsByTags + parameters: + - description: Tags to filter by + explode: false + in: query + name: tags + required: true + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + "400": + description: Invalid tag value + tags: + - pet + x-role: guest + x-internal: false + /pet/{petId}: + delete: + description: "" + operationId: deletePet + parameters: + - explode: false + in: header + name: api_key + required: false + schema: + type: string + style: simple + - description: Pet id to delete + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "400": + description: Invalid pet value + summary: Deletes a pet + tags: + - pet + x-role: admin + x-internal: true + get: + operationId: getPetById + parameters: + - description: ID of pet to return + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "400": + description: Invalid ID supplied + "404": + description: Pet not found + tags: + - pet + x-internal: false + post: + operationId: updatePetWithForm + parameters: + - description: ID of pet that needs to be updated + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + schema: + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + type: object + responses: + "405": + description: Invalid input + tags: + - pet + x-role: admin + x-internal: true + /store/inventory: + get: + deprecated: true + operationId: getInventory + responses: + "200": + content: + application/json: + schema: + additionalProperties: + format: int32 + type: integer + type: object + description: successful operation + tags: + - store + x-internal: false + /store/order: + post: + operationId: placeOrder + requestBody: + $ref: "#/components/requestBodies/Order" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: successful operation + "400": + description: Invalid Order + tags: + - store + x-internal: false + /store/order/{orderId}: + delete: + operationId: deleteOrder + parameters: + - $ref: "#/components/parameters/orderId" + tags: + - store + x-internal: false + get: + operationId: getOrderById + parameters: + - $ref: "#/components/parameters/orderId" + responses: + "200": + $ref: "#/components/responses/Order" + tags: + - store + x-internal: false + /user: + post: + operationId: createUser + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Created user object + required: true + responses: + default: + description: successful operation + tags: + - user + x-internal: false + /user/createWithList: + post: + operationId: createUsersWithListInput + requestBody: + $ref: "#/components/requestBodies/UserArray" + tags: + - user + x-internal: false + /user/login: + get: + operationId: loginUser + parameters: + - description: The user name for login + explode: true + in: query + name: username + required: true + schema: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + style: form + - description: The password for login in clear text + explode: true + in: query + name: password + required: true + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + type: string + description: successful operation + headers: + X-Rate-Limit: + description: calls per hour allowed by the user + explode: false + schema: + format: int32 + type: integer + style: simple + x-internal: true + "400": + description: Invalid username/password supplied + tags: + - user + x-internal: false + /user/logout: + get: + operationId: logoutUser + tags: + - user + x-internal: false + /user/{username}: + delete: + operationId: deleteUser + parameters: + - description: The name that needs to be deleted + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + tags: + - user + x-internal: false + get: + operationId: getUserByName + parameters: + - description: The name that needs to be fetched. Use user1 for testing. + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: successful operation + tags: + - user + x-internal: false + put: + operationId: updateUser + parameters: + - description: name that need to be deleted + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Updated user object + required: true + responses: + "400": + description: Invalid user supplied + "404": + description: User not found + tags: + - user + x-internal: false +components: + parameters: + orderId: + explode: false + in: path + name: orderId + required: true + schema: + type: string + style: simple + offset: + deprecated: true + explode: true + in: query + name: offset + required: false + schema: + type: integer + style: form + x-internal: true + requestBodies: + UserArray: + content: + application/json: + schema: + items: + $ref: "#/components/schemas/User" + type: array + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + Order: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: order placed for purchasing the pet + required: true + responses: + Order: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: successful operation + schemas: + Order: + description: An order for a pets from the pet store + properties: + id: + format: int64 + type: integer + petId: + format: int64 + type: integer + quantity: + format: int32 + type: integer + shipDate: + format: date-time + type: string + status: + description: Order Status + enum: + - placed + - approved + - delivered + type: string + complete: + default: false + type: boolean + title: Pet Order + type: object + Category: + description: A category for a pet + properties: + id: + format: int64 + type: integer + name: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + title: Pet category + type: object + User: + description: A User who is purchasing from the pet store + properties: + id: + format: int64 + type: integer + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + description: User Status + format: int32 + type: integer + title: a User + type: object + Tag: + description: A tag for a pet + properties: + id: + format: int64 + type: integer + name: + type: string + title: Pet Tag + type: object + Pet: + description: A pet for sale in the pet store + properties: + id: + format: int64 + type: integer + category: + $ref: "#/components/schemas/Category" + name: + example: doggie + type: string + photoUrls: + items: + type: string + type: array + tags: + items: + $ref: "#/components/schemas/Tag" + type: array + status: + deprecated: true + description: pet status in the store + enum: + - available + - pending + - sold + type: string + required: + - name + - photoUrls + title: a Pet + type: object + ApiResponse: + description: Describes the result of uploading an image resource + properties: + code: + format: int32 + type: integer + type: + type: string + message: + type: string + title: An uploaded response + type: object diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_filter_role_guest_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_filter_role_guest_config.yaml new file mode 100644 index 000000000000..53cf7518c78d --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_filter_role_guest_config.yaml @@ -0,0 +1,5 @@ +# remove the elements having deprecated: true +inputSpec: src/test/resources/openapi_normalizer/petstore.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_filter_role_guest.yaml +inputRules: + FILTER: 'x-role: guest' diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_normalized.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_normalized.yaml index 1481e19d5c6a..d9fdb32074a4 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_normalized.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeDeprecated_normalized.yaml @@ -74,6 +74,7 @@ paths: summary: Deletes a pet tags: - pet + x-role: admin get: operationId: getPetById parameters: @@ -128,6 +129,7 @@ paths: description: Invalid input tags: - pet + x-role: admin /store/order: post: operationId: placeOrder diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_normalized.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_normalized.yaml index d2e9ff06866d..f4c4b4f2172b 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_normalized.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeTags_normalized.yaml @@ -67,6 +67,7 @@ paths: type: string type: array style: form + x-role: admin responses: "200": content: @@ -114,6 +115,7 @@ paths: description: successful operation "400": description: Invalid tag value + x-role: guest /pet/{petId}: delete: description: "" @@ -139,6 +141,7 @@ paths: "400": description: Invalid pet value summary: Deletes a pet + x-role: admin get: operationId: getPetById parameters: @@ -189,6 +192,7 @@ paths: responses: "405": description: Invalid input + x-role: admin /store/inventory: get: deprecated: true diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_normalized.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_normalized.yaml index 2aba5a9875fc..cd7f2e14ff90 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_normalized.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeUnused_normalized.yaml @@ -64,6 +64,7 @@ paths: type: string type: array style: form + x-role: admin responses: "200": content: @@ -105,6 +106,7 @@ paths: description: Invalid tag value tags: - pet + x-role: guest /pet/{petId}: delete: description: "" @@ -132,6 +134,7 @@ paths: summary: Deletes a pet tags: - pet + x-role: admin get: operationId: getPetById parameters: @@ -186,6 +189,7 @@ paths: description: Invalid input tags: - pet + x-role: admin components: requestBodies: Pet: diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_normalized.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_normalized.yaml index d8a2b36cc696..6b6d6440acff 100644 --- a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_normalized.yaml +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_tags_normalized.yaml @@ -74,6 +74,7 @@ paths: type: string type: array style: form + x-role: admin responses: "200": content: @@ -125,6 +126,7 @@ paths: description: Invalid tag value tags: - pet + x-role: guest /pet/{petId}: delete: description: "" @@ -152,6 +154,7 @@ paths: summary: Deletes a pet tags: - pet + x-role: admin get: operationId: getPetById parameters: @@ -206,6 +209,7 @@ paths: description: Invalid input tags: - pet + x-role: admin components: parameters: offset: From f0f0d7cab1c777df48bb6d4c384393e04f44c1b7 Mon Sep 17 00:00:00 2001 From: jpfinne Date: Sun, 9 Nov 2025 13:28:37 +0100 Subject: [PATCH 15/16] Fix build (using Locale.ROOT) --- .../main/java/org/openapitools/codegen/OpenAPINormalizer.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index e8c820243ad8..b2254e161cc5 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -2139,7 +2139,7 @@ private void doParse() { operationIdFilters = parsedFilters; } else if (METHOD.equals(filterKey)) { methodFilters = parsedFilters.stream() - .map(String::toUpperCase) + .map(method -> method.toUpperCase(Locale.ROOT)) .map(PathItem.HttpMethod::valueOf) .collect(Collectors.toSet()); } else if (TAG.equals(filterKey)) { @@ -2215,7 +2215,7 @@ public void apply(String path, PathItem pathItem, Map Date: Sun, 9 Nov 2025 18:52:30 +0100 Subject: [PATCH 16/16] Add REMOVE_FILTER cookies, headers and queryParams --- docs/customization.md | 17 +- .../codegen/OpenAPINormalizer.java | 140 +++-- .../petstore_removeQueryParams.yaml | 480 ++++++++++++++++++ .../petstore_removeQueryParams_config.yaml | 5 + 4 files changed, 607 insertions(+), 35 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeQueryParams.yaml create mode 100644 modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeQueryParams_config.yaml diff --git a/docs/customization.md b/docs/customization.md index fc695c6b22a6..f93151764aa5 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -739,9 +739,9 @@ The `REMOVE_FILTER` parameter allows the removal of elements in an openAPI docum - **`deprecated`** When specified as `deprecated` or set to `deprecated:true`, all operations, schemas, properties and parameters marked with `deprecated: true` are removed. -- **`removeTags`** +- **`removeTags`** When specified as `removeTags` or set to `removeTags:true`, all tags are removed from the operations. - When set to `removeTags:store|user` all tags whose name is `store` or `user` are removed. + When set to `removeTags:store|user` all tags named is `store` or `user` are removed. When set to `removeTags:keepOnlyFirstTag` perform the KEEP_ONLY_FIRST_TAG_IN_OPERATION normalization - **`removeVendorExtensions`** @@ -750,11 +750,22 @@ The `REMOVE_FILTER` parameter allows the removal of elements in an openAPI docum - **`x-`** When specified as `x-role`, remove operations, schemas, properties and parameters having a vendor extension `x-role`. - When set to `x-role:admin|superuser`, remove operations, schemas, properties and parameters marked with vendorExtension `x-role:admin` or `x-role:superuser`. + When set to `x-role:admin|superuser`, remove operations, schemas, properties and parameters marked with vendorExtension `x-role: admin` or `x-role: superuser`. - **`tags`** When set as `tags:user|store`, remove operations marked with tags: user or store. +- **`headers`** + When set as `headers`, remove all headers. + When set as `headers:x-api-key`, remove header parameters named `x-api-key`. + +- **`cookies`** + When set as `cookies`, remove all cookies. + When set as `cookies:x-api-key`, remove cookie parameters named `x-api-key`. + +- **`queryParams`** + When set as `queryParams:offset|limit`, remove request parameters named `offset` or `limit`.`. + - **`unused`** When specified as `unused` or set to `unused:true`, remove all unused schemas, tags, requestBodies, responses and parameters. Optionally set individual options like in `unused:schemas|tags|requestBodies|responses|parameters` diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index b2254e161cc5..8dc0b8033c3a 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -436,6 +436,7 @@ protected void processRemoveItems() { } processRemoveEmptyPath(); processRemoveUnusedRequestBodies(); + processRemoveEmptyResponseHeaders(); processRemoveUnusedResponses(); processRemoveUnusedParameters(); processRemoveSchemas(); @@ -486,9 +487,25 @@ protected void processRemoveUnusedRequestBodies() { } } + protected void processRemoveEmptyResponseHeaders() { + if (openAPI.getComponents() != null && openAPI.getComponents().getResponses() != null) { + openAPI.getComponents().getResponses() + .forEach((key, value) -> processEmptyResponseHeaders(value)); + } + if (openAPI.getPaths()!= null) { + openAPI.getPaths().values().stream() + .flatMap(pathItem -> pathItem.readOperations().stream()) + .map(Operation::getResponses) + .filter(Objects::nonNull) + .map(ApiResponses::values) + .flatMap(responses -> responses.stream()) + .forEach(this::processEmptyResponseHeaders); + } + } + protected void processRemoveUnusedResponses() { if (removeFilter.unusedResponses) { - if (openAPI.getComponents() != null && openAPI.getComponents().getResponses() != null) { + if (openAPI.getComponents() != null && openAPI.getComponents().getResponses() != null && openAPI.getPaths() != null) { Set unusedResponses = openAPI.getPaths().values().stream() .flatMap(pathItem -> pathItem.readOperations().stream()) .map(Operation::getResponses) @@ -509,6 +526,12 @@ protected void processRemoveUnusedResponses() { } } + protected void processEmptyResponseHeaders(ApiResponse response) { + if (response.getHeaders() != null && response.getHeaders().isEmpty()) { + response.setHeaders(null); + } + } + protected void processRemoveUnusedParameters() { if (removeFilter.unusedParameters) { if (openAPI.getComponents() != null && openAPI.getComponents().getParameters() != null) { @@ -790,6 +813,14 @@ protected void normalizeHeaders(Map headers) { return; } + if (removeFilter.hasFilter()) { + Set headersToRemove = headers.entrySet().stream() + .filter(entry -> removeFilter.matchHeader(entry.getKey(), ModelUtils.getReferencedHeader(openAPI, entry.getValue()))) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + headers.keySet().removeIf(key -> headersToRemove.contains(key)); + } + for (String headerKey : headers.keySet()) { Header h = headers.get(headerKey); Schema updatedHeader = normalizeSchema(h.getSchema(), new HashSet<>()); @@ -2232,19 +2263,28 @@ public void applyParameter(String path, PathItem pathItem, PathItem.HttpMethod m return; } if (operation != null) { - if (!matchExtension(parameterExtensions)) { + if (!matchParameterExtension(path, method, operation, parameter)) { parameter.addExtension(X_INTERNAL, true); String operationInfo = getOperationInfo(path, pathItem, method, operation); - getLogger().info("Parameter `{}` in `{}` marked as internal (x-internal: true) by the {} FILTER", parameter.getName(), operationInfo, this.vendorExtensions); + getLogger().info("Parameter `{}` in `{}` marked as internal (x-internal: true) by FILTER", parameter.getName(), operationInfo); } } else { - if (!matchExtension(parameterExtensions)) { + if (!matchParameterExtension(path, parameter)) { parameter.addExtension(X_INTERNAL, true); - getLogger().info("Parameter `{}` in `{}` marked as internal (x-internal: true) by the {} FILTER", parameter.getName(), path, this.vendorExtensions); + getLogger().info("Parameter `{}` in `{}` marked as internal (x-internal: true) by FILTER", parameter.getName(), path); } } } + protected boolean matchParameterExtension(String path, PathItem.HttpMethod method, Operation operation, Parameter parameter) { + return matchExtension(parameter.getExtensions()); + } + + protected boolean matchParameterExtension(String path, Parameter parameter) { + return matchExtension(parameter.getExtensions()); + } + + protected boolean logIfMatch(Object filterName, String operation, boolean filterMatched) { if (filterMatched) { logIfMatch(filterName, operation); @@ -2253,7 +2293,7 @@ protected boolean logIfMatch(Object filterName, String operation, boolean filter } protected void logMismatch(String operation) { - getLogger().info("operation `{}` marked as internal only (x-internal: true) by FILTER", operation); + getLogger().debug("operation `{}` marked as internal only (x-internal: true) by FILTER", operation); } @@ -2318,7 +2358,16 @@ protected void processNormalizeOtherThanObjectWithProperties(Schema schema) { } public static class RemoveFilter { - private final static Set ANYSET = new HashSet<>(); + private final static Set ANYSET = Collections.singleton("any"); + public static final String REMOVE_TAGS = "removeTags"; + public static final String REMOVE_VENDOR_EXTENSIONS = "removeVendorExtensions"; + public static final String DEPRECATED = "deprecated"; + public static final String UNUSED = "unused"; + public static final String TAGS = "tags"; + public static final String INTERNAL = "internal"; + public static final String HEADERS = "headers"; + private static final String QUERYPARAMS = "queryParams"; + private static final String COOKIES = "cookies"; private final String filters; private boolean hasFilter; @@ -2338,6 +2387,9 @@ public static class RemoveFilter { protected boolean keepOnlyFirstTagInOperation; protected Set removeTags = Collections.emptySet(); + protected Set headers = Collections.emptySet(); + protected Set cookies = Collections.emptySet(); + protected Set queryParams = Collections.emptySet(); protected boolean deprecated; protected Set tags = Collections.emptySet(); protected Map> vendorExtensions = new HashMap<>(); @@ -2363,7 +2415,7 @@ public boolean parse() { doParse(); return hasFilter(); } catch (RuntimeException e) { - String message = String.format(Locale.ROOT, "FILTER rule [%s] `. Error: %s", + String message = String.format(Locale.ROOT, "REMOVE_FILTER rule [%s] `. Error: %s", filters, e.getMessage()); throw new IllegalArgumentException(message); } @@ -2384,7 +2436,6 @@ private void doParse() { String filterKey = filterStrs[0].trim(); String filterValue; if (isAny) { - filterStrs = new String[] { filterStrs[0], "true" }; isTrue = true; isBoolean = true; set = Collections.emptySet(); @@ -2395,9 +2446,11 @@ private void doParse() { isTrue = Boolean.parseBoolean(filterValue); isBoolean = "false".equals(filterValue) || isTrue; } + + Set anyOrSet = isAny? getAnySet(isTrue): set; hasFilter = true; - if ("internal".equals(filterKey)) { + if (INTERNAL.equals(filterKey)) { if (isBoolean) { internalOperations = isTrue; internalSchemas = isTrue; @@ -2409,9 +2462,9 @@ private void doParse() { internalProperties = set.contains("properties"); internalParameters = set.contains("parameters"); } - } else if ("deprecated".equals(filterKey)) { + } else if (DEPRECATED.equals(filterKey)) { this.deprecated = isTrue; - } else if ("unused".equals(filterKey)) { + } else if (UNUSED.equals(filterKey)) { if (isBoolean) { unusedSchemas = isTrue; unusedTags = isTrue; @@ -2420,34 +2473,34 @@ private void doParse() { unusedParameters = isTrue; } else { unusedSchemas = set.contains("schemas"); - unusedTags = set.contains("tags"); + unusedTags = set.contains(TAGS); unusedRequestBodies = set.contains("requestBodies"); unusedResponses = set.contains("responses"); unusedParameters = set.contains("parameters"); } } else if (REMOVE_X_INTERNAL.equals(filterKey)) { removeXInternal = isTrue; - } else if ("removeVendorExtensions".equals(filterKey)) { + } else if (REMOVE_VENDOR_EXTENSIONS.equals(filterKey)) { if (isAny) { removeXInternal = isTrue; - removeExtensions = getAnySet(isTrue); - } else { - removeExtensions = set; } + removeExtensions = anyOrSet; } else if (KEEP_ONLY_FIRST_TAG_IN_OPERATION.equals(filterKey)) { keepOnlyFirstTagInOperation = isTrue; - } else if ("removeTags".equals(filterKey)) { + } else if (REMOVE_TAGS.equals(filterKey)) { if ("keepOnlyFirstTag".equals(filterValue)) { keepOnlyFirstTagInOperation = true; } else { - if (isAny) { - removeTags = getAnySet(isTrue); - } else { - removeTags = set; - } + removeTags = anyOrSet; } - } else if ("tags".equals(filterKey)) { + } else if (TAGS.equals(filterKey)) { tags = set; + } else if (HEADERS.equals(filterKey)) { + headers = anyOrSet; + } else if (QUERYPARAMS.equals(filterKey)) { + queryParams = anyOrSet; + } else if (COOKIES.equals(filterKey)) { + cookies = anyOrSet; } else if (filterKey.startsWith("x-")) { vendorExtensions.put(filterKey, isAny ? getAnySet(isTrue) : set); } else { @@ -2647,10 +2700,39 @@ protected boolean matchParameter(Parameter parameter) { if (deprecated && Boolean.TRUE.equals(parameter.getDeprecated())) { return true; } + String in = parameter.getIn(); + if ("query".equals(in) && matchParameter(parameter, this.queryParams)) { + return true; + } + if ("cookie".equals(in) && matchParameter(parameter, this.cookies)) { + return true; + } + if ("header".equals(in) && matchParameter(parameter, this.headers)) { + return true; + } return matchExtension(parameter.getExtensions()); } + private boolean matchParameter(Parameter parameter, Set params) { + return matchValue(parameter.getName(), params); + } + + private boolean matchValue(Object value, Set valuesToMatch) { + if (isEmpty(valuesToMatch)) { + return false; + } + if (valuesToMatch == ANYSET) { + return true; + } + return valuesToMatch.contains(value); + + } + + protected boolean matchHeader(String key, Header header) { + return matchValue(key, this.headers) || matchExtension(header.getExtensions()); + } + private boolean matchExtension(Map extensions) { if (extensions == null || extensions.isEmpty() || vendorExtensions.isEmpty()) { return false; @@ -2660,15 +2742,9 @@ private boolean matchExtension(Map extensions) { .anyMatch(this::matchExtension); } - protected boolean matchExtension(Map.Entry entry) { + private boolean matchExtension(Map.Entry entry) { Set set = vendorExtensions.get(entry.getKey()); - if (set == null) { - return false; - } - if (set == ANYSET) { - return true; - } - return set.contains(entry.getValue()); + return matchValue(entry.getValue(), set); } private Set getAnySet(boolean isTrue) { diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeQueryParams.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeQueryParams.yaml new file mode 100644 index 000000000000..a84918b2abe5 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeQueryParams.yaml @@ -0,0 +1,480 @@ +openapi: 3.0.0 +info: + description: This is a sample server Petstore server. + version: 1.0.0 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +servers: + - url: http://petstore.swagger.io/v2 +tags: + - description: Everything about your Pets + name: pet + - description: Access to Petstore orders + name: store + - description: Operations about user + name: user +paths: + /pet: + post: + operationId: addPet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "405": + description: Invalid input + tags: + - pet + put: + externalDocs: + description: API documentation for the updatePet operation + url: http://petstore.swagger.io/v2/doc/updatePet + operationId: updatePet + requestBody: + $ref: "#/components/requestBodies/Pet" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + tags: + - pet + /pet/findByStatus: + get: + deprecated: true + operationId: findPetsByStatus + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + "400": + description: Invalid status value + tags: + - pet + /pet/findByTags: + get: + deprecated: true + operationId: findPetsByTags + parameters: + - description: Tags to filter by + explode: false + in: query + name: tags + required: true + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: "#/components/schemas/Pet" + type: array + description: successful operation + "400": + description: Invalid tag value + tags: + - pet + /pet/{petId}: + delete: + description: "" + operationId: deletePet + parameters: + - description: Pet id to delete + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "400": + description: Invalid pet value + summary: Deletes a pet + tags: + - pet + get: + operationId: getPetById + parameters: + - description: ID of pet to return + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: successful operation + "400": + description: Invalid ID supplied + "404": + description: Pet not found + tags: + - pet + post: + operationId: updatePetWithForm + parameters: + - description: ID of pet that needs to be updated + explode: false + in: path + name: petId + required: true + schema: + format: int64 + type: integer + style: simple + requestBody: + content: + application/x-www-form-urlencoded: + schema: + properties: + name: + description: Updated name of the pet + type: string + status: + description: Updated status of the pet + type: string + type: object + responses: + "405": + description: Invalid input + tags: + - pet + /store/inventory: + get: + deprecated: true + operationId: getInventory + responses: + "200": + content: + application/json: + schema: + additionalProperties: + format: int32 + type: integer + type: object + description: successful operation + tags: + - store + /store/order: + post: + operationId: placeOrder + requestBody: + $ref: "#/components/requestBodies/Order" + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: successful operation + "400": + description: Invalid Order + tags: + - store + /store/order/{orderId}: + delete: + operationId: deleteOrder + parameters: + - $ref: "#/components/parameters/orderId" + tags: + - store + get: + operationId: getOrderById + parameters: + - $ref: "#/components/parameters/orderId" + responses: + "200": + $ref: "#/components/responses/Order" + tags: + - store + /user: + post: + operationId: createUser + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Created user object + required: true + responses: + default: + description: successful operation + tags: + - user + /user/createWithList: + post: + operationId: createUsersWithListInput + requestBody: + $ref: "#/components/requestBodies/UserArray" + tags: + - user + /user/login: + get: + operationId: loginUser + parameters: + - description: The user name for login + explode: true + in: query + name: username + required: true + schema: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + style: form + - description: The password for login in clear text + explode: true + in: query + name: password + required: true + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + type: string + description: successful operation + "400": + description: Invalid username/password supplied + tags: + - user + /user/logout: + get: + operationId: logoutUser + tags: + - user + /user/{username}: + delete: + operationId: deleteUser + parameters: + - description: The name that needs to be deleted + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + tags: + - user + get: + operationId: getUserByName + parameters: + - description: The name that needs to be fetched. Use user1 for testing. + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: successful operation + tags: + - user + put: + operationId: updateUser + parameters: + - description: name that need to be deleted + explode: false + in: path + name: username + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/User" + description: Updated user object + required: true + responses: + "400": + description: Invalid user supplied + "404": + description: User not found + tags: + - user +components: + parameters: + orderId: + explode: false + in: path + name: orderId + required: true + schema: + type: string + style: simple + requestBodies: + UserArray: + content: + application/json: + schema: + items: + $ref: "#/components/schemas/User" + type: array + description: List of user object + required: true + Pet: + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + description: Pet object that needs to be added to the store + required: true + Order: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: order placed for purchasing the pet + required: true + responses: + Order: + content: + application/json: + schema: + $ref: "#/components/schemas/Order" + description: successful operation + schemas: + Order: + description: An order for a pets from the pet store + properties: + id: + format: int64 + type: integer + petId: + format: int64 + type: integer + quantity: + format: int32 + type: integer + shipDate: + format: date-time + type: string + status: + description: Order Status + enum: + - placed + - approved + - delivered + type: string + complete: + default: false + type: boolean + title: Pet Order + type: object + Category: + description: A category for a pet + properties: + id: + format: int64 + type: integer + name: + pattern: "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" + type: string + title: Pet category + type: object + User: + description: A User who is purchasing from the pet store + properties: + id: + format: int64 + type: integer + username: + type: string + firstName: + type: string + lastName: + type: string + email: + type: string + password: + type: string + phone: + type: string + userStatus: + description: User Status + format: int32 + type: integer + title: a User + type: object + Tag: + description: A tag for a pet + properties: + id: + format: int64 + type: integer + name: + type: string + title: Pet Tag + type: object + Pet: + description: A pet for sale in the pet store + properties: + id: + format: int64 + type: integer + category: + $ref: "#/components/schemas/Category" + name: + example: doggie + type: string + photoUrls: + items: + type: string + type: array + tags: + items: + $ref: "#/components/schemas/Tag" + type: array + status: + deprecated: true + description: pet status in the store + enum: + - available + - pending + - sold + type: string + required: + - name + - photoUrls + title: a Pet + type: object diff --git a/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeQueryParams_config.yaml b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeQueryParams_config.yaml new file mode 100644 index 000000000000..a38e45cb6349 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/openapi_normalizer/petstore_removeQueryParams_config.yaml @@ -0,0 +1,5 @@ +# test a combination of FILTER and REMOVE_FILTER +inputSpec: src/test/resources/openapi_normalizer/petstore.yaml +normalizedSpec: src/test/resources/openapi_normalizer/petstore_removeQueryParams.yaml +inputRules: + REMOVE_FILTER: 'queryParams:limit|status|offset ; headers: api_key|role|X-Rate-Limit ; removeVendorExtensions ; unused'