Skip to content

Commit 4835072

Browse files
committed
Make Filter extensible
1 parent 148faca commit 4835072

3 files changed

Lines changed: 166 additions & 43 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ public class OpenAPINormalizer {
133133

134134
// when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else
135135
final String FILTER = "FILTER";
136-
Filter filter = new Filter();
137136

138137
// when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else
139138
final String SET_CONTAINER_TO_NULLABLE = "SET_CONTAINER_TO_NULLABLE";
@@ -272,16 +271,7 @@ public void processRules(Map<String, String> inputRules) {
272271

273272
if (inputRules.get(FILTER) != null) {
274273
rules.put(FILTER, true);
275-
String filters = inputRules.get(FILTER);
276-
try {
277-
filter = new Filter(filters);
278-
} catch (RuntimeException e) {
279-
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",
280-
filters, Filter.OPERATION_ID, Filter.METHOD, Filter.TAG, Filter.PATH, e.getMessage());
281-
// throw an exception. This is a breaking change compared to pre 7.16.0
282-
// Workaround: fix the syntax!
283-
throw new IllegalArgumentException(message);
284-
}
274+
// actual parsing is delayed to allow customization of the Filter processing
285275
}
286276

287277
if (inputRules.get(SET_CONTAINER_TO_NULLABLE) != null) {
@@ -327,6 +317,19 @@ public void processRules(Map<String, String> inputRules) {
327317
}
328318
}
329319

320+
/**
321+
* Create the filter to process the FILTER normalizer.
322+
* Override this to create a custom filter normalizer.
323+
*
324+
* @param openApi Contract used in the filtering (could be used for customization).
325+
* @param filters full FILTER value
326+
*
327+
* @return a Filter containing the parsed filters.
328+
*/
329+
protected Filter createFilter(OpenAPI openApi, String filters) {
330+
return new Filter(filters);
331+
}
332+
330333
/**
331334
* Normalizes the OpenAPI input, which may not perfectly conform to
332335
* the specification.
@@ -388,7 +391,10 @@ protected void normalizePaths() {
388391
"trace", PathItem::getTrace
389392
);
390393

391-
if (filter.hasFilter()) {
394+
if (Boolean.TRUE.equals(getRule(FILTER))) {
395+
String filters = inputRules.get(FILTER);
396+
Filter filter = createFilter(this.openAPI, filters);
397+
filter.parse();
392398
// Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem,
393399
// and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters.
394400
filter.apply(pathsEntry.getKey(), path, methodMap);
@@ -1796,33 +1802,52 @@ protected Schema processNormalize31Spec(Schema schema, Set<Schema> visitedSchema
17961802

17971803
// ===================== end of rules =====================
17981804

1799-
static class Filter {
1805+
protected static class Filter {
18001806
public static final String OPERATION_ID = "operationId";
18011807
public static final String METHOD = "method";
18021808
public static final String TAG = "tag";
18031809
public static final String PATH = "path";
1810+
private final String filters;
18041811
protected Set<String> operationIdFilters = Collections.emptySet();
18051812
protected Set<String> methodFilters = Collections.emptySet();
18061813
protected Set<String> tagFilters = Collections.emptySet();
18071814
protected Set<String> pathStartingWithFilters = Collections.emptySet();
18081815

1809-
Filter() {
1816+
protected Filter(String filters) {
1817+
this.filters = filters.trim();
1818+
}
18101819

1820+
/**
1821+
* Perform the parsing of the filter string.
1822+
*
1823+
* @return true if filters need to be processed
1824+
*/
1825+
public boolean parse() {
1826+
if (StringUtils.isEmpty(filters)) {
1827+
return false;
1828+
}
1829+
try {
1830+
doParse();
1831+
return hasFilter();
1832+
} catch (RuntimeException e) {
1833+
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",
1834+
filters, Filter.OPERATION_ID, Filter.METHOD, Filter.TAG, Filter.PATH, e.getMessage());
1835+
// throw an exception. This is a breaking change compared to pre 7.16.0
1836+
// Workaround: fix the syntax!
1837+
throw new IllegalArgumentException(message);
1838+
}
18111839
}
18121840

