Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 semicolon.

### Available Filters

Expand All @@ -658,14 +658,17 @@ 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
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate \
-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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public class OpenAPINormalizer {

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

protected final Logger LOGGER = LoggerFactory.getLogger(OpenAPINormalizer.class);
protected static final Logger LOGGER = LoggerFactory.getLogger(OpenAPINormalizer.class);

Set<String> ruleNames = new TreeSet<>();
Set<String> rulesDefaultToTrue = new TreeSet<>();
Expand Down Expand Up @@ -133,10 +133,6 @@ public class OpenAPINormalizer {

// when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else
final String FILTER = "FILTER";
HashSet<String> operationIdFilters = new HashSet<>();
HashSet<String> methodFilters = new HashSet<>();

HashSet<String> tagFilters = new HashSet<>();

// when set (e.g. operationId:getPetById|addPet), filter out (or remove) everything else
final String SET_CONTAINER_TO_NULLABLE = "SET_CONTAINER_TO_NULLABLE";
Expand Down Expand Up @@ -275,30 +271,7 @@ public void processRules(Map<String, String> 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));
}
}
// actual parsing is delayed to allow customization of the Filter processing
}

if (inputRules.get(SET_CONTAINER_TO_NULLABLE) != null) {
Expand Down Expand Up @@ -344,6 +317,19 @@ public void processRules(Map<String, String> 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.
Expand Down Expand Up @@ -405,15 +391,15 @@ 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 (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);
}
});
}

// Include callback operation as well
for (Operation operation : path.readOperations()) {
Expand All @@ -430,22 +416,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());
Expand Down Expand Up @@ -1363,7 +1333,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<Object> subSchemas, String composedType) {
Expand Down Expand Up @@ -1832,4 +1802,164 @@ protected Schema processNormalize31Spec(Schema schema, Set<Schema> visitedSchema
}

// ===================== end of rules =====================

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<String> operationIdFilters = Collections.emptySet();
protected Set<String> methodFilters = Collections.emptySet();
protected Set<String> tagFilters = Collections.emptySet();
protected Set<String> pathStartingWithFilters = Collections.emptySet();
private boolean hasFilter;

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);
}
}

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 with no value not supported :[" + filter + "]");
} else {
String filterKey = filterStrs[0].trim();
String filterValue = filterStrs[1];
Set<String> 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;
} else if (PATH.equals(filterKey)) {
pathStartingWithFilters = parsedFilters;
} else {
parse(filterKey, filterValue);
}
}
}
}

/**
* Split the filterValue by pipe.
*
* @return the split values.
*/
protected Set<String> 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 hasFilter;
}

public void apply(String path, PathItem pathItem, Map<String, Function<PathItem, Operation>> 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);

operation.addExtension(X_INTERNAL, !found);
}
});
}

protected boolean logIfMatch(String filterName, Operation operation, boolean filterMatched) {
if (filterMatched) {
logMatch(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 Logger getLogger() {
return OpenAPINormalizer.LOGGER;
}

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);
}

}
}
Loading
Loading