-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
Improve OneOf handling with new normalizer REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING #23543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jpfinne
wants to merge
23
commits into
OpenAPITools:master
Choose a base branch
from
jpfinne:feature/normalizer_REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,145
−38
Open
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
bf1dc5f
normalizer REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING
jpfinne 18ac063
Work in progress
jpfinne bbc0fd0
Improvements
jpfinne 9d9cb63
Merge remote-tracking branch 'origin/master' into feature/normalizer_…
jpfinne af8f6c2
Merge master
jpfinne c963666
Fix invalid path
jpfinne 848e606
Improve assertions
jpfinne 73e1f6e
Fix invalid discriminator value
jpfinne 716688e
filename case
jpfinne cef6534
filename case
jpfinne bcf2047
Rollback composed-oneof.yaml
jpfinne 8319043
Improve normalization
jpfinne 53c6f01
Fix building of allOf
jpfinne 18d2f81
Fix hasParent
jpfinne 4f26f25
Fix some cubic findings
jpfinne d3e23bb
Fix some cubic findings
jpfinne 725a0ae
Fix infinite recursion stopping too early
jpfinne c1da8d4
Force build
jpfinne befa7fa
Use getReferencedSchema in search for properties
jpfinne 5d2fab6
Improve hasParent -> isParentReferencedInChild
jpfinne a6e30d4
Cubic suggestions
jpfinne 59b1c26
Add assertions for JsonSubTypes.Types
jpfinne 0facdf7
Clean moved child
jpfinne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,10 @@ public class OpenAPINormalizer { | |
| // are removed as most generators cannot handle such case at the moment | ||
| final String REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY = "REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY"; | ||
|
|
||
| // when set to true, oneOf is removed and is converted into mappings in a discriminator mapping | ||
| final String REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING = "REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING"; | ||
|
|
||
|
|
||
| // when set to true, oneOf/anyOf with either string or enum string as sub schemas will be simplified | ||
| // to just string | ||
| final String SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING = "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING"; | ||
|
|
@@ -214,6 +218,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) { | |
| ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM); | ||
| ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT); | ||
| ruleNames.add(SORT_MODEL_PROPERTIES); | ||
| ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING); | ||
|
|
||
| // rules that are default to true | ||
| rules.put(SIMPLIFY_ONEOF_ANYOF, true); | ||
|
|
@@ -639,6 +644,10 @@ protected void normalizeComponentsSchemas() { | |
|
|
||
| // normalize the schemas | ||
| schemas.put(schemaName, normalizeSchema(schema, new HashSet<>())); | ||
|
|
||
| if (getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) { | ||
| ensureInheritanceForDiscriminatorMappings(schema, schemaName); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents |
||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -1053,6 +1062,8 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) { | |
| // simplify first as the schema may no longer be a oneOf after processing the rule below | ||
| schema = processSimplifyOneOf(schema); | ||
|
|
||
| schema = processReplaceOneOfByMapping(schema); | ||
|
|
||
| // if it's still a oneOf, loop through the sub-schemas | ||
| if (schema.getOneOf() != null) { | ||
| for (int i = 0; i < schema.getOneOf().size(); i++) { | ||
|
|
@@ -1569,6 +1580,214 @@ protected Schema processSimplifyOneOf(Schema schema) { | |
| return schema; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Ensure inheritance is correctly defined for OneOf and Discriminators. | ||
| * | ||
| * For schemas containing oneOf and discriminator.propertyName: | ||
| * <ul> | ||
| * <li>Create the mappings as $refs</li> | ||
| * <li>Remove OneOf</li> | ||
| * </ul> | ||
| */ | ||
| protected Schema processReplaceOneOfByMapping(Schema schema) { | ||
| if (!getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) { | ||
| return schema; | ||
| } | ||
| Discriminator discriminator = schema.getDiscriminator(); | ||
| if (discriminator != null) { | ||
| boolean inlineSchema = isInlineSchema(schema); | ||
| if (inlineSchema) { | ||
| // the For referenced schemas, ensure that there is an allOf with this schema. | ||
| LOGGER.warn("Inline oneOf schema not supported by REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization"); | ||
| return schema; | ||
| } | ||
| if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) { | ||
| Map<String, String> mappings = new TreeMap<>(); | ||
| discriminator.setMapping(mappings); | ||
| List<Schema> oneOfs = schema.getOneOf(); | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| for (Schema oneOf: oneOfs) { | ||
| String refSchema = oneOf.get$ref(); | ||
| if (refSchema != null) { | ||
| boolean hasProperty = findProperty(schema, discriminator.getPropertyName(), false, new HashSet<>()) != null; | ||
| String name = getDiscriminatorValue(refSchema, discriminator.getPropertyName(), hasProperty); | ||
| mappings.put(name, refSchema); | ||
| } | ||
| } | ||
| } | ||
| // remove oneOf and only keep the discriminator mapping | ||
| schema.setOneOf(null); | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| return schema; | ||
| } | ||
|
|
||
| private boolean isInlineSchema(Schema schema) { | ||
| if (openAPI.getComponents()!=null && openAPI.getComponents().getSchemas()!=null) { | ||
| int identity = System.identityHashCode(schema); | ||
| for (Schema componentSchema: openAPI.getComponents().getSchemas().values()) { | ||
| if (System.identityHashCode(componentSchema) == identity) { | ||
| return false; | ||
| } | ||
| } | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| } | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Best effort to retrieve a good discriminator value. | ||
| * By order of precedence: | ||
| * <ul> | ||
| * <li>x-discriminator-value</li> | ||
| * <li>single enum value for attribute used by the discriminator.propertyName</li> | ||
| * <li>hame of the schema</li> | ||
| * </ul> | ||
| * | ||
| * @param refSchema $ref value like #/components/schemas/Dog | ||
| * @param discriminatorPropertyName name of the property used in the discriminator mapping | ||
| * @param propertyAlreadyPresent if true, delete the property in the referenced schemas to avoid duplicates | ||
| * | ||
| * @return the name | ||
| */ | ||
| protected String getDiscriminatorValue(String refSchema, String discriminatorPropertyName, boolean propertyAlreadyPresent) { | ||
| String schemaName = ModelUtils.getSimpleRef(refSchema); | ||
| Schema schema = ModelUtils.getSchema(openAPI, schemaName); | ||
| Schema property = findProperty(schema, discriminatorPropertyName, propertyAlreadyPresent, new HashSet<>()); | ||
| if (schema != null && schema.getExtensions() != null) { | ||
| Object discriminatorValue = schema.getExtensions().get("x-discriminator-value"); | ||
| if (discriminatorValue != null) { | ||
| return discriminatorValue.toString(); | ||
| } | ||
| } | ||
|
|
||
| // find the discriminator value as a unique enum value | ||
| if (property != null) { | ||
| List enums = property.getEnum(); | ||
| if (enums != null && enums.size() == 1) { | ||
| return enums.get(0).toString(); | ||
| } | ||
| } | ||
|
|
||
| return schemaName; | ||
| } | ||
|
|
||
| /** | ||
| * find a property under the schema. | ||
| * | ||
| * @param schema | ||
| * @param propertyName property to find | ||
| * @param toDelete if true delete the found property | ||
| * @param visitedSchemas avoid infinite recursion | ||
| * @return found property or null if not found. | ||
| */ | ||
| private Schema findProperty(Schema schema, String propertyName, boolean toDelete, HashSet<Object> visitedSchemas) { | ||
| if (propertyName == null || schema == null || visitedSchemas.contains(schema)) { | ||
| return null; | ||
| } | ||
| visitedSchemas.add(schema); | ||
| Map<String, Schema> properties = schema.getProperties(); | ||
| if (properties != null) { | ||
| Schema property = properties.get(propertyName); | ||
| if (property != null) { | ||
| if (toDelete) { | ||
| if (schema.getProperties().remove(propertyName) != null && schema.getProperties().isEmpty()) { | ||
| schema.setProperties(null); | ||
| } | ||
| } | ||
| return property; | ||
| } | ||
| } | ||
| List<Schema> allOfs = schema.getAllOf(); | ||
| if (allOfs != null) { | ||
| for (Schema child : allOfs) { | ||
| Schema found = findProperty(child, propertyName, toDelete, visitedSchemas); | ||
| if (found != null) { | ||
| return found; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * ensure that all schemas referenced in the discriminator mapping has an allOf to the parent schema. | ||
| * | ||
| * This allows DefaultCodeGen to detect inheritance. | ||
| * | ||
| * @param parent parent schma | ||
| * @param parentName name of the parent schema | ||
| */ | ||
| protected void ensureInheritanceForDiscriminatorMappings(Schema parent, String parentName) { | ||
| Discriminator discriminator = parent.getDiscriminator(); | ||
| if (discriminator != null && discriminator.getMapping() != null) { | ||
| for (String mapping : discriminator.getMapping().values()) { | ||
| String refSchemaName = ModelUtils.getSimpleRef(mapping); | ||
| Schema child = ModelUtils.getSchema(openAPI, refSchemaName); | ||
| if (child != null) { | ||
| if (parentName != null) { | ||
| ensureInheritanceForDiscriminatorMappings(parent, child, parentName, new HashSet<>()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * If not already present, add in the child an allOf referencing the parent. | ||
| */ | ||
| protected void ensureInheritanceForDiscriminatorMappings(Schema parent, Schema child, String parentName, Set<Schema> visitedSchemas) { | ||
| String reference = "#/components/schemas/" + parentName; | ||
| List<Schema> allOf = child.getAllOf(); | ||
| if (allOf != null) { | ||
| if (hasParent(parent, child, reference, visitedSchemas)) { | ||
| // already done, so no need to add | ||
| return; | ||
| } | ||
| Schema refToParent = new Schema<>().$ref(reference); | ||
| allOf.add(refToParent); | ||
| } else { | ||
| allOf = new ArrayList<>(); | ||
| child.setAllOf(allOf); | ||
| Schema refToParent = new Schema<>().$ref(reference); | ||
| allOf.add(refToParent); | ||
| Map<String, Schema> childProperties = child.getProperties(); | ||
| if (childProperties != null) { | ||
| // move the properties inside the new allOf. | ||
| Schema newChildProperties = new Schema<>().properties(childProperties); | ||
| allOf.add(newChildProperties); | ||
| child.setProperties(null); | ||
| child.setType(null); | ||
| } | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
|
||
| /** | ||
| * return true if the child as an allOf referencing the parent scham. | ||
| */ | ||
| private boolean hasParent(Schema parent, Schema child, String reference, Set<Schema> visitedSchemas) { | ||
| if (child == null) { | ||
| return false; | ||
| } | ||
| if (child.get$ref() != null && child.get$ref().equals(reference)) { | ||
| return true; | ||
| } | ||
| List<Schema> allOf = child.getAllOf(); | ||
| if (allOf != null) { | ||
| for (Schema schema : allOf) { | ||
| if (visitedSchemas.contains(schema)) { | ||
| return false; | ||
| } | ||
| visitedSchemas.add(schema); | ||
| if (hasParent(parent, schema, reference, visitedSchemas)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Set nullable to true in array/set if needed. | ||
| * | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.