Skip to content
Closed
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
9 changes: 9 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,15 @@ Example:
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/simplifyAnyOfStringAndEnumString_test.yaml -o /tmp/java-okhttp/ --openapi-normalizer SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING=true
```

- `SIMPLIFY_ONEOF_ANYOF_ENUM`: when set to true, oneOf/anyOf with only enum sub-schemas all containing enum values will be converted to a single enum
This is enabled by default

Example:

```
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/simplifyOneOfWithEnums_test.yaml -o /tmp/java-okhttp/ --openapi-normalizer SIMPLIFY_ONEOF_ANYOF_ENUM=true
```

- `SIMPLIFY_BOOLEAN_ENUM`: when set to `true`, convert boolean enum to just enum.

Example:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ public class OpenAPINormalizer {
// when set to true, boolean enum will be converted to just boolean
final String SIMPLIFY_BOOLEAN_ENUM = "SIMPLIFY_BOOLEAN_ENUM";

// when set to true, oneOf/anyOf with enum sub-schemas containing single values will be converted to a single enum
final String SIMPLIFY_ONEOF_ANYOF_ENUM = "SIMPLIFY_ONEOF_ANYOF_ENUM";

// when set to a string value, tags in all operations will be reset to the string value provided
final String SET_TAGS_FOR_ALL_OPERATIONS = "SET_TAGS_FOR_ALL_OPERATIONS";
String setTagsForAllOperations;
Expand Down Expand Up @@ -206,11 +209,12 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
ruleNames.add(FILTER);
ruleNames.add(SET_CONTAINER_TO_NULLABLE);
ruleNames.add(SET_PRIMITIVE_TYPES_TO_NULLABLE);

ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM);

// rules that are default to true
rules.put(SIMPLIFY_ONEOF_ANYOF, true);
rules.put(SIMPLIFY_BOOLEAN_ENUM, true);
rules.put(SIMPLIFY_ONEOF_ANYOF_ENUM, true);

processRules(inputRules);

Expand Down Expand Up @@ -973,6 +977,8 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
// Remove duplicate oneOf entries
ModelUtils.deduplicateOneOfSchema(schema);

schema = processSimplifyOneOfEnum(schema);

// simplify first as the schema may no longer be a oneOf after processing the rule below
schema = processSimplifyOneOf(schema);

Expand Down Expand Up @@ -1001,6 +1007,11 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
}

protected Schema normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) {
//transform anyOf into enums if needed
schema = processSimplifyAnyOfEnum(schema);
if (schema.getAnyOf() == null) {
return schema;
}
for (int i = 0; i < schema.getAnyOf().size(); i++) {
// normalize anyOf sub schemas one by one
Object item = schema.getAnyOf().get(i);
Expand Down Expand Up @@ -1276,6 +1287,161 @@ protected Schema processSimplifyAnyOfStringAndEnumString(Schema schema) {
}


/**
* If the schema is anyOf and all sub-schemas are enums (with one or more values),
* then simplify it to a single enum schema containing all the values.
*
* @param schema Schema
* @return Schema
*/
protected Schema processSimplifyAnyOfEnum(Schema schema) {
if (!getRule(SIMPLIFY_ONEOF_ANYOF_ENUM)) {
return schema;
}

if (schema.getAnyOf() == null || schema.getAnyOf().isEmpty()) {
return schema;
}
if(schema.getOneOf() != null && !schema.getOneOf().isEmpty() ||
schema.getAllOf() != null && !schema.getAllOf().isEmpty() ||
schema.getNot() != null) {
//only convert to enum if anyOf is the only composition
return schema;
}

return simplifyComposedSchemaWithEnums(schema, schema.getAnyOf(), "anyOf");
}

/**
* If the schema is oneOf and all sub-schemas are enums (with one or more values),
* then simplify it to a single enum schema containing all the values.
*
* @param schema Schema
* @return Schema
*/
protected Schema processSimplifyOneOfEnum(Schema schema) {
if (!getRule(SIMPLIFY_ONEOF_ANYOF_ENUM)) {
return schema;
}

if (schema.getOneOf() == null || schema.getOneOf().isEmpty()) {
return schema;
}
if(schema.getAnyOf() != null && !schema.getAnyOf().isEmpty() ||
schema.getAllOf() != null && !schema.getAllOf().isEmpty() ||
schema.getNot() != null) {
//only convert to enum if oneOf is the only composition
return schema;
}

return simplifyComposedSchemaWithEnums(schema, schema.getOneOf(), "oneOf");
}

/**
* Simplifies a composed schema (oneOf/anyOf) where all sub-schemas are enums
* to a single enum schema containing all the values.
*
* @param schema Schema to modify
* @param subSchemas List of sub-schemas to check
* @param schemaType Type of composed schema ("oneOf" or "anyOf")
* @return Simplified schema
*/
protected Schema simplifyComposedSchemaWithEnums(Schema schema, List<Object> subSchemas, String composedType) {
Map<Object, String> enumValues = new LinkedHashMap<>();

if(schema.getTypes() != null && schema.getTypes().size() > 1) {
// we cannot handle enums with multiple types
return schema;
}

if(subSchemas.size() < 2) {
//do not process if there's less than 2 sub-schemas. It will be normalized later, and this prevents
//named enum schemas from being converted to inline enum schemas
return schema;
}
String schemaType = ModelUtils.getType(schema);

for (Object item : subSchemas) {
if (!(item instanceof Schema)) {
return schema;
}

Schema subSchema = ModelUtils.getReferencedSchema(openAPI, (Schema) item);

// Check if this sub-schema has an enum (with one or more values)
if (subSchema.getEnum() == null || subSchema.getEnum().isEmpty()) {
return schema;
}

// Ensure all sub-schemas have the same type (if type is specified)
if(subSchema.getTypes() != null && subSchema.getTypes().size() > 1) {
// we cannot handle enums with multiple types
return schema;
}
String subSchemaType = ModelUtils.getType(subSchema);
if (subSchemaType != null) {
if (schemaType == null) {
schemaType = subSchemaType;
} else if (!schemaType.equals(subSchema.getType())) {
return schema;
}
}
// Add all enum values from this sub-schema to our collection
if(subSchema.getEnum().size() == 1) {
String description = subSchema.getTitle() == null ? "" : subSchema.getTitle();
if(subSchema.getDescription() != null) {
if(!description.isEmpty()) {
description += " - ";
}
description += subSchema.getDescription();
}
enumValues.put(subSchema.getEnum().get(0), description);
} else {
for(Object e: subSchema.getEnum()) {
enumValues.put(e, "");
}
}

}

return createSimplifiedEnumSchema(schema, enumValues, schemaType, composedType);
}


/**
* Creates a simplified enum schema from collected enum values.
*
* @param originalSchema Original schema to modify
* @param enumValues Collected enum values
* @param schemaType Consistent type across sub-schemas
* @param composedType Type of composed schema being simplified
* @return Simplified enum schema
*/
protected Schema createSimplifiedEnumSchema(Schema originalSchema, Map<Object, String> enumValues, String schemaType, String composedType) {
// Clear the composed schema type
if ("oneOf".equals(composedType)) {
originalSchema.setOneOf(null);
} else if ("anyOf".equals(composedType)) {
originalSchema.setAnyOf(null);
}

if (ModelUtils.getType(originalSchema) == null && schemaType != null) {
//if type was specified in subschemas, keep it in the main schema
ModelUtils.setType(originalSchema, schemaType);
}

originalSchema.setEnum(new ArrayList<>(enumValues.keySet()));
if(enumValues.values().stream().anyMatch(e -> !e.isEmpty())) {
//set x-enum-descriptions only if there's at least one non-empty description
originalSchema.addExtension("x-enum-descriptions", new ArrayList<>(enumValues.values()));
}

LOGGER.debug("Simplified {} with enum sub-schemas to single enum: {}", composedType, originalSchema);

return originalSchema;
}


/**
* If the schema is oneOf and the sub-schemas is null, set `nullable: true`
* instead.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2164,6 +2164,22 @@ public static String getType(Schema schema) {
}
}

/**
* Set schema type.
* For 3.1 spec, set as types, for 3.0, type
*
* @param schema the schema
* @return schema type
*/
public static void setType(Schema schema, String type) {
if (schema instanceof JsonSchema) {
schema.setTypes(null);
schema.addType(type);
} else {
schema.setType(type);
}
}

/**
* Returns true if any of the common attributes of the schema (e.g. readOnly, default, maximum, etc) is defined.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.openapitools.codegen.utils.ModelUtils;
Expand Down Expand Up @@ -132,6 +133,7 @@ public void testOpenAPINormalizerRemoveAnyOfOneOfAndKeepPropertiesOnly() {
assertNull(schema.getAnyOf());
}


@Test
public void testOpenAPINormalizerSimplifyOneOfAnyOfStringAndEnumString() {
// to test the rule SIMPLIFY_ONEOF_ANYOF_STRING_AND_ENUM_STRING
Expand All @@ -151,6 +153,72 @@ public void testOpenAPINormalizerSimplifyOneOfAnyOfStringAndEnumString() {
assertTrue(schema3.getEnum().size() > 0);
}

@Test
public void testSimplifyOneOfAnyOfEnum() throws Exception {
// Load OpenAPI spec from external YAML file
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/simplifyoneOfWithEnums_test.yaml");

// Test with rule enabled (default)
Map<String, String> options = new HashMap<>();
options.put("SIMPLIFY_ONEOF_ANYOF_ENUM", "true");
OpenAPINormalizer normalizer = new OpenAPINormalizer(openAPI, options);
normalizer.normalize();

// Verify component schema was simplified
Schema colorSchema = openAPI.getComponents().getSchemas().get("ColorEnum");
assertNull(colorSchema.getOneOf());
assertEquals(colorSchema.getType(), "string");
assertEquals(colorSchema.getEnum(), Arrays.asList("red", "green", "blue", "yellow", "purple"));

Schema statusSchema = openAPI.getComponents().getSchemas().get("StatusEnum");
assertNull(statusSchema.getOneOf());
assertEquals(statusSchema.getType(), "number");
assertEquals(statusSchema.getEnum(), Arrays.asList(1, 2, 3));

// Verify parameter schema was simplified
Parameter param = openAPI.getPaths().get("/test").getGet().getParameters().get(0);
assertNull(param.getSchema().getOneOf());
assertEquals(param.getSchema().getType(), "string");
assertEquals(param.getSchema().getEnum(), Arrays.asList("option1", "option2"));

// Verify parameter schema was simplified
Parameter anyOfParam = openAPI.getPaths().get("/test").getGet().getParameters().get(1);
assertNull(anyOfParam.getSchema().getAnyOf());
assertEquals(anyOfParam.getSchema().getType(), "string");
assertEquals(anyOfParam.getSchema().getEnum(), Arrays.asList("anyof 1", "anyof 2"));
assertEquals(anyOfParam.getSchema().getExtensions().get("x-enum-descriptions"), Arrays.asList("title 1", "title 2"));

Schema combinedRefsEnum = openAPI.getComponents().getSchemas().get("combinedRefsEnum");

assertEquals(anyOfParam.getSchema().getType(), "string");
assertNull(combinedRefsEnum.get$ref());
assertEquals(combinedRefsEnum.getEnum(), Arrays.asList("A", "B", "C", "D"));
assertNull(combinedRefsEnum.getOneOf());

// Test with rule disabled
OpenAPI openAPI2 = TestUtils.parseSpec("src/test/resources/3_0/simplifyoneOfWithEnums_test.yaml");
Map<String, String> options2 = new HashMap<>();
options2.put("SIMPLIFY_ONEOF_ANYOF_ENUM", "false");
OpenAPINormalizer normalizer2 = new OpenAPINormalizer(openAPI2, options2);
normalizer2.normalize();

// oneOf will be removed, as they are in this normalizer if a primitive type has a oneOf
Schema colorSchema2 = openAPI2.getComponents().getSchemas().get("ColorEnum");
assertNull(colorSchema2.getOneOf());
assertNull(colorSchema2.getEnum());

//If you put string on every subscheme of oneOf, it does not remove it. This might need a fix at some other time
Parameter param2 = openAPI2.getPaths().get("/test").getGet().getParameters().get(0);
assertNotNull(param2.getSchema().getOneOf());
assertNull(param2.getSchema().getEnum());

//but here it does
Parameter anyOfParam2 = openAPI2.getPaths().get("/test").getGet().getParameters().get(1);
assertNull(anyOfParam2.getSchema().getOneOf());
assertNull(anyOfParam2.getSchema().getEnum());

}

@Test
public void testOpenAPINormalizerSimplifyOneOfAnyOf() {
// to test the rule SIMPLIFY_ONEOF_ANYOF
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
openapi: 3.0.0
info:
title: Test API
version: 1.0.0
components:
schemas:
ColorEnum:
type: string
oneOf:
- title: PrimaryColors
enum: ["red", "green"]
- title: SecondaryColors
enum: ["blue", "yellow"]
- title: purple
enum: ["purple"]
StatusEnum:
type: number
oneOf:
- title: active
enum: [1]
- title: inactive_pending
enum: [2, 3]
enum1:
type: string
enum:
- A
- B
enum2:
type: string
enum:
- C
- D
combinedRefsEnum:
oneOf:
- $ref: '#/components/schemas/enum1'
- $ref: '#/components/schemas/enum2'
paths:
/test:
get:
parameters:
- name: color
in: query
schema:
oneOf:
- type: string
enum: ["option1"]
- type: string
enum: ["option2"]
- name: anyOfEnum
in: query
schema:
type: string
anyOf:
- title: title 1
enum: [ "anyof 1" ]
- title: title 2
enum: [ "anyof 2" ]
responses:
'200':
description: Success
Loading
Loading