From 9bf8f695b373410e8b51e1363270c08dda7b8127 Mon Sep 17 00:00:00 2001 From: Rico Suter Date: Wed, 17 Jul 2024 15:49:03 +0200 Subject: [PATCH] TypeScript generator: Use string type as the discriminator property type in specialized interfaces (#1718) * improve TypeScript interface generation * TypeScript generator: Use string type as the discriminator property type in specialized interfaces * fix tests --- .../EnumGenerationTests.cs | 4 +- ...ce_contract_add_discriminator.verified.txt | 34 ++++ ..._discriminator_string_literal.verified.txt | 31 +++ ...act_then_generate_union_class.verified.txt | 181 ++++++++++++++++++ ...then_generate_union_interface.verified.txt | 34 ++++ .../TypeScriptDiscriminatorTests.cs | 14 +- .../Models/EnumTemplateModel.cs | 4 +- .../Models/PropertyModel.cs | 29 ++- 8 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_generating_interface_contract_add_discriminator.verified.txt create mode 100644 src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_generating_interface_contract_add_discriminator_string_literal.verified.txt create mode 100644 src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_parameter_is_abstract_then_generate_union_class.verified.txt create mode 100644 src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_parameter_is_abstract_then_generate_union_interface.verified.txt diff --git a/src/NJsonSchema.CodeGeneration.Tests/EnumGenerationTests.cs b/src/NJsonSchema.CodeGeneration.Tests/EnumGenerationTests.cs index bb77d9326..eb85ea960 100644 --- a/src/NJsonSchema.CodeGeneration.Tests/EnumGenerationTests.cs +++ b/src/NJsonSchema.CodeGeneration.Tests/EnumGenerationTests.cs @@ -166,8 +166,8 @@ public async Task When_enum_has_string_value_then_TS_code_has_string_value() var code = generator.GenerateFile("MyClass"); //// Assert - Assert.Contains("_0562 = \"0562\",", code); - Assert.Contains("_0532 = \"0532\",", code); + Assert.Contains("_0562 = \"0562\",", code); + Assert.Contains("_0532 = \"0532\",", code); } public class ClassWithStringEnum diff --git a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_generating_interface_contract_add_discriminator.verified.txt b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_generating_interface_contract_add_discriminator.verified.txt new file mode 100644 index 000000000..b9362d308 --- /dev/null +++ b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_generating_interface_contract_add_discriminator.verified.txt @@ -0,0 +1,34 @@ +//---------------------- +// +// +//---------------------- + + + + + + + +export interface Base { + Type: EBase; +} + +export enum EBase { + OneChild = "OneChild", + SecondChild = "SecondChild", +} + +export interface OneChild extends Base { + A: string; + Type: EBase.OneChild; +} + +export interface SecondChild extends Base { + B: string; + Type: EBase.SecondChild; +} + +export interface MyClass { + Child: Base; + Children: Base[]; +} \ No newline at end of file diff --git a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_generating_interface_contract_add_discriminator_string_literal.verified.txt b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_generating_interface_contract_add_discriminator_string_literal.verified.txt new file mode 100644 index 000000000..a05edf0c6 --- /dev/null +++ b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_generating_interface_contract_add_discriminator_string_literal.verified.txt @@ -0,0 +1,31 @@ +//---------------------- +// +// +//---------------------- + + + + + + + +export interface Base { + Type: EBase; +} + +export type EBase = "OneChild" | "SecondChild"; + +export interface OneChild extends Base { + A: string; + Type: 'OneChild'; +} + +export interface SecondChild extends Base { + B: string; + Type: 'SecondChild'; +} + +export interface MyClass { + Child: Base; + Children: Base[]; +} \ No newline at end of file diff --git a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_parameter_is_abstract_then_generate_union_class.verified.txt b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_parameter_is_abstract_then_generate_union_class.verified.txt new file mode 100644 index 000000000..fa8a3a3f1 --- /dev/null +++ b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_parameter_is_abstract_then_generate_union_class.verified.txt @@ -0,0 +1,181 @@ +//---------------------- +// +// +//---------------------- + + + + + + + +export abstract class Base implements IBase { + + protected _discriminator: string; + + constructor(data?: IBase) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + this._discriminator = "Base"; + } + + init(_data?: any) { + } + + static fromJS(data: any): Base { + data = typeof data === 'object' ? data : {}; + if (data["Type"] === "OneChild") { + let result = new OneChild(); + result.init(data); + return result; + } + if (data["Type"] === "SecondChild") { + let result = new SecondChild(); + result.init(data); + return result; + } + throw new Error("The abstract class 'Base' cannot be instantiated."); + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["Type"] = this._discriminator; + return data; + } +} + +export interface IBase { +} + +export class OneChild extends Base implements IOneChild { + a: string; + type: EBase; + + constructor(data?: IOneChild) { + super(data); + this._discriminator = "OneChild"; + } + + init(_data?: any) { + super.init(_data); + if (_data) { + this.a = _data["A"]; + this.type = _data["Type"]; + } + } + + static fromJS(data: any): OneChild { + data = typeof data === 'object' ? data : {}; + let result = new OneChild(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["A"] = this.a; + data["Type"] = this.type; + super.toJSON(data); + return data; + } +} + +export interface IOneChild extends IBase { + a: string; + type: EBase; +} + +export enum EBase { + OneChild = "OneChild", + SecondChild = "SecondChild", +} + +export class SecondChild extends Base implements ISecondChild { + b: string; + type: EBase; + + constructor(data?: ISecondChild) { + super(data); + this._discriminator = "SecondChild"; + } + + init(_data?: any) { + super.init(_data); + if (_data) { + this.b = _data["B"]; + this.type = _data["Type"]; + } + } + + static fromJS(data: any): SecondChild { + data = typeof data === 'object' ? data : {}; + let result = new SecondChild(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["B"] = this.b; + data["Type"] = this.type; + super.toJSON(data); + return data; + } +} + +export interface ISecondChild extends IBase { + b: string; + type: EBase; +} + +export class MyClass implements IMyClass { + child: OneChild | SecondChild; + children: (OneChild | SecondChild)[]; + + constructor(data?: IMyClass) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.child = _data["Child"] ? OneChild | SecondChild.fromJS(_data["Child"]) : undefined; + if (Array.isArray(_data["Children"])) { + this.children = [] as any; + for (let item of _data["Children"]) + this.children.push(OneChild | SecondChild.fromJS(item)); + } + } + } + + static fromJS(data: any): MyClass { + data = typeof data === 'object' ? data : {}; + let result = new MyClass(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["Child"] = this.child ? this.child.toJSON() : undefined; + if (Array.isArray(this.children)) { + data["Children"] = []; + for (let item of this.children) + data["Children"].push(item.toJSON()); + } + return data; + } +} + +export interface IMyClass { + child: OneChild | SecondChild; + children: (OneChild | SecondChild)[]; +} \ No newline at end of file diff --git a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_parameter_is_abstract_then_generate_union_interface.verified.txt b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_parameter_is_abstract_then_generate_union_interface.verified.txt new file mode 100644 index 000000000..50fdde97f --- /dev/null +++ b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/Snapshots/TypeScriptDiscriminatorTests.When_parameter_is_abstract_then_generate_union_interface.verified.txt @@ -0,0 +1,34 @@ +//---------------------- +// +// +//---------------------- + + + + + + + +export interface Base { + Type: string; +} + +export interface OneChild extends Base { + A: string; + Type: EBase.OneChild; +} + +export enum EBase { + OneChild = "OneChild", + SecondChild = "SecondChild", +} + +export interface SecondChild extends Base { + B: string; + Type: EBase.SecondChild; +} + +export interface MyClass { + Child: OneChild | SecondChild; + Children: (OneChild | SecondChild)[]; +} \ No newline at end of file diff --git a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/TypeScriptDiscriminatorTests.cs b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/TypeScriptDiscriminatorTests.cs index a75134a04..52b598912 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/TypeScriptDiscriminatorTests.cs +++ b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/TypeScriptDiscriminatorTests.cs @@ -1,14 +1,16 @@ using System.Threading.Tasks; using Newtonsoft.Json; -using NJsonSchema.Generation; using System.Collections.Generic; using System.Runtime.Serialization; using Xunit; using NJsonSchema.NewtonsoftJson.Converters; using NJsonSchema.NewtonsoftJson.Generation; +using VerifyXunit; +using Newtonsoft.Json.Converters; namespace NJsonSchema.CodeGeneration.TypeScript.Tests { + [UsesVerify] public class TypeScriptDiscriminatorTests { [JsonConverter(typeof(JsonInheritanceConverter), nameof(Type))] @@ -19,6 +21,7 @@ public abstract class Base public abstract EBase Type { get; } } + [JsonConverter(typeof(StringEnumConverter))] public enum EBase { OneChild, @@ -55,6 +58,7 @@ public async Task When_generating_interface_contract_add_discriminator() GenerateAbstractProperties = true, }); var data = schema.ToJson(); + var json = JsonConvert.SerializeObject(new OneChild()); //// Act var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings @@ -66,8 +70,9 @@ public async Task When_generating_interface_contract_add_discriminator() //// Assert Assert.Contains("export interface Base {\n Type: EBase;\n}", code); + await VerifyHelper.Verify(code); } - + [Fact] public async Task When_generating_interface_contract_add_discriminator_string_literal() { @@ -89,6 +94,7 @@ public async Task When_generating_interface_contract_add_discriminator_string_li //// Assert Assert.Contains("export interface Base {\n Type: EBase;\n}", code); + await VerifyHelper.Verify(code); } [Fact] @@ -112,8 +118,9 @@ public async Task When_parameter_is_abstract_then_generate_union_interface() Assert.Contains("export interface SecondChild extends Base", code); Assert.Contains("Child: OneChild | SecondChild;", code); Assert.Contains("Children: (OneChild | SecondChild)[];", code); + await VerifyHelper.Verify(code); } - + [Fact] public async Task When_parameter_is_abstract_then_generate_union_class() { @@ -135,6 +142,7 @@ public async Task When_parameter_is_abstract_then_generate_union_class() Assert.Contains("export class SecondChild extends Base", code); Assert.Contains("child: OneChild | SecondChild;", code); Assert.Contains("children: (OneChild | SecondChild)[];", code); + await VerifyHelper.Verify(code); } } } diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/Models/EnumTemplateModel.cs b/src/NJsonSchema.CodeGeneration.TypeScript/Models/EnumTemplateModel.cs index e2a7483b4..84e2606bc 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript/Models/EnumTemplateModel.cs +++ b/src/NJsonSchema.CodeGeneration.TypeScript/Models/EnumTemplateModel.cs @@ -62,9 +62,7 @@ public List Enums entries.Add(new EnumerationItemModel { Name = _settings.EnumNameGenerator.Generate(i, name, value, _schema), - Value = _schema.Type.IsInteger() ? - value.ToString() : - (_settings.TypeScriptVersion < 2.4m ? "" : "") + "\"" + value + "\"", + Value = _schema.Type.IsInteger() ? value.ToString() : "\"" + value + "\"", }); } } diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/Models/PropertyModel.cs b/src/NJsonSchema.CodeGeneration.TypeScript/Models/PropertyModel.cs index f1e07c252..fc247f2cb 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript/Models/PropertyModel.cs +++ b/src/NJsonSchema.CodeGeneration.TypeScript/Models/PropertyModel.cs @@ -19,6 +19,7 @@ public class PropertyModel : PropertyModelBase private readonly string _parentTypeName; private readonly TypeScriptGeneratorSettings _settings; + private readonly ClassTemplateModel _classTemplateModel; private readonly JsonSchemaProperty _property; private readonly TypeScriptTypeResolver _resolver; @@ -35,6 +36,7 @@ public PropertyModel( TypeScriptGeneratorSettings settings) : base(property, classTemplateModel, typeResolver, settings) { + _classTemplateModel = classTemplateModel; _property = property; _resolver = typeResolver; _parentTypeName = parentTypeName; @@ -51,7 +53,32 @@ public PropertyModel( public string? Description => _property.Description; /// Gets the type of the property. - public override string Type => _resolver.Resolve(_property, _property.IsNullable(_settings.SchemaType), GetTypeNameHint()); + public override string Type + { + get + { + if (_settings.TypeStyle == TypeScriptTypeStyle.Interface && + _classTemplateModel.HasInheritance && + InterfaceName == _classTemplateModel.BaseDiscriminator) + { + // use string type as the discriminator property type in specialized interfaces + if (_property.ActualTypeSchema.IsEnumeration && + _settings.EnumStyle == TypeScriptEnumStyle.Enum) + { + return _resolver.Resolve(_property, _property.IsNullable(_settings.SchemaType), GetTypeNameHint()) + "." + + _classTemplateModel.DiscriminatorName; + } + else + { + return $"'{_classTemplateModel.DiscriminatorName}'"; + } + } + else + { + return _resolver.Resolve(_property, _property.IsNullable(_settings.SchemaType), GetTypeNameHint()); + } + } + } /// Gets the type of the property in the initializer interface. public string ConstructorInterfaceType => _settings.ConvertConstructorInterfaceData ?