Skip to content
Open
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 bin/configs/typescript-fetch-any-of-union.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
generatorName: typescript-fetch
outputDir: samples/client/petstore/typescript-fetch/builds/any-of-union
inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-fetch/anyOf.yaml
templateDir: modules/openapi-generator/src/main/resources/typescript-fetch
additionalProperties:
generateAnyOfAsUnion: true
npmVersion: 1.0.0
npmName: '@openapitools/typescript-fetch-petstore'
npmRepository: https://skimdb.npmjs.com/registry
1 change: 1 addition & 0 deletions docs/generators/typescript-fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|enumPropertyNamingReplaceSpecialChar|Set to true to replace '-' and '+' symbols with 'minus_' and 'plus_' in enum of type string| |false|
|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response. With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|<dl><dt>**false**</dt><dd>No changes to the enums are made, this is the default option.</dd><dt>**true**</dt><dd>With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.</dd></dl>|false|
|fileNaming|Naming convention for the output files: 'PascalCase', 'camelCase', 'kebab-case'.| |PascalCase|
|generateAnyOfAsUnion|Generate anyOf schemas as TypeScript union types instead of one interface merging all members' properties. A value matching several object members keeps the properties of all of them. An anyOf schema that also declares its own properties stays an interface.| |false|
|importFileExtension|File extension to use with relative imports. Set it to '.js' or '.mjs' when using [ESM](https://nodejs.org/api/esm.html).| ||
|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|<dl><dt>**true**</dt><dd>The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.</dd><dt>**false**</dt><dd>The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.</dd></dl>|true|
|licenseName|The name of the license| |null|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
public static final String DATE_LIBRARY_DATE = "date";
public static final String DATE_LIBRARY_STRING = "string";
public static final String DATE_LIBRARY_TEMPORAL = "temporal";
public static final String GENERATE_ANY_OF_AS_UNION = "generateAnyOfAsUnion";
public static final String GENERATE_ANY_OF_AS_UNION_DESC = "Generate anyOf schemas as TypeScript union types instead of one interface merging all members' properties. A value matching several object members keeps the properties of all of them. An anyOf schema that also declares its own properties stays an interface.";
public static final String STRING_ENUMS = "stringEnums";
public static final String STRING_ENUMS_DESC = "Generate string enums instead of objects for enum values.";
public static final String IMPORT_FILE_EXTENSION_SWITCH = "importFileExtension";
Expand All @@ -96,6 +98,7 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
protected boolean addedModelIndex = false;
protected boolean withoutRuntimeChecks = false;
protected String dateLibrary = DATE_LIBRARY_DATE;
protected boolean generateAnyOfAsUnion = false;
protected boolean stringEnums = false;
protected String fileNaming = PASCAL_CASE;
protected String apiDocPath = "docs";
Expand Down Expand Up @@ -171,6 +174,7 @@ public TypeScriptFetchClientCodegen() {
dateLibraryOption.setEnum(dateOptions);
this.cliOptions.add(dateLibraryOption);
this.cliOptions.add(new CliOption(SAGAS_AND_RECORDS, "Setting this property to true will generate additional files for use with redux-saga and immutablejs.", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
this.cliOptions.add(new CliOption(GENERATE_ANY_OF_AS_UNION, GENERATE_ANY_OF_AS_UNION_DESC, SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
this.cliOptions.add(new CliOption(STRING_ENUMS, STRING_ENUMS_DESC, SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
this.cliOptions.add(new CliOption(IMPORT_FILE_EXTENSION_SWITCH, IMPORT_FILE_EXTENSION_SWITCH_DESC).defaultValue(""));
this.cliOptions.add(new CliOption(FILE_NAMING, "Naming convention for the output files: 'PascalCase', 'camelCase', 'kebab-case'.").defaultValue(this.fileNaming));
Expand Down Expand Up @@ -236,6 +240,14 @@ public void setDateLibrary(String dateLibrary) {
this.dateLibrary = dateLibrary;
}

public Boolean getGenerateAnyOfAsUnion() {
return this.generateAnyOfAsUnion;
}

public void setGenerateAnyOfAsUnion(Boolean generateAnyOfAsUnion) {
this.generateAnyOfAsUnion = generateAnyOfAsUnion;
}

public Boolean getStringEnums() {
return this.stringEnums;
}
Expand Down Expand Up @@ -344,6 +356,10 @@ public void processOpts() {
this.setWithoutRuntimeChecks(convertPropertyToBoolean(WITHOUT_RUNTIME_CHECKS));
}

if (additionalProperties.containsKey(GENERATE_ANY_OF_AS_UNION)) {
this.setGenerateAnyOfAsUnion(convertPropertyToBoolean(GENERATE_ANY_OF_AS_UNION));
}

if (additionalProperties.containsKey(STRING_ENUMS)) {
this.setStringEnums(convertPropertyToBoolean(STRING_ENUMS));
additionalProperties.put("stringEnums", this.stringEnums);
Expand Down Expand Up @@ -604,6 +620,22 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
}
}

// Split the oneOf/anyOf model members: the instanceOf check of an enum takes the primitive value itself,
// while the one of an object model needs an object.
Set<String> enumModelNames = allModels.stream()
.filter(m -> m.isEnum)
.map(m -> m.classname)
.collect(Collectors.toSet());
for (ExtendedCodegenModel m : allModels) {
for (String member : m.oneOfModels) {
if (enumModelNames.contains(member)) {
m.oneOfEnumModels.add(member);
} else {
m.oneOfObjectModels.add(member);
}
}
}

for (ExtendedCodegenModel rootModel : allModels) {
for (String curImport : rootModel.imports) {
boolean isModelImport = false;
Expand Down Expand Up @@ -834,7 +866,11 @@ public ExtendedCodegenProperty fromProperty(String name, Schema p, boolean requi
@Override
public ExtendedCodegenModel fromModel(String name, Schema model) {
CodegenModel cm = super.fromModel(name, model);
return new ExtendedCodegenModel(cm);
ExtendedCodegenModel result = new ExtendedCodegenModel(cm);
// The union template renders no properties, so an anyOf declaring its own properties stays an interface.
result.anyOfHasOwnProperties = model.getAnyOf() != null && !model.getAnyOf().isEmpty()
&& model.getProperties() != null && !model.getProperties().isEmpty();
return result;
}

@Override
Expand Down Expand Up @@ -1081,8 +1117,20 @@ private ExtendedCodegenModel processCodeGenModel(ExtendedCodegenModel cm) {
.map(CodegenComposedSchemas::getOneOf)
.orElse(Collections.emptyList());

// Type names omit branch nullability. Null is valid only when exactly one branch accepts it.
if (oneOfsList.stream().filter(cp -> cp.isNullable).count() == 1) {
// With generateAnyOfAsUnion an anyOf is modeled like a oneOf, so the oneOf templates render it as a union.
boolean fromAnyOf = this.generateAnyOfAsUnion && cm.oneOf.isEmpty() && !cm.anyOf.isEmpty() && !cm.anyOfHasOwnProperties;
if (fromAnyOf) {
oneOfsList = Optional.ofNullable(cm.getComposedSchemas())
.map(CodegenComposedSchemas::getAnyOf)
.orElse(Collections.emptyList());
cm.oneOf.addAll(cm.anyOf);
cm.isAnyOfUnion = true;
}

// Type names omit branch nullability. For oneOf, null is valid only when exactly one branch accepts it;
// for anyOf, when at least one branch does.
long nullableBranches = oneOfsList.stream().filter(cp -> cp.isNullable).count();
if (fromAnyOf ? nullableBranches >= 1 : nullableBranches == 1) {
cm.oneOf.add("null");
}

Expand Down Expand Up @@ -2107,6 +2155,9 @@ public class ExtendedCodegenModel extends CodegenModel {
// 'TypedJSON' conversion methods in the composed model classes.
@Getter @Setter
public Set<String> oneOfModels = new TreeSet<>();
// oneOfModels split by kind, see postProcessAllModels
public Set<String> oneOfObjectModels = new TreeSet<>();
public Set<String> oneOfEnumModels = new TreeSet<>();
@Getter @Setter
public Set<String> oneOfArrays = new TreeSet<>();
@Getter @Setter
Expand All @@ -2115,6 +2166,9 @@ public class ExtendedCodegenModel extends CodegenModel {
public CodegenDiscriminator.MappedModel selfReferencingDiscriminatorMapping;
@Getter @Setter
public boolean parentIsOneOf; // true when this model's parent is a oneOf union type
@Getter @Setter
public boolean isAnyOfUnion; // true when an anyOf is generated as a union type (generateAnyOfAsUnion)
public boolean anyOfHasOwnProperties; // true when an anyOf schema declares properties next to its members

public boolean isEntity; // Is a model containing an "id" property marked as isUniqueId
public String returnPassthrough;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,44 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole
}
{{/discriminator}}
{{^discriminator}}
{{#oneOfModels}}
{{#oneOfObjectModels}}
{{#-first}}
if (typeof json !== 'object') {
return json;
}
if (typeof json === 'object') {
{{#isAnyOfUnion}}
// anyOf: a value may match several members, so combine all of them instead of keeping the first match
let anyOfResult: any = {};
let anyOfMatched = false;
{{/isAnyOfUnion}}
{{/-first}}
{{#isAnyOfUnion}}
if (instanceOf{{{.}}}(json)) {
anyOfResult = { ...anyOfResult, ...{{{.}}}FromJSONTyped(json, true) };
anyOfMatched = true;
}
{{/isAnyOfUnion}}
{{^isAnyOfUnion}}
if (instanceOf{{{.}}}(json)) {
return {{{.}}}FromJSONTyped(json, true);
}
{{/isAnyOfUnion}}
{{#-last}}
{{#isAnyOfUnion}}
if (anyOfMatched) {
return anyOfResult;
}
{{/isAnyOfUnion}}
}
{{/-last}}
{{/oneOfObjectModels}}
{{#oneOfEnumModels}}
if (instanceOf{{{.}}}(json)) {
return {{{.}}}FromJSONTyped(json, true);
}
{{/oneOfModels}}
{{/oneOfEnumModels}}
{{#oneOfArrays}}
{{#-first}}
if (Array.isArray(json)) {
if (json.every(item => typeof item === 'object')) {
if (json.every(item => item !== null && typeof item === 'object')) {
{{/-first}}
if (json.every(item => instanceOf{{{.}}}(item))) {
return json.map(value => {{{.}}}FromJSONTyped(value, true));
Expand Down Expand Up @@ -197,20 +221,44 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis
}
{{/discriminator}}
{{^discriminator}}
{{#oneOfModels}}
{{#oneOfObjectModels}}
{{#-first}}
if (typeof value !== 'object') {
return value;
}
if (typeof value === 'object') {
{{#isAnyOfUnion}}
// anyOf: a value may match several members, so combine all of them instead of keeping the first match
let anyOfResult: any = {};
let anyOfMatched = false;
{{/isAnyOfUnion}}
{{/-first}}
{{#isAnyOfUnion}}
if (instanceOf{{{.}}}(value)) {
anyOfResult = { ...anyOfResult, ...{{{.}}}ToJSON(value as {{{.}}}) };
anyOfMatched = true;
}
{{/isAnyOfUnion}}
{{^isAnyOfUnion}}
if (instanceOf{{{.}}}(value)) {
return {{{.}}}ToJSON(value as {{{.}}});
}
{{/isAnyOfUnion}}
{{#-last}}
{{#isAnyOfUnion}}
if (anyOfMatched) {
return anyOfResult;
}
{{/isAnyOfUnion}}
}
{{/-last}}
{{/oneOfObjectModels}}
{{#oneOfEnumModels}}
if (instanceOf{{{.}}}(value)) {
return {{{.}}}ToJSON(value as {{{.}}});
}
{{/oneOfModels}}
{{/oneOfEnumModels}}
{{#oneOfArrays}}
{{#-first}}
if (Array.isArray(value)) {
if (value.every(item => typeof item === 'object')) {
if (value.every(item => item !== null && typeof item === 'object')) {
{{/-first}}
if (value.every(item => instanceOf{{{.}}}(item))) {
return value.map(value => {{{.}}}ToJSON(value as {{{.}}}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class TypeScriptFetchClientOptionsProvider implements TypeScriptSharedCli
private static final String NPM_REPOSITORY = "https://registry.npmjs.org";
public static final String SAGAS_AND_RECORDS = "false";
public static final String STRING_ENUMS = "false";
public static final String GENERATE_ANY_OF_AS_UNION = "false";
public static final String FILE_NAMING_VALUE = PASCAL_CASE;
public static final String DATE_LIBRARY_VALUE = TypeScriptFetchClientCodegen.DATE_LIBRARY_STRING;

Expand All @@ -53,6 +54,7 @@ public Map<String, String> createOptions() {
.put(TypeScriptFetchClientCodegen.FILE_NAMING, FILE_NAMING_VALUE)
.put(TypeScriptFetchClientCodegen.DATE_LIBRARY, DATE_LIBRARY_VALUE)
.put(TypeScriptFetchClientCodegen.STRING_ENUMS, STRING_ENUMS)
.put(TypeScriptFetchClientCodegen.GENERATE_ANY_OF_AS_UNION, GENERATE_ANY_OF_AS_UNION)
.put(TypeScriptFetchClientCodegen.USE_SQUARE_BRACKETS_IN_ARRAY_NAMES, Boolean.FALSE.toString())
.put(TypeScriptFetchClientCodegen.VALIDATION_ATTRIBUTES, Boolean.FALSE.toString())
.put(TypeScriptFetchClientCodegen.WITH_REQUEST_OPTS_IN_INTERFACE, Boolean.TRUE.toString())
Expand Down
Loading