1813-
public Filter(String filters) {
1841+
private void doParse() {
18141842
for (String filter : filters.split(";")) {
18151843
filter = filter.trim();
18161844
String[] filterStrs = filter.split(":");
18171845
if (filterStrs.length != 2) { // only support filter with : at the moment
1818-
throw new IllegalArgumentException("filter not supported :[" + filters + "]");
1846+
throw new IllegalArgumentException("filter with no value not supported :[" + filter + "]");
18191847
} else {
18201848
String filterKey = filterStrs[0].trim();
18211849
String filterValue = filterStrs[1];
1822-
Set<String> parsedFilters = Arrays.stream(filterValue.split("[|]"))
1823-
.filter(Objects::nonNull)
1824-
.map(String::trim)
1825-
.collect(Collectors.toCollection(HashSet::new));
1850+
Set<String> parsedFilters = splitByPipe(filterValue);
18261851
if (OPERATION_ID.equals(filterKey)) {
18271852
operationIdFilters = parsedFilters;
18281853
} else if (METHOD.equals(filterKey)) {
@@ -1833,12 +1858,56 @@ public Filter(String filters) {
18331858
} else if (PATH.equals(filterKey)) {
18341859
pathStartingWithFilters = parsedFilters;
18351860
} else {
1836-
throw new IllegalArgumentException("filter not supported :[" + filters + "]");
1861+
parse(filterKey, filterValue);
18371862
}
18381863
}
18391864
}
18401865
}
18411866

1867+
/**
1868+
* Split the filterValue by pipe.
1869+
*
1870+
* @return the split values.
1871+
*/
1872+
protected Set<String> splitByPipe(String filterValue) {
1873+
return Arrays.stream(filterValue.split("[|]"))
1874+
.filter(Objects::nonNull)
1875+
.map(String::trim)
1876+
.collect(Collectors.toCollection(HashSet::new));
1877+
}
1878+
1879+
/**
1880+
* Parse non default filters.
1881+
*
1882+
* Override this method to add custom parsing logic.
1883+
*
1884+
* By default throws IllegalArgumentException.
1885+
*
1886+
* @param filterName name of the filter
1887+
* @param filterValue value of the filter
1888+
*/
1889+
protected void parse(String filterName, String filterValue) {
1890+
parseFails(filterName, filterValue);
1891+
}
1892+
1893+
protected void parseFails(String filterName, String filterValue) {
1894+
throw new IllegalArgumentException("filter not supported :[" + filterName + ":" + filterValue + "]");
1895+
}
1896+
1897+
/**
1898+
* Test if the OpenAPI contract match an extra filter.
1899+
*
1900+
* Override this method to add custom logic.
1901+
*
1902+
* @param operation Openapi Operation
1903+
* @param path Path of the operation
1904+
*
1905+
* @return true if the operation of path match the filter
1906+
*/
1907+
protected boolean hasCustomFilterMatch(String path, Operation operation) {
1908+
return false;
1909+
}
1910+
18421911
public boolean hasFilter() {
18431912
return !operationIdFilters.isEmpty() || !methodFilters.isEmpty() || !tagFilters.isEmpty() || !pathStartingWithFilters.isEmpty ();
18441913
}
@@ -1848,22 +1917,32 @@ public void apply(String path, PathItem pathItem, Map<String, Function<PathItem,
18481917
Operation operation = getter.apply(pathItem);
18491918
if (operation != null) {
18501919
boolean found = false;
1851-
found |= hasMatch(PATH, operation, hasPathStarting(path));
1852-
found |= hasMatch(TAG, operation, hasTag(operation));
1853-
found |= hasMatch(OPERATION_ID, operation, hasOperationId(operation));
1854-
found |= hasMatch(METHOD, operation, hasMethod(method));
1920+
found |= logIfMatch(PATH, operation, hasPathStarting(path));
1921+
found |= logIfMatch(TAG, operation, hasTag(operation));
1922+
found |= logIfMatch(OPERATION_ID, operation, hasOperationId(operation));
1923+
found |= logIfMatch(METHOD, operation, hasMethod(method));
1924+
found |= hasCustomFilterMatch(path, operation);
1925+
18551926
operation.addExtension(X_INTERNAL, !found);
18561927
}
18571928
});
18581929
}
18591930

