-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
[TypeScript] Implement oneOf type resolution without discriminator #22201
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
base: master
Are you sure you want to change the base?
Changes from all commits
0e6998f
451f488
74351c7
fe10060
234643d
e03add7
49e768a
97a775d
eee422e
81acbbc
8e7a128
de4e09e
7da0c2a
ca525a9
0bd35fc
a117fcc
c590359
ded868c
c806d60
1a4b46d
8fd54d2
6be8f0c
69954aa
23a2dd7
3bfe9cc
14d1edc
d4984c0
b04a5ed
f8c65fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -50,6 +50,7 @@ | |||
| import java.util.*; | ||||
| import java.util.regex.Matcher; | ||||
| import java.util.regex.Pattern; | ||||
| import java.util.stream.Collectors; | ||||
|
|
||||
| import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER; | ||||
| import static org.openapitools.codegen.utils.OnceLogger.once; | ||||
|
|
@@ -183,6 +184,9 @@ public TypeScriptClientCodegen() { | |||
| // models | ||||
| setModelPackage("models"); | ||||
| supportingFiles.add(new SupportingFile("model" + File.separator + "ObjectSerializer.mustache", "models", "ObjectSerializer.ts")); | ||||
| supportingFiles.add(new SupportingFile("model" + File.separator + "TypeMatcher.mustache", "models", "TypeMatcher.ts")); | ||||
| supportingFiles.add(new SupportingFile("model" + File.separator + "ModelTypes.mustache", "models", "ModelTypes.ts")); | ||||
|
|
||||
| modelTemplateFiles.put("model" + File.separator + "model.mustache", ".ts"); | ||||
|
|
||||
| // api | ||||
|
|
@@ -369,12 +373,22 @@ private String getNameWithEnumPropertyNaming(String name) { | |||
| } | ||||
| } | ||||
|
|
||||
| @Override | ||||
| public CodegenModel fromModel(String name, Schema schema) { | ||||
| CodegenModel codegenModel = super.fromModel(name, schema); | ||||
| return new ExtendedCodegenModel(codegenModel); | ||||
| } | ||||
|
|
||||
| @Override | ||||
| public ModelsMap postProcessModels(ModelsMap objs) { | ||||
| // process enum in models | ||||
| List<ModelMap> models = postProcessModelsEnum(objs).getModels(); | ||||
| for (ModelMap mo : models) { | ||||
| CodegenModel cm = mo.getModel(); | ||||
| CodegenModel model = mo.getModel(); | ||||
| // Convert to ExtendedCodegenModel if it's not already one (e.g., in tests) | ||||
| ExtendedCodegenModel cm = model instanceof ExtendedCodegenModel | ||||
| ? (ExtendedCodegenModel) model | ||||
| : new ExtendedCodegenModel(model); | ||||
| cm.imports = new TreeSet<>(cm.imports); | ||||
| // name enum with model name, e.g. StatusEnum => Pet.StatusEnum | ||||
| for (CodegenProperty var : cm.vars) { | ||||
|
|
@@ -390,15 +404,37 @@ public ModelsMap postProcessModels(ModelsMap objs) { | |||
| } | ||||
| } | ||||
| } | ||||
|
|
||||
| List<CodegenProperty> oneOfsList = Optional.ofNullable(cm.getComposedSchemas()) | ||||
| .map(CodegenComposedSchemas::getOneOf) | ||||
| .orElse(Collections.emptyList()); | ||||
|
|
||||
| // create a set of any non-primitive, non-array types used in the oneOf schemas which will | ||||
| // need to be imported. | ||||
| cm.oneOfModels = oneOfsList.stream() | ||||
| .filter(cp -> !cp.getIsPrimitiveType() && !cp.getIsArray()) | ||||
| .map(CodegenProperty::getBaseType) | ||||
| .filter(Objects::nonNull) | ||||
| .collect(Collectors.toCollection(TreeSet::new)); | ||||
|
|
||||
| // create a set of any complex, inner types used by arrays in the oneOf schema (e.g. if | ||||
| // the oneOf uses Array<Foo>, Foo needs to be imported). | ||||
| cm.oneOfArrays = oneOfsList.stream() | ||||
| .filter(CodegenProperty::getIsArray) | ||||
| .map(CodegenProperty::getComplexType) | ||||
| .filter(Objects::nonNull) | ||||
| .collect(Collectors.toCollection(TreeSet::new)); | ||||
|
|
||||
| // create a set of primitive types used in the oneOf schemas for the deserialization process. | ||||
| cm.oneOfPrimitives = oneOfsList.stream() | ||||
| .filter(CodegenProperty::getIsPrimitiveType) | ||||
| .collect(Collectors.toCollection(HashSet::new)); | ||||
|
|
||||
| if (!cm.oneOf.isEmpty()) { | ||||
| // For oneOfs only import $refs within the oneOf | ||||
| TreeSet<String> oneOfRefs = new TreeSet<>(); | ||||
| for (String im : cm.imports) { | ||||
| if (cm.oneOf.contains(im)) { | ||||
| oneOfRefs.add(im); | ||||
| } | ||||
| } | ||||
| cm.imports = oneOfRefs; | ||||
| cm.imports = cm.imports.stream() | ||||
| .filter(im -> cm.oneOfModels.contains(im) || cm.oneOfArrays.contains(im)) | ||||
| .collect(Collectors.toCollection(TreeSet::new)); | ||||
| } | ||||
| } | ||||
| for (ModelMap mo : models) { | ||||
|
|
@@ -1135,4 +1171,111 @@ protected void addImport(Set<String> importsToBeAddedTo, String type) { | |||
| protected String[] splitComposedTypes(String type) { | ||||
| return type.replace(" ", "").split("[|&<>]"); | ||||
| } | ||||
|
|
||||
| public class ExtendedCodegenModel extends CodegenModel { | ||||
|
Member
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. could we somehow do without an extended CodegenModel class? everytime the base class is updated (e.g. new properties are added), this would need to be updated here as well
Contributor
Author
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. Alternatively, we could consider moving our additional properties to the base class itself. I've extended the Line 1533 in 03c13fb
|
||||
| // oneOfModels contains a list of non-primitive, non-array types referenced in oneOf schemas that need to be | ||||
| // imported. | ||||
| // oneOfArrays contains a list of complex inner types used in arrays within oneOf schemas (e.g. if | ||||
| // oneOf uses Array<Foo>, Foo needs to be imported). | ||||
| // oneOfPrimitives contains a list of primitive types used in oneOf schemas for the deserialization process. | ||||
| @Getter @Setter | ||||
| public Set<String> oneOfModels = new TreeSet<>(); | ||||
| @Getter @Setter | ||||
| public Set<String> oneOfArrays = new TreeSet<>(); | ||||
| @Getter @Setter | ||||
| public Set<CodegenProperty> oneOfPrimitives = new HashSet<>(); | ||||
|
|
||||
| public ExtendedCodegenModel(CodegenModel cm) { | ||||
| super(); | ||||
|
|
||||
| this.parent = cm.parent; | ||||
| this.parentSchema = cm.parentSchema; | ||||
| this.interfaces = cm.interfaces; | ||||
| this.allParents = cm.allParents; | ||||
| this.parentModel = cm.parentModel; | ||||
| this.interfaceModels = cm.interfaceModels; | ||||
| this.children = cm.children; | ||||
| this.anyOf = cm.anyOf; | ||||
| this.oneOf = cm.oneOf; | ||||
| this.allOf = cm.allOf; | ||||
| this.permits = cm.permits; | ||||
| this.name = cm.name; | ||||
| this.schemaName = cm.schemaName; | ||||
| this.classname = cm.classname; | ||||
| this.title = cm.title; | ||||
| this.description = cm.description; | ||||
| this.classVarName = cm.classVarName; | ||||
| this.modelJson = cm.modelJson; | ||||
| this.dataType = cm.dataType; | ||||
| this.xmlPrefix = cm.xmlPrefix; | ||||
| this.xmlNamespace = cm.xmlNamespace; | ||||
| this.xmlName = cm.xmlName; | ||||
| this.classFilename = cm.classFilename; | ||||
| this.unescapedDescription = cm.unescapedDescription; | ||||
| this.defaultValue = cm.defaultValue; | ||||
| this.arrayModelType = cm.arrayModelType; | ||||
| this.isAlias = cm.isAlias; | ||||
| this.isString = cm.isString; | ||||
| this.isInteger = cm.isInteger; | ||||
| this.isLong = cm.isLong; | ||||
| this.isNumber = cm.isNumber; | ||||
| this.isNumeric = cm.isNumeric; | ||||
| this.isFloat = cm.isFloat; | ||||
| this.isDouble = cm.isDouble; | ||||
| this.isDate = cm.isDate; | ||||
| this.isDateTime = cm.isDateTime; | ||||
| this.vars = cm.vars; | ||||
| this.allVars = cm.allVars; | ||||
| this.requiredVars = cm.requiredVars; | ||||
| this.optionalVars = cm.optionalVars; | ||||
| this.hasReadOnly = cm.hasReadOnly; | ||||
| this.readOnlyVars = cm.readOnlyVars; | ||||
| this.readWriteVars = cm.readWriteVars; | ||||
| this.parentVars = cm.parentVars; | ||||
| this.parentRequiredVars = cm.parentRequiredVars; | ||||
| this.nonNullableVars = cm.nonNullableVars; | ||||
| this.allowableValues = cm.allowableValues; | ||||
| this.mandatory = cm.mandatory; | ||||
| this.allMandatory = cm.allMandatory; | ||||
| this.imports = cm.imports; | ||||
| this.hasVars = cm.hasVars; | ||||
| this.emptyVars = cm.emptyVars; | ||||
| this.hasMoreModels = cm.hasMoreModels; | ||||
| this.hasEnums = cm.hasEnums; | ||||
| this.isEnum = cm.isEnum; | ||||
| this.hasValidation = cm.hasValidation; | ||||
| this.isNullable = cm.isNullable; | ||||
| this.hasRequired = cm.hasRequired; | ||||
| this.hasOptional = cm.hasOptional; | ||||
| this.isArray = cm.isArray; | ||||
| this.hasChildren = cm.hasChildren; | ||||
| this.isMap = cm.isMap; | ||||
| this.isDeprecated = cm.isDeprecated; | ||||
| this.hasOnlyReadOnly = cm.hasOnlyReadOnly; | ||||
| this.externalDocumentation = cm.externalDocumentation; | ||||
|
|
||||
| this.vendorExtensions = cm.vendorExtensions; | ||||
| this.additionalPropertiesType = cm.additionalPropertiesType; | ||||
| this.isAdditionalPropertiesTrue = cm.isAdditionalPropertiesTrue; | ||||
| this.setMaxProperties(cm.getMaxProperties()); | ||||
| this.setMinProperties(cm.getMinProperties()); | ||||
| this.setUniqueItems(cm.getUniqueItems()); | ||||
| this.setMaxItems(cm.getMaxItems()); | ||||
| this.setMinItems(cm.getMinItems()); | ||||
| this.setMaxLength(cm.getMaxLength()); | ||||
| this.setMinLength(cm.getMinLength()); | ||||
| this.setExclusiveMinimum(cm.getExclusiveMinimum()); | ||||
| this.setExclusiveMaximum(cm.getExclusiveMaximum()); | ||||
| this.setMinimum(cm.getMinimum()); | ||||
| this.setMaximum(cm.getMaximum()); | ||||
| this.setPattern(cm.getPattern()); | ||||
| this.setMultipleOf(cm.getMultipleOf()); | ||||
| this.setItems(cm.getItems()); | ||||
| this.setAdditionalProperties(cm.getAdditionalProperties()); | ||||
| this.setIsModel(cm.getIsModel()); | ||||
| this.setComposedSchemas(cm.getComposedSchemas()); | ||||
| this.setDiscriminator(cm.getDiscriminator()); | ||||
| } | ||||
|
|
||||
| } | ||||
| } | ||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| /** | ||
| * Represents a single attribute/property metadata entry in the attributeTypeMap. | ||
| * Used for validation and type matching in oneOf scenarios. | ||
| */ | ||
| export type AttributeTypeMapEntry = { | ||
| name: string; | ||
| baseName: string; | ||
| type: string; | ||
| format: string; | ||
| required: boolean; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { AttributeTypeMapEntry } from '../models/ModelTypes{{importFileExtension}}'; | ||
|
|
||
| /** | ||
| * Validates if data contains all required attributes from the attributeTypeMap. | ||
| * | ||
| * @param data - The data object to validate | ||
| * @param attributeTypeMap - Array of attribute metadata including required flag | ||
| * @returns true if all required attributes are present in data, false otherwise | ||
| */ | ||
| export function instanceOfType(data: any, attributeTypeMap: Array<AttributeTypeMapEntry>): boolean { | ||
| for (const attribute of attributeTypeMap) { | ||
| if (attribute.required && data[attribute.baseName] === undefined) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Attempts to find a matching type from an array of possible types by validating | ||
| * the data against each type's attribute requirements. | ||
| * | ||
| * @param data - The data object to match | ||
| * @param types - Array of possible type constructors | ||
| * @returns The name of the matching type, or undefined if no match found | ||
| */ | ||
| export function findMatchingType(data: any, types: Array<any>): string | undefined { | ||
| for (const type of types) { | ||
| if (instanceOfType(data, type.getAttributeTypeMap())) { | ||
| return type.name; | ||
| } | ||
| } | ||
|
|
||
| return undefined; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,14 @@ | ||
| {{>licenseInfo}} | ||
| {{#models}} | ||
| {{#model}} | ||
| {{#oneOf}} | ||
| {{#-first}}{{>model/modelOneOf}}{{/-first}} | ||
| {{/oneOf}} | ||
| {{^oneOf}} | ||
| {{#tsImports}} | ||
| import { {{classname}} } from '{{filename}}{{importFileExtension}}'; | ||
| {{/tsImports}} | ||
| import { AttributeTypeMapEntry } from '../models/ModelTypes{{importFileExtension}}'; | ||
| import { HttpFile } from '../http/http{{importFileExtension}}'; | ||
|
Member
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. are we sure this is not used in the oneOf case? |
||
|
|
||
| {{#description}} | ||
|
|
@@ -12,10 +17,6 @@ import { HttpFile } from '../http/http{{importFileExtension}}'; | |
| */ | ||
| {{/description}} | ||
| {{^isEnum}} | ||
| {{#oneOf}} | ||
| {{#-first}}{{>model/modelOneOf}}{{/-first}} | ||
| {{/oneOf}} | ||
| {{^oneOf}} | ||
| export class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{ | ||
| {{#vars}} | ||
| {{#description}} | ||
|
|
@@ -49,13 +50,14 @@ export class {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{ | |
| {{/hasDiscriminatorWithNonEmptyMapping}} | ||
|
|
||
| {{^isArray}} | ||
| static {{#parent}}override {{/parent}}readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [ | ||
| static {{#parent}}override {{/parent}}readonly attributeTypeMap: Array<AttributeTypeMapEntry> = [ | ||
| {{#vars}} | ||
| { | ||
| "name": "{{name}}", | ||
| "baseName": "{{baseName}}", | ||
| "type": "{{#isEnum}}{{{datatypeWithEnum}}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}", | ||
| "format": "{{dataFormat}}" | ||
| "format": "{{dataFormat}}", | ||
| "required": {{required}} | ||
| }{{^-last}}, | ||
| {{/-last}} | ||
| {{/vars}} | ||
|
|
@@ -100,7 +102,6 @@ export enum {{classname}}{{enumName}} { | |
| {{/vars}} | ||
|
|
||
| {{/hasEnums}} | ||
| {{/oneOf}} | ||
| {{/isEnum}} | ||
| {{#isEnum}} | ||
| export enum {{classname}} { | ||
|
|
@@ -111,5 +112,6 @@ export enum {{classname}} { | |
| {{/allowableValues}} | ||
| } | ||
| {{/isEnum}} | ||
| {{/oneOf}} | ||
| {{/model}} | ||
| {{/models}} | ||
Uh oh!
There was an error while loading. Please reload this page.