From 99c9751d2eaa192ca3ae6c1c1e52d46a15a8abbf Mon Sep 17 00:00:00 2001 From: jpfinne Date: Mon, 13 Oct 2025 19:08:16 +0200 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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() {