1860-
private boolean hasMatch(String filterName, Operation operation, boolean filterMatched) {
1931+
protected boolean logIfMatch(String filterName, Operation operation, boolean filterMatched) {
18611932
if (filterMatched) {
1862-
OpenAPINormalizer.LOGGER.info("operation `{}` marked as internal only (x-internal: true) by the {} FILTER", operation.getOperationId(), filterName);
1933+
logMatch(filterName, operation);
18631934
}
18641935
return filterMatched;
18651936
}
18661937

1938+
protected void logMatch(String filterName, Operation operation) {
1939+
getLogger().info("operation `{}` marked as internal only (x-internal: true) by the {} FILTER", operation.getOperationId(), filterName);
1940+
}
1941+
1942+
protected Logger getLogger() {
1943+
return OpenAPINormalizer.LOGGER;
1944+
}
1945+
18671946
private boolean hasPathStarting(String path) {
18681947
return pathStartingWithFilters.stream().anyMatch(filter -> path.startsWith(filter));
18691948
}
@@ -1879,5 +1958,6 @@ private boolean hasOperationId(Operation operation) {
18791958
private boolean hasMethod(String method) {
18801959
return methodFilters.contains(method);
18811960
}
1961+
18821962
}
18831963
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.openapitools.codegen;
1818

1919
import io.swagger.v3.oas.models.OpenAPI;
20+
import io.swagger.v3.oas.models.Operation;
2021
import io.swagger.v3.oas.models.PathItem;
2122
import io.swagger.v3.oas.models.media.*;
2223
import io.swagger.v3.oas.models.parameters.Parameter;
@@ -622,7 +623,6 @@ public void testOperationIdFilter() {
622623

623624
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions(), null);
624625
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true);
625-
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null);
626626

627627
Map<String, String> options = Map.of("FILTER", "operationId:delete|list");
628628
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options);
@@ -639,7 +639,6 @@ public void testFilterWithMethod() {
639639

640640
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions(), null);
641641
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true);
642-
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null);
643642

644643
Map<String, String> options = Map.of("FILTER", "method:get");
645644
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options);
@@ -650,43 +649,45 @@ public void testFilterWithMethod() {
650649
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true);
651650
}
652651

652+
static OpenAPINormalizer.Filter parseFilter(String filters) {
653+
OpenAPINormalizer.Filter filter = new OpenAPINormalizer.Filter(filters);
654+
filter.parse();
655+
return filter;
656+
}
657+
653658
@Test
654659
public void testFilterParsing() {
655660
OpenAPINormalizer.Filter filter;
656661

657-
// default
658-
filter = new OpenAPINormalizer.Filter();
659-
assertFalse(filter.hasFilter());
660-
661662
// no filter
662-
filter = new OpenAPINormalizer.Filter();
663+
filter = parseFilter(" ");
663664
assertFalse(filter.hasFilter());
664665

665666
// invalid filter
666667
assertThrows(IllegalArgumentException.class, () ->
667-
new OpenAPINormalizer.Filter("operationId:"));
668+
parseFilter("operationId:"));
668669

669670
assertThrows(IllegalArgumentException.class, () ->
670-
new OpenAPINormalizer.Filter("invalid:invalid:"));
671+
parseFilter("invalid:invalid:"));
671672

672673
// extra spaces are trimmed
673-
filter = new OpenAPINormalizer.Filter("method:\n\t\t\t\tget");
674+
filter = parseFilter("method:\n\t\t\t\tget");
674675
assertTrue(filter.hasFilter());
675676
assertEquals(filter.methodFilters, Set.of("get"));
676677
assertTrue(filter.operationIdFilters.isEmpty());
677678
assertTrue(filter.tagFilters.isEmpty());
678679
assertTrue(filter.pathStartingWithFilters.isEmpty());
679680

