From a7bbccece4db88921aaa502f865d8032ed7ddff5 Mon Sep 17 00:00:00 2001 From: Pavel Vostretsov Date: Tue, 19 Feb 2019 18:02:31 +0500 Subject: [PATCH] Global nullable option --- .../CustomGenerator/array-types.expected.js | 2 +- .../CustomGenerator/array-types.expected.ts | 2 +- .../global-nullable-disabled.js | 15 ++++ .../global-nullable-disabled.ts | 15 ++++ .../global-nullable-enabled.js | 11 +++ .../global-nullable-enabled.ts | 11 +++ .../SimpleGenerator/array-types.expected.js | 2 +- .../SimpleGenerator/array-types.expected.ts | 2 +- .../OptionsTests.cs | 10 +++ .../TypeScript.ContractGenerator.Tests.csproj | 12 ++++ .../Types/GlobalNullableRootType.cs | 16 +++++ .../FlowTypeGenerationOptions.cs | 2 + .../FlowTypeGenerator.cs | 69 +++++++++---------- .../BuildInTypeBuildingContext.cs | 40 ++++------- .../CustomTypeTypeBuildingContext.cs | 21 ++++-- .../NullableTypeBuildingContext.cs | 32 +++++++++ 16 files changed, 190 insertions(+), 72 deletions(-) create mode 100644 TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-disabled.js create mode 100644 TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-disabled.ts create mode 100644 TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-enabled.js create mode 100644 TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-enabled.ts create mode 100644 TypeScript.ContractGenerator.Tests/Types/GlobalNullableRootType.cs create mode 100644 TypeScript.ContractGenerator/TypeBuilders/NullableTypeBuildingContext.cs diff --git a/TypeScript.ContractGenerator.Tests/Files/CustomGenerator/array-types.expected.js b/TypeScript.ContractGenerator.Tests/Files/CustomGenerator/array-types.expected.js index 7b498fa..8fe42bb 100644 --- a/TypeScript.ContractGenerator.Tests/Files/CustomGenerator/array-types.expected.js +++ b/TypeScript.ContractGenerator.Tests/Files/CustomGenerator/array-types.expected.js @@ -2,7 +2,7 @@ export type ArrayRootType = { ints?: null | number[]; nullableInts?: null | Nullable[]; - byteArray?: null | Byte[]; + byteArray?: null | string; nullableByteArray?: null | Nullable[]; enums?: null | AnotherEnum[]; nullableEnums?: null | Nullable[]; diff --git a/TypeScript.ContractGenerator.Tests/Files/CustomGenerator/array-types.expected.ts b/TypeScript.ContractGenerator.Tests/Files/CustomGenerator/array-types.expected.ts index a850037..f4baf84 100644 --- a/TypeScript.ContractGenerator.Tests/Files/CustomGenerator/array-types.expected.ts +++ b/TypeScript.ContractGenerator.Tests/Files/CustomGenerator/array-types.expected.ts @@ -2,7 +2,7 @@ export type ArrayRootType = { ints?: null | number[]; nullableInts?: null | Nullable[]; - byteArray?: null | Byte[]; + byteArray?: null | string; nullableByteArray?: null | Nullable[]; enums?: null | AnotherEnum[]; nullableEnums?: null | Nullable[]; diff --git a/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-disabled.js b/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-disabled.js new file mode 100644 index 0000000..cd63271 --- /dev/null +++ b/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-disabled.js @@ -0,0 +1,15 @@ + +export type GlobalNullableRootType = { + int: number; + nullableInt?: null | number; + nullableInts?: null | Nullable[]; + intGeneric?: null | GenericClass; + nullableIntGeneric?: null | GenericClass>; +}; +export type Nullable = { + hasValue: boolean; + value: T; +}; +export type GenericClass = { + genericType?: T; +}; diff --git a/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-disabled.ts b/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-disabled.ts new file mode 100644 index 0000000..cd63271 --- /dev/null +++ b/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-disabled.ts @@ -0,0 +1,15 @@ + +export type GlobalNullableRootType = { + int: number; + nullableInt?: null | number; + nullableInts?: null | Nullable[]; + intGeneric?: null | GenericClass; + nullableIntGeneric?: null | GenericClass>; +}; +export type Nullable = { + hasValue: boolean; + value: T; +}; +export type GenericClass = { + genericType?: T; +}; diff --git a/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-enabled.js b/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-enabled.js new file mode 100644 index 0000000..c2a2154 --- /dev/null +++ b/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-enabled.js @@ -0,0 +1,11 @@ + +export type GlobalNullableRootType = { + int: number; + nullableInt?: ?number; + nullableInts?: ??number[]; + intGeneric?: ?GenericClass; + nullableIntGeneric?: ?GenericClass; +}; +export type GenericClass = { + genericType?: T; +}; diff --git a/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-enabled.ts b/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-enabled.ts new file mode 100644 index 0000000..b379fca --- /dev/null +++ b/TypeScript.ContractGenerator.Tests/Files/Options.Expected/global-nullable-enabled.ts @@ -0,0 +1,11 @@ + +export type GlobalNullableRootType = { + int: number; + nullableInt?: Nullable; + nullableInts?: Nullable[]>; + intGeneric?: Nullable>; + nullableIntGeneric?: Nullable>>; +}; +export type GenericClass = { + genericType?: T; +}; diff --git a/TypeScript.ContractGenerator.Tests/Files/SimpleGenerator/array-types.expected.js b/TypeScript.ContractGenerator.Tests/Files/SimpleGenerator/array-types.expected.js index d22c74c..21823aa 100644 --- a/TypeScript.ContractGenerator.Tests/Files/SimpleGenerator/array-types.expected.js +++ b/TypeScript.ContractGenerator.Tests/Files/SimpleGenerator/array-types.expected.js @@ -2,7 +2,7 @@ export type ArrayRootType = { ints?: null | number[]; nullableInts?: null | Nullable[]; - byteArray?: null | Byte[]; + byteArray?: null | string; nullableByteArray?: null | Nullable[]; enums?: null | AnotherEnum[]; nullableEnums?: null | Nullable[]; diff --git a/TypeScript.ContractGenerator.Tests/Files/SimpleGenerator/array-types.expected.ts b/TypeScript.ContractGenerator.Tests/Files/SimpleGenerator/array-types.expected.ts index 83a5376..6823a51 100644 --- a/TypeScript.ContractGenerator.Tests/Files/SimpleGenerator/array-types.expected.ts +++ b/TypeScript.ContractGenerator.Tests/Files/SimpleGenerator/array-types.expected.ts @@ -2,7 +2,7 @@ export type ArrayRootType = { ints?: null | number[]; nullableInts?: null | Nullable[]; - byteArray?: null | Byte[]; + byteArray?: null | string; nullableByteArray?: null | Nullable[]; enums?: null | AnotherEnum[]; nullableEnums?: null | Nullable[]; diff --git a/TypeScript.ContractGenerator.Tests/OptionsTests.cs b/TypeScript.ContractGenerator.Tests/OptionsTests.cs index ec9db3e..d93ed6e 100644 --- a/TypeScript.ContractGenerator.Tests/OptionsTests.cs +++ b/TypeScript.ContractGenerator.Tests/OptionsTests.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using FluentAssertions; @@ -42,5 +43,14 @@ public void ExplicitNullabilityTest(bool explicitNullabilityEnabled, string expe var expectedCode = GetExpectedCode($"Options.Expected/{expectedFileName}"); generatedCode.Should().Be(expectedCode); } + + [TestCase(true, "global-nullable-enabled")] + [TestCase(false, "global-nullable-disabled")] + public void GlobalNullableTest(bool useGlobalNullable, string expectedFileName) + { + var generatedCode = GenerateCode(new FlowTypeGenerationOptions {UseGlobalNullable = useGlobalNullable}, CustomTypeGenerator.Null, typeof(GlobalNullableRootType)).Single().Replace("\r\n", "\n"); + var expectedCode = GetExpectedCode($"Options.Expected/{expectedFileName}"); + generatedCode.Should().Be(expectedCode); + } } } \ No newline at end of file diff --git a/TypeScript.ContractGenerator.Tests/TypeScript.ContractGenerator.Tests.csproj b/TypeScript.ContractGenerator.Tests/TypeScript.ContractGenerator.Tests.csproj index d267cc5..4aa5a57 100644 --- a/TypeScript.ContractGenerator.Tests/TypeScript.ContractGenerator.Tests.csproj +++ b/TypeScript.ContractGenerator.Tests/TypeScript.ContractGenerator.Tests.csproj @@ -218,6 +218,18 @@ Always + + Always + + + Always + + + Always + + + Always + diff --git a/TypeScript.ContractGenerator.Tests/Types/GlobalNullableRootType.cs b/TypeScript.ContractGenerator.Tests/Types/GlobalNullableRootType.cs new file mode 100644 index 0000000..33d0c09 --- /dev/null +++ b/TypeScript.ContractGenerator.Tests/Types/GlobalNullableRootType.cs @@ -0,0 +1,16 @@ +namespace SkbKontur.TypeScript.ContractGenerator.Tests.Types +{ + public class GlobalNullableRootType + { + public int Int { get; set; } + public int? NullableInt { get; set; } + public int?[] NullableInts { get; set; } + public GenericClass IntGeneric { get; set; } + public GenericClass NullableIntGeneric { get; set; } + } + + public class GenericClass + { + public T GenericType { get; set; } + } +} \ No newline at end of file diff --git a/TypeScript.ContractGenerator/FlowTypeGenerationOptions.cs b/TypeScript.ContractGenerator/FlowTypeGenerationOptions.cs index 2b3b846..c6f333f 100644 --- a/TypeScript.ContractGenerator/FlowTypeGenerationOptions.cs +++ b/TypeScript.ContractGenerator/FlowTypeGenerationOptions.cs @@ -11,6 +11,8 @@ public class FlowTypeGenerationOptions public bool EnableExplicitNullability { get; set; } = true; + public bool UseGlobalNullable { get; set; } + [NotNull] public static FlowTypeGenerationOptions Default { get; } = new FlowTypeGenerationOptions(); } diff --git a/TypeScript.ContractGenerator/FlowTypeGenerator.cs b/TypeScript.ContractGenerator/FlowTypeGenerator.cs index 063a970..4a9a70c 100644 --- a/TypeScript.ContractGenerator/FlowTypeGenerator.cs +++ b/TypeScript.ContractGenerator/FlowTypeGenerator.cs @@ -76,48 +76,43 @@ public ITypeBuildingContext ResolveType(Type type) return flowTypeDeclarations[type]; } var typeLocation = customTypeGenerator.GetTypeLocation(type); - var typeBuildingContext = customTypeGenerator.ResolveType(typeLocation, type, flowTypeUnitFactory); - if (typeBuildingContext == null) - { - if (BuildInTypeBuildingContext.Accept(type)) - { - typeBuildingContext = new BuildInTypeBuildingContext(type); - } - if (type.IsArray) - { - typeBuildingContext = new ArrayTypeBuildingContext(type.GetElementType()); - } - if (type.IsEnum) - { - var targetUnit = flowTypeUnitFactory.GetOrCreateTypeUnit(typeLocation); - typeBuildingContext = options.EnumGenerationMode == EnumGenerationMode.FixedStringsAndDictionary - ? (ITypeBuildingContext)new FixedStringsAndDictionaryTypeBuildingContext(targetUnit, type) - : new TypeScriptEnumTypeBuildingContext(targetUnit, type); - } - if (type.IsGenericType && !type.IsGenericTypeDefinition) - { - typeBuildingContext = new GenericTypeTypeBuildingContext(type); - } - if (type.IsGenericParameter) - { - typeBuildingContext = new GenericParameterTypeBuildingContext(type); - } - if (type.IsGenericTypeDefinition) - { - var targetUnit = flowTypeUnitFactory.GetOrCreateTypeUnit(typeLocation); - typeBuildingContext = new CustomTypeTypeBuildingContext(targetUnit, type, options); - } - if (typeBuildingContext == null) - { - var targetUnit = flowTypeUnitFactory.GetOrCreateTypeUnit(typeLocation); - typeBuildingContext = new CustomTypeTypeBuildingContext(targetUnit, type, options); - } - } + var typeBuildingContext = customTypeGenerator.ResolveType(typeLocation, type, flowTypeUnitFactory) ?? GetTypeBuildingContext(typeLocation, type); typeBuildingContext.Initialize(this); flowTypeDeclarations.Add(type, typeBuildingContext); return typeBuildingContext; } + private ITypeBuildingContext GetTypeBuildingContext(string typeLocation, Type type) + { + if (BuildInTypeBuildingContext.Accept(type)) + return new BuildInTypeBuildingContext(type); + + if (type.IsArray) + return new ArrayTypeBuildingContext(type.GetElementType()); + + if (type.IsEnum) + { + var targetUnit = flowTypeUnitFactory.GetOrCreateTypeUnit(typeLocation); + return options.EnumGenerationMode == EnumGenerationMode.FixedStringsAndDictionary + ? (ITypeBuildingContext)new FixedStringsAndDictionaryTypeBuildingContext(targetUnit, type) + : new TypeScriptEnumTypeBuildingContext(targetUnit, type); + } + + if (options.UseGlobalNullable && type.IsGenericType && !type.IsGenericTypeDefinition && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + return new NullableTypeBuildingContext(type); + + if (type.IsGenericType && !type.IsGenericTypeDefinition) + return new GenericTypeTypeBuildingContext(type); + + if (type.IsGenericParameter) + return new GenericParameterTypeBuildingContext(type); + + if (type.IsGenericTypeDefinition) + return new CustomTypeTypeBuildingContext(flowTypeUnitFactory.GetOrCreateTypeUnit(typeLocation), type, options); + + return new CustomTypeTypeBuildingContext(flowTypeUnitFactory.GetOrCreateTypeUnit(typeLocation), type, options); + } + public FlowTypeType BuildAndImportType(FlowTypeUnit targetUnit, ICustomAttributeProvider attributeProvider, Type type) { var (isNullable, resultType) = FlowTypeGeneratorHelpers.ProcessNullable(attributeProvider, type); diff --git a/TypeScript.ContractGenerator/TypeBuilders/BuildInTypeBuildingContext.cs b/TypeScript.ContractGenerator/TypeBuilders/BuildInTypeBuildingContext.cs index 880ea35..ae0dc2f 100644 --- a/TypeScript.ContractGenerator/TypeBuilders/BuildInTypeBuildingContext.cs +++ b/TypeScript.ContractGenerator/TypeBuilders/BuildInTypeBuildingContext.cs @@ -1,5 +1,5 @@ using System; -using System.Linq; +using System.Collections.Generic; using SkbKontur.TypeScript.ContractGenerator.CodeDom; @@ -14,27 +14,13 @@ public BuildInTypeBuildingContext(Type type) public static bool Accept(Type type) { - return buildInTyped.Contains(type); + return builtinTypes.ContainsKey(type); } public FlowTypeType ReferenceFrom(FlowTypeUnit targetUnit, ITypeGenerator typeGenerator) { - if (type == typeof(string)) - return new FlowTypeBuildInType("string"); - if (type == typeof(bool)) - return new FlowTypeBuildInType("boolean"); - if (type == typeof(int)) - return new FlowTypeBuildInType("number"); - if (type == typeof(decimal)) - return new FlowTypeBuildInType("number"); - if (type == typeof(long)) - return new FlowTypeBuildInType("string"); - if (type == typeof(DateTime)) - return new FlowTypeBuildInType("(Date | string)"); - if (type == typeof(byte[])) - return new FlowTypeBuildInType("string"); - if (type == typeof(void)) - return new FlowTypeBuildInType("void"); + if (builtinTypes.ContainsKey(type)) + return new FlowTypeBuildInType(builtinTypes[type]); throw new ArgumentOutOfRangeException(); } @@ -50,16 +36,16 @@ public void BuildDefinition(ITypeGenerator typeGenerator) private readonly Type type; - private static readonly Type[] buildInTyped = + private static readonly Dictionary builtinTypes = new Dictionary { - typeof(string), - typeof(bool), - typeof(int), - typeof(decimal), - typeof(long), - typeof(DateTime), - typeof(void), - typeof(byte[]), + {typeof(string), "string"}, + {typeof(bool), "boolean"}, + {typeof(int), "number"}, + {typeof(decimal), "number"}, + {typeof(long), "string"}, + {typeof(DateTime), "(Date | string)"}, + {typeof(byte[]), "string"}, + {typeof(void), "void"} }; } } \ No newline at end of file diff --git a/TypeScript.ContractGenerator/TypeBuilders/CustomTypeTypeBuildingContext.cs b/TypeScript.ContractGenerator/TypeBuilders/CustomTypeTypeBuildingContext.cs index 2b56138..079619e 100644 --- a/TypeScript.ContractGenerator/TypeBuilders/CustomTypeTypeBuildingContext.cs +++ b/TypeScript.ContractGenerator/TypeBuilders/CustomTypeTypeBuildingContext.cs @@ -55,19 +55,32 @@ protected virtual FlowTypeTypeDefintion CreateComplexFlowTypeDefinition(ITypeGen { var (isNullable, type) = FlowTypeGeneratorHelpers.ProcessNullable(property, property.PropertyType); - var propertyType = typeGenerator.BuildAndImportType(Unit, null, type); result.Members.Add(new FlowTypeTypeMemberDeclaration { Name = BuildPropertyName(property.Name), Optional = isNullable && options.EnableOptionalProperties, - Type = property.PropertyType.IsGenericParameter - ? new FlowTypeTypeReference(property.PropertyType.Name) - : isNullable && options.EnableExplicitNullability ? OrNull(propertyType) : propertyType, + Type = GetMaybeNullableComplexType(typeGenerator, type, property, isNullable), }); } return result; } + private FlowTypeType GetMaybeNullableComplexType(ITypeGenerator typeGenerator, Type type, PropertyInfo property, bool isNullable) + { + var propertyType = typeGenerator.BuildAndImportType(Unit, null, type); + + if (property.PropertyType.IsGenericParameter) + return new FlowTypeTypeReference(property.PropertyType.Name); + + if (isNullable && options.EnableExplicitNullability && !options.UseGlobalNullable) + return OrNull(propertyType); + + if (isNullable && options.EnableExplicitNullability && options.UseGlobalNullable) + return new FlowTypeNullableType(propertyType); + + return propertyType; + } + private static FlowTypeUnionType OrNull(FlowTypeType buildAndImportType) { return new FlowTypeUnionType( diff --git a/TypeScript.ContractGenerator/TypeBuilders/NullableTypeBuildingContext.cs b/TypeScript.ContractGenerator/TypeBuilders/NullableTypeBuildingContext.cs new file mode 100644 index 0000000..0eb00ae --- /dev/null +++ b/TypeScript.ContractGenerator/TypeBuilders/NullableTypeBuildingContext.cs @@ -0,0 +1,32 @@ +using System; + +using SkbKontur.TypeScript.ContractGenerator.CodeDom; + +namespace SkbKontur.TypeScript.ContractGenerator.TypeBuilders +{ + public class NullableTypeBuildingContext : ITypeBuildingContext + { + public NullableTypeBuildingContext(Type nullableType) + { + itemType = nullableType.GetGenericArguments()[0]; + } + + public bool IsDefinitionBuilt => true; + + public void Initialize(ITypeGenerator typeGenerator) + { + } + + public void BuildDefinition(ITypeGenerator typeGenerator) + { + } + + public FlowTypeType ReferenceFrom(FlowTypeUnit targetUnit, ITypeGenerator typeGenerator) + { + var itemFlowType = typeGenerator.ResolveType(itemType).ReferenceFrom(targetUnit, typeGenerator); + return new FlowTypeNullableType(itemFlowType); + } + + private readonly Type itemType; + } +} \ No newline at end of file