Skip to content

Commit 88bba25

Browse files
authored
OpenApiNormalizer FILTER improvements. Multi filters + filter on path (#22128)
* Use Filter Parser and allow multiple filters * OpenAPINormalizer fails for invalid FILTER syntax * Fix typo * Use constants for filter keys. Improve exception handling and tests. * Fix format missing Locale.ROOT * Make Filter extensible * Additional unit test for invalid filter
1 parent 8c85e3c commit 88bba25

File tree

4 files changed

+317
-108
lines changed

4 files changed

+317
-108
lines changed

docs/customization.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -645,7 +645,7 @@ java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generat
645645
646646
- `FILTER`
647647
648-
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.
648+
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.
649649
650650
### Available Filters
651651
@@ -658,14 +658,17 @@ The `FILTER` parameter allows selective inclusion of API operations based on spe
658658
- **`tag`**
659659
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.
660660
661+
- **`path`**
662+
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.
663+
661664
### Example Usage
662665
663666
```sh
664667
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \
665668
-g java \
666669
-i modules/openapi-generator/src/test/resources/3_0/petstore.yaml \
667670
-o /tmp/java-okhttp/ \
668-
--openapi-normalizer FILTER="operationId:addPet|getPetById"
671+
--openapi-normalizer FILTER="operationId:addPet|getPetById ; tag:store"
669672
```
670673

671674
- `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.

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

Lines changed: 184 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public class OpenAPINormalizer {
4949

5050
private TreeSet<String> anyTypeTreeSet = new TreeSet<>();
5151

52-
protected final Logger LOGGER = LoggerFactory.getLogger(OpenAPINormalizer.class);
52+
protected static final Logger LOGGER = LoggerFactory.getLogger(OpenAPINormalizer.class);
5353

5454
Set<String> ruleNames = new TreeSet<>();
5555
Set<String> rulesDefaultToTrue = new TreeSet<>();
@@ -133,10 +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-
HashSet<String> operationIdFilters = new HashSet<>();
137-
HashSet<String> methodFilters = new HashSet<>();
138-
139-
HashSet<String> tagFilters = new HashSet<>();
140136

141137
// when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else
142138
final String SET_CONTAINER_TO_NULLABLE = "SET_CONTAINER_TO_NULLABLE";
@@ -275,30 +271,7 @@ public void processRules(Map<String, String> inputRules) {
275271

276272
if (inputRules.get(FILTER) != null) {
277273
rules.put(FILTER, true);
278-
279-
String[] filterStrs = inputRules.get(FILTER).split(":");
280-
if (filterStrs.length != 2) { // only support operationId with : at the moment
281-
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));
282-
} else {
283-
if ("operationId".equals(filterStrs[0])) {
284-
operationIdFilters = Arrays.stream(filterStrs[1].split("[|]"))
285-
.filter(Objects::nonNull)
286-
.map(String::trim)
287-
.collect(Collectors.toCollection(HashSet::new));
288-
} else if ("method".equals(filterStrs[0])) {
289-
methodFilters = Arrays.stream(filterStrs[1].split("[|]"))
290-
.filter(Objects::nonNull)
291-
.map(String::trim)
292-
.collect(Collectors.toCollection(HashSet::new));
293-
} else if ("tag".equals(filterStrs[0])) {
294-
tagFilters = Arrays.stream(filterStrs[1].split("[|]"))
295-
.filter(Objects::nonNull)
296-
.map(String::trim)
297-
.collect(Collectors.toCollection(HashSet::new));
298-
} else {
299-
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));
300-
}
301-
}
274+
// actual parsing is delayed to allow customization of the Filter processing
302275
}
303276

304277
if (inputRules.get(SET_CONTAINER_TO_NULLABLE) != null) {
@@ -344,6 +317,19 @@ public void processRules(Map<String, String> inputRules) {
344317
}
345318
}
346319

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+
347333
/**
348334
* Normalizes the OpenAPI input, which may not perfectly conform to
349335
* the specification.
@@ -405,15 +391,15 @@ protected void normalizePaths() {
405391
"trace", PathItem::getTrace
406392
);
407393

408-
// Iterates over each HTTP method in methodMap, retrieves the corresponding Operation from the PathItem,
409-
// and marks it as internal (`x-internal`) if the method is not in methodFilters.
410-
methodMap.forEach((method, getter) -> {
411-
Operation operation = getter.apply(path);
412-
if (operation != null && !methodFilters.isEmpty()) {
413-
LOGGER.info("operation `{}` marked internal only (x-internal: `{}`) by the method FILTER", operation.getOperationId(), !methodFilters.contains(method));
414-
operation.addExtension("x-internal", !methodFilters.contains(method));
394+
if (Boolean.TRUE.equals(getRule(FILTER))) {
395+
String filters = inputRules.get(FILTER);
396+
Filter filter = createFilter(this.openAPI, filters);
397+
if (filter.parse()) {
398+
// Iterates over each HTTP method in methodMap, retrieves the corresponding Operations from the PathItem,
399+
// and marks it as internal (`x-internal=true`) if the method/operationId/tag/path is not in the filters.
400+
filter.apply(pathsEntry.getKey(), path, methodMap);
415401
}
416-
});
402+
}
417403

418404
// Include callback operation as well
419405
for (Operation operation : path.readOperations()) {
@@ -430,22 +416,6 @@ protected void normalizePaths() {
430416
normalizeParameters(path.getParameters());
431417

432418
for (Operation operation : operations) {
433-
if (operationIdFilters.size() > 0) {
434-
if (operationIdFilters.contains(operation.getOperationId())) {
435-
operation.addExtension(X_INTERNAL, false);
436-
} else {
437-
LOGGER.info("operation `{}` marked as internal only (x-internal: true) by the operationId FILTER", operation.getOperationId());
438-
operation.addExtension(X_INTERNAL, true);
439-
}
440-
} else if (!tagFilters.isEmpty()) {
441-
if (operation.getTags().stream().anyMatch(tagFilters::contains)) {
442-
operation.addExtension(X_INTERNAL, false);
443-
} else {
444-
LOGGER.info("operation `{}` marked as internal only (x-internal: true) by the tag FILTER", operation.getOperationId());
445-
operation.addExtension(X_INTERNAL, true);
446-
}
447-
}
448-
449419
normalizeOperation(operation);
450420
normalizeRequestBody(operation);
451421
normalizeParameters(operation.getParameters());
@@ -1363,7 +1333,7 @@ protected Schema processSimplifyOneOfEnum(Schema schema) {
13631333
*
13641334
* @param schema Schema to modify
13651335
* @param subSchemas List of sub-schemas to check
1366-
* @param schemaType Type of composed schema ("oneOf" or "anyOf")
1336+
* @param composedType Type of composed schema ("oneOf" or "anyOf")
13671337
* @return Simplified schema
13681338
*/
13691339
protected Schema simplifyComposedSchemaWithEnums(Schema schema, List<Object> subSchemas, String composedType) {
@@ -1832,4 +1802,164 @@ protected Schema processNormalize31Spec(Schema schema, Set<Schema> visitedSchema
18321802
}
18331803

18341804
// ===================== end of rules =====================
1805+
1806+
protected static class Filter {
1807+
public static final String OPERATION_ID = "operationId";
1808+
public static final String METHOD = "method";
1809+
public static final String TAG = "tag";
1810+
public static final String PATH = "path";
1811+
private final String filters;
1812+
protected Set<String> operationIdFilters = Collections.emptySet();
1813+
protected Set<String> methodFilters = Collections.emptySet();
1814+
protected Set<String> tagFilters = Collections.emptySet();
1815+
protected Set<String> pathStartingWithFilters = Collections.emptySet();
1816+
private boolean hasFilter;
1817+
1818+
protected Filter(String filters) {
1819+
this.filters = filters.trim();
1820+
}
1821+
1822+
/**
1823+
* Perform the parsing of the filter string.
1824+
*
1825+
* @return true if filters need to be processed
1826+
*/
1827+
public boolean parse() {
1828+
if (StringUtils.isEmpty(filters)) {
1829+
return false;
1830+
}
1831+
try {
1832+
doParse();
1833+
return hasFilter();
1834+
} catch (RuntimeException e) {
1835+
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",
1836+
filters, Filter.OPERATION_ID, Filter.METHOD, Filter.TAG, Filter.PATH, e.getMessage());
1837+
// throw an exception. This is a breaking change compared to pre 7.16.0
1838+
// Workaround: fix the syntax!
1839+
throw new IllegalArgumentException(message);
1840+
}
1841+
}
1842+
1843+
private void doParse() {
1844+
for (String filter : filters.split(";")) {
1845+
filter = filter.trim();
1846+
String[] filterStrs = filter.split(":");
1847+
if (filterStrs.length != 2) { // only support filter with : at the moment
1848+
throw new IllegalArgumentException("filter with no value not supported :[" + filter + "]");
1849+
} else {
1850+
String filterKey = filterStrs[0].trim();
1851+
String filterValue = filterStrs[1];
1852+
Set<String> parsedFilters = splitByPipe(filterValue);
1853+
hasFilter = true;
1854+
if (OPERATION_ID.equals(filterKey)) {
1855+
operationIdFilters = parsedFilters;
1856+
} else if (METHOD.equals(filterKey)) {
1857+
methodFilters = parsedFilters;
1858+
} else if (TAG.equals(filterKey)) {
1859+
tagFilters = parsedFilters;
1860+
} else if (PATH.equals(filterKey)) {
1861+
pathStartingWithFilters = parsedFilters;
1862+
} else {
1863+
parse(filterKey, filterValue);
1864+
}
1865+
}
1866+
}
1867+
}
1868+
1869+
/**
1870+
* Split the filterValue by pipe.
1871+
*
1872+
* @return the split values.
1873+
*/
1874+
protected Set<String> splitByPipe(String filterValue) {
1875+
return Arrays.stream(filterValue.split("[|]"))
1876+
.filter(Objects::nonNull)
1877+
.map(String::trim)
1878+
.collect(Collectors.toCollection(HashSet::new));
1879+
}
1880+
1881+
/**
1882+
* Parse non default filters.
1883+
*
1884+
* Override this method to add custom parsing logic.
1885+
*
1886+
* By default throws IllegalArgumentException.
1887+
*
1888+
* @param filterName name of the filter
1889+
* @param filterValue value of the filter
1890+
*/
1891+
protected void parse(String filterName, String filterValue) {
1892+
parseFails(filterName, filterValue);
1893+
}
1894+
1895+
protected void parseFails(String filterName, String filterValue) {
1896+
throw new IllegalArgumentException("filter not supported :[" + filterName + ":" + filterValue + "]");
1897+
}
1898+
1899+
/**
1900+
* Test if the OpenAPI contract match an extra filter.
1901+
*
1902+
* Override this method to add custom logic.
1903+
*
1904+
* @param operation Openapi Operation
1905+
* @param path Path of the operation
1906+
*
1907+
* @return true if the operation of path match the filter
1908+
*/
1909+
protected boolean hasCustomFilterMatch(String path, Operation operation) {
1910+
return false;
1911+
}
1912+
1913+
public boolean hasFilter() {
1914+
return hasFilter;
1915+
}
1916+
1917+
public void apply(String path, PathItem pathItem, Map<String, Function<PathItem, Operation>> methodMap) {
1918+
methodMap.forEach((method, getter) -> {
1919+
Operation operation = getter.apply(pathItem);
1920+
if (operation != null) {
1921+
boolean found = false;
1922+
found |= logIfMatch(PATH, operation, hasPathStarting(path));
1923+
found |= logIfMatch(TAG, operation, hasTag(operation));
1924+
found |= logIfMatch(OPERATION_ID, operation, hasOperationId(operation));
1925+
found |= logIfMatch(METHOD, operation, hasMethod(method));
1926+
found |= hasCustomFilterMatch(path, operation);
1927+
1928+
operation.addExtension(X_INTERNAL, !found);
1929+
}
1930+
});
1931+
}
1932+
1933+
protected boolean logIfMatch(String filterName, Operation operation, boolean filterMatched) {
1934+
if (filterMatched) {
1935+
logMatch(filterName, operation);
1936+
}
1937+
return filterMatched;
1938+
}
1939+
1940+
protected void logMatch(String filterName, Operation operation) {
1941+
getLogger().info("operation `{}` marked as internal only (x-internal: true) by the {} FILTER", operation.getOperationId(), filterName);
1942+
}
1943+
1944+
protected Logger getLogger() {
1945+
return OpenAPINormalizer.LOGGER;
1946+
}
1947+
1948+
private boolean hasPathStarting(String path) {
1949+
return pathStartingWithFilters.stream().anyMatch(filter -> path.startsWith(filter));
1950+
}
1951+
1952+
private boolean hasTag( Operation operation) {
1953+
return operation.getTags() != null && operation.getTags().stream().anyMatch(tagFilters::contains);
1954+
}
1955+
1956+
private boolean hasOperationId(Operation operation) {
1957+
return operationIdFilters.contains(operation.getOperationId());
1958+
}
1959+
1960+
private boolean hasMethod(String method) {
1961+
return methodFilters.contains(method);
1962+
}
1963+
1964+
}
18351965
}

0 commit comments

Comments
 (0)