680681
// multiple values separated by pipe
681-
filter = new OpenAPINormalizer.Filter("operationId:\n\t\t\t\tdelete|\n\t\tlist\t");
682+
filter = parseFilter("operationId:\n\t\t\t\tdelete|\n\t\tlist\t");
682683
assertTrue(filter.hasFilter());
683684
assertTrue(filter.methodFilters.isEmpty());
684685
assertEquals(filter.operationIdFilters, Set.of("delete", "list"));
685686
assertTrue(filter.tagFilters.isEmpty());
686687
assertTrue(filter.pathStartingWithFilters.isEmpty());
687688

688689
// multiple filters
689-
filter = new OpenAPINormalizer.Filter("operationId:delete|list;path:/v1");
690+
filter = parseFilter("operationId:delete|list;path:/v1");
690691
assertTrue(filter.hasFilter());
691692
assertTrue(filter.methodFilters.isEmpty());
692693
assertEquals(filter.operationIdFilters, Set.of("delete", "list"));
@@ -696,7 +697,7 @@ public void testFilterParsing() {
696697

697698
@Test
698699
public void testMultiFilterParsing() {
699-
OpenAPINormalizer.Filter filter = new OpenAPINormalizer.Filter("operationId: delete| list ; tag : testA |testB ");
700+
OpenAPINormalizer.Filter filter = parseFilter("operationId: delete| list ; tag : testA |testB ");
700701
assertEquals(filter.operationIdFilters, Set.of("delete", "list"));
701702
assertEquals(filter.tagFilters, Set.of("testA", "testB"));
702703
}
@@ -707,7 +708,6 @@ public void testFilterWithTag() {
707708

708709
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions(), null);
709710
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true);
710-
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions(), null);
711711

712712
Map<String, String> options = Map.of("FILTER", "tag:basic");
713713
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options);
@@ -718,16 +718,57 @@ public void testFilterWithTag() {
718718
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), true);
719719
}
720720

721+
@Test
722+
public void testCustomRoleFilter() {
723+
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml");
724+
725+
Map<String, String> options = Map.of("FILTER", "role:admin");
726+
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options) {
727+
@Override
728+
protected Filter createFilter(OpenAPI openApi, String filters) {
729+
return new CustomRoleFilter(filters);
730+
}
731+
};
732+
openAPINormalizer.normalize();
733+
734+
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getGet().getExtensions().get(X_INTERNAL), true);
735+
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getDelete().getExtensions().get(X_INTERNAL), true);
736+
assertEquals(openAPI.getPaths().get("/person/display/{personId}").getPut().getExtensions().get(X_INTERNAL), false);
737+
}
738+
739+
private class CustomRoleFilter extends OpenAPINormalizer.Filter {
740+
private Set<String> filteredRoles;
741+
742+
public CustomRoleFilter(String filters) {
743+
super(filters);
744+
}
745+
746+
747+
@Override
748+
protected void parse(String filterName, String filterValue) {
749+
if ("role".equals(filterName)) {
750+
this.filteredRoles = splitByPipe(filterValue);
751+
} else {
752+
parseFails(filterName, filterValue);
753+
}
754+
}
755+
756+
@Override
757+
protected boolean hasCustomFilterMatch(String path, Operation operation) {
758+
return operation.getExtensions() != null && filteredRoles.contains(operation.getExtensions().get("x-role"));
759+
}
760+
}
761+
721762
@Test
722763
public void testFilterInvalidDoesThrow() {
723764
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml");
724765

725766
Map<String, String> options = Map.of("FILTER", "tag ; invalid");
726767
try {
727-
new OpenAPINormalizer(openAPI, options);
768+
new OpenAPINormalizer(openAPI, options).normalize();
728769
fail("Expected IllegalArgumentException");
729770
} catch (IllegalArgumentException e) {
730-
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]");
771+
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]");
731772
}
732773
}
733774

@@ -1213,4 +1254,5 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
12131254
return super.normalizeSchema(schema, visitedSchemas);
12141255
}
12151256
}
1257+
12161258
}

modules/openapi-generator/src/test/resources/3_0/enableKeepOnlyFirstTagInOperation_test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ paths:
5959
schema:
6060
$ref: "#/components/schemas/Person"
6161
put:
62+
x-role: admin
6263
tags:
6364
- person
6465
parameters:

0 commit comments

Comments
 (0)