From e6b853c6484c3733ce61600c130a6e024718ab1c Mon Sep 17 00:00:00 2001 From: tterrag1098 Date: Wed, 26 Jul 2023 20:04:27 -0400 Subject: [PATCH 1/6] Try to use object constructors for complex property values where possible --- .../Generator/Services/TsContentGenerator.cs | 70 +++++++++++++++---- .../Entities/DefaultMemberValues.cs | 4 +- .../Expected/default-member-values.ts | 3 +- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs index d7972cf3..0cfc1c53 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; @@ -312,25 +313,21 @@ public string GetCustomHead(string filePath) /// The text to be used as a member value. Null if the member has no value or value cannot be determined. public string GetMemberValueText(MemberInfo memberInfo) { + var temp = memberInfo.Name; if (memberInfo.DeclaringType == null) return null; try { object instance = memberInfo.IsStatic() ? null : ActivatorUtils.CreateInstanceAutoFillGenericParameters(memberInfo.DeclaringType); + if (instance != null) + { + memberInfo = instance.GetType().GetMember(memberInfo.Name).First(); // Properties and fields can't overload, right? + } var valueObj = new object(); object valueObjGuard = valueObj; bool isConstant = false; - - switch (memberInfo) - { - case FieldInfo fieldInfo: - valueObj = fieldInfo.GetValue(instance); - isConstant = fieldInfo.IsStatic && fieldInfo.IsLiteral && !fieldInfo.IsInitOnly; - break; - case PropertyInfo propertyInfo: - valueObj = propertyInfo.GetValue(instance); - break; - } + + GetMemberValue(memberInfo, instance, out valueObj, out isConstant); // if only default values for constants are allowed if (GeneratorOptions.CsDefaultValuesForConstantsOnly && !isConstant) return null; @@ -341,6 +338,7 @@ public string GetMemberValueText(MemberInfo memberInfo) // if valueObj's value is the default value for its type if (valueObj == null || valueObj.Equals(TypeUtils.GetDefaultValue(valueObj.GetType()))) return null; + var valueType = valueObj.GetType(); string memberType = _typeService.GetTsTypeName(memberInfo).GetTsTypeUnion(0); string quote = GeneratorOptions.SingleQuotes ? "'" : "\""; @@ -358,7 +356,18 @@ public string GetMemberValueText(MemberInfo memberInfo) case DateTimeOffset valueDateTimeOffset when memberType == "string": return quote + valueDateTimeOffset.ToString("o", CultureInfo.InvariantCulture) + quote; default: - return JsonConvert.SerializeObject(valueObj, _jsonSerializerSettings).Replace("\"", quote); + var serializedValue = JsonConvert.SerializeObject(valueObj, _jsonSerializerSettings).Replace("\"", quote); + if (serializedValue.StartsWith("{") && // Make sure it's not a list, array, or other special type + !valueType.GetTypeInfo().IsValueType && // Ignore value types + valueType.GetConstructor(Type.EmptyTypes) != null) // Make sure the type has a default constructor to use for this + { + var defaultCtorValueType = Activator.CreateInstance(valueType); + if (defaultCtorValueType != null && memberwiseEquals(valueObj, defaultCtorValueType)) + { + return $@"new {_typeService.GetTsTypeName(memberInfo)}()"; + } + } + return serializedValue; } } catch (MissingMethodException e) @@ -376,5 +385,42 @@ public string GetMemberValueText(MemberInfo memberInfo) return null; } + + private bool memberwiseEquals(object a, object b) + { + if (a == b || a.Equals(b)) return true; + if (a.GetType() != b.GetType()) return false; + var type = a.GetType(); + if (type.GetTypeInfo().IsValueType) + { + return false; + } + foreach (var member in type.GetTsExportableMembers(this._metadataReaderFactory.GetInstance())) + { + if (member is PropertyInfo p && p.GetIndexParameters().Length > 0) continue; + GetMemberValue(member, a, out var aVal, out var _); + GetMemberValue(member, b, out var bVal, out var _); + if (!memberwiseEquals(aVal, bVal)) return false; + } + return true; + } + + private void GetMemberValue(MemberInfo memberInfo, object instance, out object valueObj, out bool isConstant) + { + switch (memberInfo) + { + case FieldInfo fieldInfo: + valueObj = fieldInfo.GetValue(instance); + isConstant = fieldInfo.IsStatic && fieldInfo.IsLiteral && !fieldInfo.IsInitOnly; + break; + case PropertyInfo propertyInfo: + valueObj = propertyInfo.GetValue(instance); + isConstant = false; + break; + default: + throw new Exception(); + } + + } } } diff --git a/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/DefaultMemberValues.cs b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/DefaultMemberValues.cs index a8c3c1c6..9298e682 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/DefaultMemberValues.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/DefaultMemberValues.cs @@ -19,6 +19,8 @@ public class DefaultMemberValues public DateTime fieldDateTimeUnassigned; - public DefaultMemberComplexValues PropertyComplex { get; set; } = new(); + public DefaultMemberComplexValues PropertyComplexDefaultValue { get; set; } = new(); + + public DefaultMemberComplexValues PropertyComplexNotDefaultValue { get; set; } = new() { Number = 4 }; } } \ No newline at end of file diff --git a/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/default-member-values.ts b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/default-member-values.ts index 93085dfd..60199951 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/default-member-values.ts +++ b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/default-member-values.ts @@ -13,5 +13,6 @@ export class DefaultMemberValues { static staticFieldNumber: number = 2; propertyNumber: number = 3; static staticPropertyString: string = "StaticPropertyString"; - propertyComplex: DefaultMemberComplexValues = {"number":0,"numberNull":null,"string":"default","stringNull":null}; + propertyComplexDefaultValue: DefaultMemberComplexValues = new DefaultMemberComplexValues(); + propertyComplexNotDefaultValue: DefaultMemberComplexValues = {"number":4,"numberNull":null,"string":"default","stringNull":null}; } From 8f3a869ae4485951c6b1d94b27220394da11cba2 Mon Sep 17 00:00:00 2001 From: tterrag1098 Date: Thu, 27 Jul 2023 18:06:11 -0400 Subject: [PATCH 2/6] Fix broken handling of dictionary types --- .../TypeGen.Core/Generator/Services/TsContentGenerator.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs index 0cfc1c53..a48c1ac4 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs @@ -357,7 +357,9 @@ public string GetMemberValueText(MemberInfo memberInfo) return quote + valueDateTimeOffset.ToString("o", CultureInfo.InvariantCulture) + quote; default: var serializedValue = JsonConvert.SerializeObject(valueObj, _jsonSerializerSettings).Replace("\"", quote); - if (serializedValue.StartsWith("{") && // Make sure it's not a list, array, or other special type + if (!_typeService.IsCollectionType(valueType) && + !_typeService.IsDictionaryType(valueType) && + _typeService.IsTsClass(valueType) && // Make sure it's not a list, array, or other special type !valueType.GetTypeInfo().IsValueType && // Ignore value types valueType.GetConstructor(Type.EmptyTypes) != null) // Make sure the type has a default constructor to use for this { From 7c703fdbb71ab3c87530bd8fb67e85fabce2811c Mon Sep 17 00:00:00 2001 From: tterrag1098 Date: Mon, 31 Jul 2023 14:02:27 -0400 Subject: [PATCH 3/6] Add support for generating constructors --- .../TypeGen.Core/Generator/Generator.cs | 12 ++- .../Generator/Services/ITemplateService.cs | 6 +- .../Generator/Services/ITsContentGenerator.cs | 1 + .../Generator/Services/TemplateService.cs | 25 +++++- .../Generator/Services/TsContentGenerator.cs | 87 +++++++++++++++++-- .../Services/TypeDependencyService.cs | 26 ++++++ src/TypeGen/TypeGen.Core/Templates/Class.tpl | 2 +- .../Templates/ClassDefaultExport.tpl | 2 +- .../TypeGen.Core/Templates/Constructor.tpl | 4 + .../Templates/ConstructorAssignment.tpl | 1 + .../TypeAnnotations/TsConstructorAttribute.cs | 12 +++ src/TypeGen/TypeGen.Core/TypeGen.Core.csproj | 4 + .../CircularGenericConstraintTest.cs | 3 + .../CommonCases/CommonCasesGenerationTest.cs | 5 ++ .../Entities/ConstructorChildClass.cs | 16 ++++ .../CommonCases/Entities/ConstructorClass.cs | 25 ++++++ .../Expected/constructor-child-class.ts | 14 +++ .../CommonCases/Expected/constructor-class.ts | 17 ++++ .../ConstantsOnly/ConstantsOnlyTest.cs | 3 + .../CustomBaseInterfacesTest.cs | 3 + .../CustomMappingsClassGeneration.cs | 3 + .../DefaultExport/DefaultExportTest.cs | 3 + .../ExportTypesAsInterfacesByDefaultTest.cs | 3 + .../GenerationSpecsForStructsTest.cs | 3 + .../IgnoreBaseInterfacesTest.cs | 3 + .../NullableTranslationTest.cs | 3 + .../StructImplementsInterfacesTest.cs | 3 + .../TestingUtils/GenerationTestBase.cs | 31 ++++++- .../TypeGen.IntegrationTest.csproj | 4 + ...ultExportBreaksInterfaceInheritanceTest.cs | 3 + 30 files changed, 309 insertions(+), 18 deletions(-) create mode 100644 src/TypeGen/TypeGen.Core/Templates/Constructor.tpl create mode 100644 src/TypeGen/TypeGen.Core/Templates/ConstructorAssignment.tpl create mode 100644 src/TypeGen/TypeGen.Core/TypeAnnotations/TsConstructorAttribute.cs create mode 100644 src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/ConstructorChildClass.cs create mode 100644 src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/ConstructorClass.cs create mode 100644 src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/constructor-child-class.ts create mode 100644 src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/constructor-class.ts diff --git a/src/TypeGen/TypeGen.Core/Generator/Generator.cs b/src/TypeGen/TypeGen.Core/Generator/Generator.cs index 164b67a3..1349f3c2 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Generator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Generator.cs @@ -447,7 +447,7 @@ private IEnumerable GenerateClassOrInterface(Type type, ExportTsClassAtt string importsText = _tsContentGenerator.GetImportsText(type, outputDir); string propertiesText = classAttribute != null ? GetClassPropertiesText(type) : GetInterfacePropertiesText(type); - + string constructorText = _tsContentGenerator.GetConstructorText(type); // generate the file content string tsTypeName = _typeService.GetTsTypeName(type, true); @@ -462,8 +462,8 @@ private IEnumerable GenerateClassOrInterface(Type type, ExportTsClassAtt if (classAttribute != null) { content = _typeService.UseDefaultExport(type) ? - _templateService.FillClassDefaultExportTemplate(importsText, tsTypeName, tsTypeNameFirstPart, extendsText, implementsText, propertiesText, customHead, customBody, Options.FileHeading) : - _templateService.FillClassTemplate(importsText, tsTypeName, extendsText, implementsText, propertiesText, customHead, customBody, Options.FileHeading); + _templateService.FillClassDefaultExportTemplate(importsText, tsTypeName, tsTypeNameFirstPart, extendsText, implementsText, propertiesText, constructorText, customHead, customBody, Options.FileHeading) : + _templateService.FillClassTemplate(importsText, tsTypeName, extendsText, implementsText, propertiesText, constructorText, customHead, customBody, Options.FileHeading); } else { @@ -477,7 +477,7 @@ private IEnumerable GenerateClassOrInterface(Type type, ExportTsClassAtt FileContentGenerated?.Invoke(this, new FileContentGeneratedArgs(type, filePath, content)); return new[] { filePathRelative }.Concat(dependenciesGenerationResult).ToList(); } - + private static List GetNotNullOrEmptyImplementedInterfaceNames(TsCustomBaseAttribute tsCustomBaseAttribute) => tsCustomBaseAttribute.ImplementedInterfaces .Select(x => x.Name) @@ -550,6 +550,10 @@ private string GetClassPropertyText(MemberInfo memberInfo) isOptional = true; } + var ctorAttribute = _metadataReaderFactory.GetInstance().GetAttribute(memberInfo); + if (ctorAttribute != null) + return _templateService.FillClassPropertyTemplate(modifiers, name, typeName, typeUnions, isOptional); + // try to get default value from TsDefaultValueAttribute var defaultValueAttribute = _metadataReaderFactory.GetInstance().GetAttribute(memberInfo); if (defaultValueAttribute != null) diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/ITemplateService.cs b/src/TypeGen/TypeGen.Core/Generator/Services/ITemplateService.cs index 12da64a8..08f2d15c 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/ITemplateService.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/ITemplateService.cs @@ -4,8 +4,10 @@ namespace TypeGen.Core.Generator.Services { internal interface ITemplateService { - string FillClassTemplate(string imports, string name, string extends, string implements, string properties, string customHead, string customBody, string fileHeading = null); - string FillClassDefaultExportTemplate(string imports, string name, string exportName, string extends, string implements, string properties, string customHead, string customBody, string fileHeading = null); + string FillClassTemplate(string imports, string name, string extends, string implements, string properties, string constructor, string customHead, string customBody, string fileHeading = null); + string FillClassDefaultExportTemplate(string imports, string name, string exportName, string extends, string implements, string properties, string constructor, string customHead, string customBody, string fileHeading = null); + string FillConstructorTemplate(string type, string parameters, string superCall, string body); + string FillConstructorAssignmentTemplate(string name); string FillClassPropertyTemplate(string modifiers, string name, string type, IEnumerable typeUnions, bool isOptional, string defaultValue = null); string FillInterfaceTemplate(string imports, string name, string extends, string properties, string customHead, string customBody, string fileHeading = null); string FillInterfaceDefaultExportTemplate(string imports, string name, string exportName, string extends, string properties, string customHead, string customBody, string fileHeading = null); diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs b/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs index 94bfe60d..689a2b58 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs @@ -46,5 +46,6 @@ internal interface ITsContentGenerator string GetMemberValueText(MemberInfo memberInfo); string GetImplementsText(Type type); string GetExtendsForInterfacesText(Type type); + string GetConstructorText(Type type); } } \ No newline at end of file diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/TemplateService.cs b/src/TypeGen/TypeGen.Core/Generator/Services/TemplateService.cs index 67df7446..5eb4600a 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/TemplateService.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/TemplateService.cs @@ -24,6 +24,8 @@ internal class TemplateService : ITemplateService private readonly string _enumUnionTypeValueTemplate; private readonly string _classTemplate; private readonly string _classDefaultExportTemplate; + private readonly string _constructorTemplate; + private readonly string _constructorAssignmentTemplate; private readonly string _classPropertyTemplate; private readonly string _interfaceTemplate; private readonly string _interfaceDefaultExportTemplate; @@ -49,6 +51,8 @@ public TemplateService(IInternalStorage internalStorage, IGeneratorOptionsProvid _enumUnionTypeValueTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.EnumUnionTypeValue.tpl"); _classTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.Class.tpl"); _classDefaultExportTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.ClassDefaultExport.tpl"); + _constructorTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.Constructor.tpl"); + _constructorAssignmentTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.ConstructorAssignment.tpl"); _classPropertyTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.ClassProperty.tpl"); _interfaceTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.Interface.tpl"); _interfaceDefaultExportTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.InterfaceDefaultExport.tpl"); @@ -60,7 +64,7 @@ public TemplateService(IInternalStorage internalStorage, IGeneratorOptionsProvid _headingTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.Heading.tpl"); } - public string FillClassTemplate(string imports, string name, string extends, string implements, string properties, string customHead, string customBody, string fileHeading = null) + public string FillClassTemplate(string imports, string name, string extends, string implements, string properties, string constructor, string customHead, string customBody, string fileHeading = null) { if (fileHeading == null) fileHeading = _headingTemplate; @@ -70,12 +74,13 @@ public string FillClassTemplate(string imports, string name, string extends, str .Replace(GetTag("extends"), extends) .Replace(GetTag("implements"), implements) .Replace(GetTag("properties"), properties) + .Replace(GetTag("constructor"), constructor) .Replace(GetTag("customHead"), customHead) .Replace(GetTag("customBody"), customBody) .Replace(GetTag("fileHeading"), fileHeading); } - public string FillClassDefaultExportTemplate(string imports, string name, string exportName, string extends, string implements, string properties, string customHead, string customBody, string fileHeading = null) + public string FillClassDefaultExportTemplate(string imports, string name, string exportName, string extends, string implements, string properties, string constructor, string customHead, string customBody, string fileHeading = null) { if (fileHeading == null) fileHeading = _headingTemplate; @@ -86,11 +91,27 @@ public string FillClassDefaultExportTemplate(string imports, string name, string .Replace(GetTag("extends"), extends) .Replace(GetTag("implements"), implements) .Replace(GetTag("properties"), properties) + .Replace(GetTag("constructor"), constructor) .Replace(GetTag("customHead"), customHead) .Replace(GetTag("customBody"), customBody) .Replace(GetTag("fileHeading"), fileHeading); } + public string FillConstructorTemplate(string type, string parameters, string superCall, string body) + { + return ReplaceSpecialChars(_constructorTemplate) + .Replace(GetTag("type"), type) + .Replace(GetTag("parameters"), parameters) + .Replace(GetTag("super"), superCall) + .Replace(GetTag("body"), body); + } + + public string FillConstructorAssignmentTemplate(string name) + { + return ReplaceSpecialChars(_constructorAssignmentTemplate) + .Replace(GetTag("name"), name); + } + public string FillClassPropertyTemplate(string modifiers, string name, string type, IEnumerable typeUnions, bool isOptional, string defaultValue = null) { type = $": {type}"; diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs index a48c1ac4..c8e1290f 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs @@ -5,6 +5,8 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Text; +using System.Xml.Linq; using Newtonsoft.Json; using TypeGen.Core.Extensions; using TypeGen.Core.Logging; @@ -363,6 +365,7 @@ public string GetMemberValueText(MemberInfo memberInfo) !valueType.GetTypeInfo().IsValueType && // Ignore value types valueType.GetConstructor(Type.EmptyTypes) != null) // Make sure the type has a default constructor to use for this { + _logger?.Log($"Checking type {valueType.FullName} for constructor usage", LogLevel.Info); var defaultCtorValueType = Activator.CreateInstance(valueType); if (defaultCtorValueType != null && memberwiseEquals(valueObj, defaultCtorValueType)) { @@ -397,12 +400,20 @@ private bool memberwiseEquals(object a, object b) { return false; } - foreach (var member in type.GetTsExportableMembers(this._metadataReaderFactory.GetInstance())) + while (type != null) { - if (member is PropertyInfo p && p.GetIndexParameters().Length > 0) continue; - GetMemberValue(member, a, out var aVal, out var _); - GetMemberValue(member, b, out var bVal, out var _); - if (!memberwiseEquals(aVal, bVal)) return false; + foreach (var member in type.GetTsExportableMembers(this._metadataReaderFactory.GetInstance())) + { + if (member is PropertyInfo p && p.GetIndexParameters().Length > 0) continue; + GetMemberValue(member, a, out var aVal, out var _); + GetMemberValue(member, b, out var bVal, out var _); + if (!memberwiseEquals(aVal, bVal)) + { + _logger?.Log($"Difference found in member {type.FullName}.{member.Name}: {aVal} != {bVal}", LogLevel.Info); + return false; + } + } + type = type.GetTypeInfo().BaseType; } return true; } @@ -424,5 +435,71 @@ private void GetMemberValue(MemberInfo memberInfo, object instance, out object v } } + + public string GetConstructorText(Type type) + { + var reader = _metadataReaderFactory.GetInstance(); + var ctorParams = GetHierarchyConstructorParams(type); + if (ctorParams.Count == 0 || ctorParams.All(l => l.Count == 0)) + { + return ""; + } + ctorParams.Reverse(); + var paramsText = string.Join(", ", ctorParams.SelectMany(_ => _).Select(m => + { + string defaultValue; + var typeName = _typeService.GetTsTypeName(m); + // try to get default value from TsDefaultValueAttribute + var defaultValueAttribute = _metadataReaderFactory.GetInstance().GetAttribute(m); + if (defaultValueAttribute != null) + { + defaultValue = defaultValueAttribute.DefaultValue; + } + else + { + // try to get default value from the member's default value + string valueText = GetMemberValueText(m); + if (!string.IsNullOrWhiteSpace(valueText)) + defaultValue = valueText; + // try to get default value from Options.DefaultValuesForTypes + else if (_generatorOptionsProvider.GeneratorOptions.DefaultValuesForTypes.Any() && _generatorOptionsProvider.GeneratorOptions.DefaultValuesForTypes.ContainsKey(typeName)) + defaultValue = _generatorOptionsProvider.GeneratorOptions.DefaultValuesForTypes[typeName]; + else + defaultValue = null; + } + var ret = $"{GetTsMemberName(m)}: {typeName}"; + if (!string.IsNullOrWhiteSpace(defaultValue)) + { + ret += $" = {defaultValue}"; + } + return ret; + })); + string tab = GeneratorOptions.UseTabCharacter ? "\t" : StringUtils.GetTabText(GeneratorOptions.TabLength); + var newParams = ctorParams.Last(); + var superParams = new List>(ctorParams); + superParams.RemoveAt(superParams.Count - 1); + var superText = superParams.Count == 0 || superParams.All(l => l.Count == 0) ? "" : $"{tab}{tab}super({string.Join(", ", superParams.SelectMany(_ => _).Select(GetTsMemberName))});\r\n"; + var bodyText = newParams.Aggregate("", (acc, m) => acc + _templateService.FillConstructorAssignmentTemplate(GetTsMemberName(m))); + return _templateService.FillConstructorTemplate(_typeService.GetTsTypeName(type), paramsText, superText, bodyText); + } + + private List> GetHierarchyConstructorParams(Type type) + { + var reader = _metadataReaderFactory.GetInstance(); + var ret = new List>(); + while (type != null) + { + var parameters = type.GetTsExportableMembers(reader).Where(m => reader.GetAttribute(m) != null).ToList(); + ret.Add(parameters); + type = type.GetTypeInfo().BaseType; + } + return ret; + } + + private string GetTsMemberName(MemberInfo memberInfo) + { + var nameAttribute = _metadataReaderFactory.GetInstance().GetAttribute(memberInfo); + return nameAttribute?.Name ?? _generatorOptionsProvider.GeneratorOptions.PropertyNameConverters.Convert(memberInfo.Name, memberInfo); + } } } diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/TypeDependencyService.cs b/src/TypeGen/TypeGen.Core/Generator/Services/TypeDependencyService.cs index f1dacb74..b7e5bae5 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/TypeDependencyService.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/TypeDependencyService.cs @@ -53,6 +53,7 @@ public IEnumerable GetTypeDependencies(Type type) .Concat(GetBaseTypeDependency(type)) .Concat(GetImplementedInterfaceTypesDependencies(type)) .Concat(GetMemberTypeDependencies(type)) + .Concat(GetSuperConstructorTypeDependencies(type)) .Distinct(new TypeDependencyInfoTypeComparer()) .Where(t => t.Type != type) .ToList(); @@ -149,6 +150,31 @@ private IEnumerable GetMemberTypeDependencies(Type type) return result; } + private IEnumerable GetSuperConstructorTypeDependencies(Type type) + { + var baseClass = type.GetTypeInfo().BaseType; + var result = new List(); + while (baseClass != null) + { + IEnumerable memberInfos = baseClass.GetTsExportableMembers(_metadataReaderFactory.GetInstance()).Where(m => _metadataReaderFactory.GetInstance().GetAttribute(m) != null); + + foreach (var memberInfo in memberInfos) + { + Type memberType = _typeService.GetMemberType(memberInfo); + Type memberFlatType = _typeService.GetFlatType(memberType); + + if (memberFlatType == type || (memberFlatType.IsConstructedGenericType && memberFlatType.GetGenericTypeDefinition() == type)) continue; // NOT a dependency if it's the type itself + if (GeneratorOptions.CustomTypeMappings.ContainsKey(memberFlatType.FullName ?? "")) continue; // NOT a dependency if specified in custom type mappings + + IEnumerable memberAttributes = _metadataReaderFactory.GetInstance().GetAttributes(memberInfo); + result.AddRange(GetFlatTypeDependencies(memberFlatType, memberAttributes)); + } + + baseClass = baseClass.BaseType; + } + return result; + } + private IEnumerable GetFlatTypeDependencies(Type flatType, IEnumerable memberAttributes = null, bool isBase = false) { if (_typeService.IsTsBuiltInType(flatType) || flatType.IsGenericParameter) return Enumerable.Empty(); diff --git a/src/TypeGen/TypeGen.Core/Templates/Class.tpl b/src/TypeGen/TypeGen.Core/Templates/Class.tpl index afd4fe1d..8eb615ac 100644 --- a/src/TypeGen/TypeGen.Core/Templates/Class.tpl +++ b/src/TypeGen/TypeGen.Core/Templates/Class.tpl @@ -1,3 +1,3 @@ $tg{fileHeading}$tg{imports}$tg{customHead}export class $tg{name}$tg{extends}$tg{implements} { -$tg{properties}$tg{customBody} +$tg{properties}$tg{constructor}$tg{customBody} } diff --git a/src/TypeGen/TypeGen.Core/Templates/ClassDefaultExport.tpl b/src/TypeGen/TypeGen.Core/Templates/ClassDefaultExport.tpl index ae3bdcde..2f796b19 100644 --- a/src/TypeGen/TypeGen.Core/Templates/ClassDefaultExport.tpl +++ b/src/TypeGen/TypeGen.Core/Templates/ClassDefaultExport.tpl @@ -1,4 +1,4 @@ $tg{fileHeading}$tg{imports}$tg{customHead}class $tg{name}$tg{extends}$tg{implements} { -$tg{properties}$tg{customBody} +$tg{properties}$tg{constructor}$tg{customBody} } export default $tg{exportName}; diff --git a/src/TypeGen/TypeGen.Core/Templates/Constructor.tpl b/src/TypeGen/TypeGen.Core/Templates/Constructor.tpl new file mode 100644 index 00000000..f3b55fff --- /dev/null +++ b/src/TypeGen/TypeGen.Core/Templates/Constructor.tpl @@ -0,0 +1,4 @@ + + +$tg{tab}constructor($tg{parameters}) { +$tg{super}$tg{body}$tg{tab}} \ No newline at end of file diff --git a/src/TypeGen/TypeGen.Core/Templates/ConstructorAssignment.tpl b/src/TypeGen/TypeGen.Core/Templates/ConstructorAssignment.tpl new file mode 100644 index 00000000..d4ecb948 --- /dev/null +++ b/src/TypeGen/TypeGen.Core/Templates/ConstructorAssignment.tpl @@ -0,0 +1 @@ +$tg{tab}$tg{tab}this.$tg{name} = $tg{name}; diff --git a/src/TypeGen/TypeGen.Core/TypeAnnotations/TsConstructorAttribute.cs b/src/TypeGen/TypeGen.Core/TypeAnnotations/TsConstructorAttribute.cs new file mode 100644 index 00000000..4199cccb --- /dev/null +++ b/src/TypeGen/TypeGen.Core/TypeAnnotations/TsConstructorAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace TypeGen.Core.TypeAnnotations +{ + /// + /// Identifies a property that should be required as a constructor parameter when generating a TypeScript file + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class TsConstructorAttribute : Attribute + { + } +} diff --git a/src/TypeGen/TypeGen.Core/TypeGen.Core.csproj b/src/TypeGen/TypeGen.Core/TypeGen.Core.csproj index be93202d..8d27dcd8 100644 --- a/src/TypeGen/TypeGen.Core/TypeGen.Core.csproj +++ b/src/TypeGen/TypeGen.Core/TypeGen.Core.csproj @@ -9,6 +9,8 @@ + + @@ -23,6 +25,8 @@ + + diff --git a/src/TypeGen/TypeGen.IntegrationTest/CircularGenericConstraint/CircularGenericConstraintTest.cs b/src/TypeGen/TypeGen.IntegrationTest/CircularGenericConstraint/CircularGenericConstraintTest.cs index fbc95a28..6f611fca 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/CircularGenericConstraint/CircularGenericConstraintTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/CircularGenericConstraint/CircularGenericConstraintTest.cs @@ -4,11 +4,14 @@ using TypeGen.IntegrationTest.CommonCases; using TypeGen.IntegrationTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.CircularGenericConstraint { public class CircularGenericConstraintTest : GenerationTestBase { + public CircularGenericConstraintTest(ITestOutputHelper output) : base(output) { } + /// /// Looks into generating classes and interfaces with circular type constraints /// diff --git a/src/TypeGen/TypeGen.IntegrationTest/CommonCases/CommonCasesGenerationTest.cs b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/CommonCasesGenerationTest.cs index 9bbfff65..9808ca57 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/CommonCases/CommonCasesGenerationTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/CommonCasesGenerationTest.cs @@ -23,11 +23,14 @@ using StrictNullsClass = TypeGen.IntegrationTest.CommonCases.Entities.StrictNullsClass; using TestInterface = TypeGen.IntegrationTest.CommonCases.Entities.TestInterface; using TypeUnions = TypeGen.IntegrationTest.CommonCases.Entities.TypeUnions; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.CommonCases { public class CommonCasesGenerationTest : GenerationTestBase { + public CommonCasesGenerationTest(ITestOutputHelper output) : base(output) {} + /// /// Tests if types are correctly translated to TypeScript. /// The tested types contain all major use cases that should be supported. @@ -42,6 +45,8 @@ public class CommonCasesGenerationTest : GenerationTestBase [InlineData(typeof(Entities.BaseClass<>), "TypeGen.IntegrationTest.CommonCases.Expected.base-class.ts")] [InlineData(typeof(BaseClass2<>), "TypeGen.IntegrationTest.CommonCases.Expected.base-class2.ts")] [InlineData(typeof(C), "TypeGen.IntegrationTest.CommonCases.Expected.c.ts")] + [InlineData(typeof(ConstructorClass), "TypeGen.IntegrationTest.CommonCases.Expected.constructor-class.ts")] + [InlineData(typeof(ConstructorChildClass), "TypeGen.IntegrationTest.CommonCases.Expected.constructor-child-class.ts")] [InlineData(typeof(CustomBaseClass), "TypeGen.IntegrationTest.CommonCases.Expected.custom-base-class.ts")] [InlineData(typeof(CustomEmptyBaseClass), "TypeGen.IntegrationTest.CommonCases.Expected.custom-empty-base-class.ts")] [InlineData(typeof(D), "TypeGen.IntegrationTest.CommonCases.Expected.d.ts")] diff --git a/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/ConstructorChildClass.cs b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/ConstructorChildClass.cs new file mode 100644 index 00000000..bfe22549 --- /dev/null +++ b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/ConstructorChildClass.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TypeGen.Core.TypeAnnotations; + +namespace TypeGen.IntegrationTest.CommonCases.Entities +{ + [ExportTsClass] + public class ConstructorChildClass : ConstructorClass + { + [TsConstructor] + public string ChildConstructorArg { get; set; } = "testchild"; + } +} diff --git a/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/ConstructorClass.cs b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/ConstructorClass.cs new file mode 100644 index 00000000..8d63f9ab --- /dev/null +++ b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Entities/ConstructorClass.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TypeGen.Core.TypeAnnotations; + +namespace TypeGen.IntegrationTest.CommonCases.Entities +{ + [ExportTsClass] + public class ConstructorClass + { + [TsConstructor] + public int ConstructorArgWithDefault { get; set; } = 4; + + [TsConstructor] + [TsDefaultValue("5")] + public int ConstructorArgWithAttributeDefault { get; set; } + + [TsConstructor] + public int ConstructorArgWithNoDefault { get; set; } + + public string NonConstructorArg { get; set; } = "test"; + } +} diff --git a/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/constructor-child-class.ts b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/constructor-child-class.ts new file mode 100644 index 00000000..2de592d3 --- /dev/null +++ b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/constructor-child-class.ts @@ -0,0 +1,14 @@ +/** + * This is a TypeGen auto-generated file. + * Any changes made to this file can be lost when this file is regenerated. + */ +import { ConstructorClass } from "./constructor-class"; + +export class ConstructorChildClass extends ConstructorClass { + childConstructorArg: string; + + constructor(constructorArgWithDefault: number = 4, constructorArgWithAttributeDefault: number = 5, constructorArgWithNoDefault: number, childConstructorArg: string = "testchild") { + super(constructorArgWithDefault, constructorArgWithAttributeDefault, constructorArgWithNoDefault); + this.childConstructorArg = childConstructorArg; + } +} \ No newline at end of file diff --git a/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/constructor-class.ts b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/constructor-class.ts new file mode 100644 index 00000000..601a6f8f --- /dev/null +++ b/src/TypeGen/TypeGen.IntegrationTest/CommonCases/Expected/constructor-class.ts @@ -0,0 +1,17 @@ +/** + * This is a TypeGen auto-generated file. + * Any changes made to this file can be lost when this file is regenerated. + */ + +export class ConstructorClass { + constructorArgWithDefault: number; + constructorArgWithAttributeDefault: number; + constructorArgWithNoDefault: number; + nonConstructorArg: string = "test"; + + constructor(constructorArgWithDefault: number = 4, constructorArgWithAttributeDefault: number = 5, constructorArgWithNoDefault: number) { + this.constructorArgWithDefault = constructorArgWithDefault; + this.constructorArgWithAttributeDefault = constructorArgWithAttributeDefault; + this.constructorArgWithNoDefault = constructorArgWithNoDefault; + } +} diff --git a/src/TypeGen/TypeGen.IntegrationTest/ConstantsOnly/ConstantsOnlyTest.cs b/src/TypeGen/TypeGen.IntegrationTest/ConstantsOnly/ConstantsOnlyTest.cs index 1628e900..c0518c69 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/ConstantsOnly/ConstantsOnlyTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/ConstantsOnly/ConstantsOnlyTest.cs @@ -4,11 +4,14 @@ using TypeGen.Core.SpecGeneration; using TypeGen.IntegrationTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.ConstantsOnly; public class ConstantsOnlyTest : GenerationTestBase { + public ConstantsOnlyTest(ITestOutputHelper output) : base(output) { } + [Theory] [InlineData(typeof(Entities.ConstantsOnly), "TypeGen.IntegrationTest.ConstantsOnly.Expected.constants-only.ts")] public async Task TestConstantsOnlyGenerationSpec(Type type, string expectedLocation) diff --git a/src/TypeGen/TypeGen.IntegrationTest/CustomBaseInterfaces/CustomBaseInterfacesTest.cs b/src/TypeGen/TypeGen.IntegrationTest/CustomBaseInterfaces/CustomBaseInterfacesTest.cs index e3099449..8f346a02 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/CustomBaseInterfaces/CustomBaseInterfacesTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/CustomBaseInterfaces/CustomBaseInterfacesTest.cs @@ -6,11 +6,14 @@ using TypeGen.IntegrationTest.CustomBaseInterfaces.Entities; using TypeGen.IntegrationTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.CustomBaseInterfaces; public class CustomBaseInterfacesTest : GenerationTestBase { + public CustomBaseInterfacesTest(ITestOutputHelper output) : base(output) {} + [Theory] [InlineData(typeof(Foo), "TypeGen.IntegrationTest.CustomBaseInterfaces.Expected.foo.ts")] public async Task TestCustomBaseInterfacesGenerationSpec(Type type, string expectedLocation) diff --git a/src/TypeGen/TypeGen.IntegrationTest/CustomMappingsClassGeneration/CustomMappingsClassGeneration.cs b/src/TypeGen/TypeGen.IntegrationTest/CustomMappingsClassGeneration/CustomMappingsClassGeneration.cs index 7bb211c8..9bee6769 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/CustomMappingsClassGeneration/CustomMappingsClassGeneration.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/CustomMappingsClassGeneration/CustomMappingsClassGeneration.cs @@ -7,11 +7,14 @@ using TypeGen.IntegrationTest.Extensions; using TypeGen.IntegrationTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.CustomMappingsClassGeneration { public class CustomMappingsClassGeneration : GenerationTestBase { + public CustomMappingsClassGeneration(ITestOutputHelper output) : base(output) { } + [Fact] public async Task GeneratesCorrectly() { diff --git a/src/TypeGen/TypeGen.IntegrationTest/DefaultExport/DefaultExportTest.cs b/src/TypeGen/TypeGen.IntegrationTest/DefaultExport/DefaultExportTest.cs index 283a4217..0f1ec4d5 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/DefaultExport/DefaultExportTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/DefaultExport/DefaultExportTest.cs @@ -5,11 +5,14 @@ using TypeGen.IntegrationTest.DefaultExport.Entities; using TypeGen.IntegrationTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.DefaultExport { public class DefaultExportTest : GenerationTestBase { + public DefaultExportTest(ITestOutputHelper output) : base(output) { } + /// /// Looks into generating classes and interfaces with circular type constraints /// diff --git a/src/TypeGen/TypeGen.IntegrationTest/ExportTypesAsInterfacesByDefault/ExportTypesAsInterfacesByDefaultTest.cs b/src/TypeGen/TypeGen.IntegrationTest/ExportTypesAsInterfacesByDefault/ExportTypesAsInterfacesByDefaultTest.cs index d007f8f4..97c5663a 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/ExportTypesAsInterfacesByDefault/ExportTypesAsInterfacesByDefaultTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/ExportTypesAsInterfacesByDefault/ExportTypesAsInterfacesByDefaultTest.cs @@ -4,11 +4,14 @@ using TypeGen.IntegrationTest.ExportTypesAsInterfacesByDefault.Entities; using TypeGen.IntegrationTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.ExportTypesAsInterfacesByDefault; public class ExportTypesAsInterfacesByDefaultTest : GenerationTestBase { + public ExportTypesAsInterfacesByDefaultTest(ITestOutputHelper output) : base(output) { } + [Fact] public async Task if_option_not_set_should_generate_class() { diff --git a/src/TypeGen/TypeGen.IntegrationTest/GenerationSpecForStructs/GenerationSpecsForStructsTest.cs b/src/TypeGen/TypeGen.IntegrationTest/GenerationSpecForStructs/GenerationSpecsForStructsTest.cs index 7cb46e9b..741cba56 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/GenerationSpecForStructs/GenerationSpecsForStructsTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/GenerationSpecForStructs/GenerationSpecsForStructsTest.cs @@ -5,11 +5,14 @@ using TypeGen.IntegrationTest.CommonCases.Entities.Structs; using TypeGen.IntegrationTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.GenerationSpecForStructs; public class GenerationSpecsForStructsTest : GenerationTestBase { + public GenerationSpecsForStructsTest(ITestOutputHelper output) : base(output) { } + [Theory] [InlineData(typeof(CustomBaseClass), "TypeGen.IntegrationTest.CommonCases.Expected.custom-base-class.ts")] [InlineData(typeof(CustomBaseCustomImport), "TypeGen.IntegrationTest.CommonCases.Expected.custom-base-custom-import.ts")] diff --git a/src/TypeGen/TypeGen.IntegrationTest/IgnoreBaseInterfaces/IgnoreBaseInterfacesTest.cs b/src/TypeGen/TypeGen.IntegrationTest/IgnoreBaseInterfaces/IgnoreBaseInterfacesTest.cs index 345c324d..14b80942 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/IgnoreBaseInterfaces/IgnoreBaseInterfacesTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/IgnoreBaseInterfaces/IgnoreBaseInterfacesTest.cs @@ -3,11 +3,14 @@ using TypeGen.IntegrationTest.IgnoreBaseInterfaces.Entities; using TypeGen.IntegrationTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.IgnoreBaseInterfaces; public class IgnoreBaseInterfacesTest : GenerationTestBase { + public IgnoreBaseInterfacesTest(ITestOutputHelper output) : base(output) { } + [Theory] [InlineData(typeof(Test), "TypeGen.IntegrationTest.IgnoreBaseInterfaces.Expected.test.ts")] public async Task TestIgnoreBaseInterfaces(Type type, string expectedLocation) diff --git a/src/TypeGen/TypeGen.IntegrationTest/NullableTranslation/NullableTranslationTest.cs b/src/TypeGen/TypeGen.IntegrationTest/NullableTranslation/NullableTranslationTest.cs index 7c18f3c9..a308bd2f 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/NullableTranslation/NullableTranslationTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/NullableTranslation/NullableTranslationTest.cs @@ -6,11 +6,14 @@ using TypeGen.IntegrationTest.NullableTranslation.Entities; using TypeGen.IntegrationTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.NullableTranslation; public class NullableTranslationTest : GenerationTestBase { + public NullableTranslationTest(ITestOutputHelper output) : base(output) { } + [Theory] [InlineData(typeof(NullableClass), "TypeGen.IntegrationTest.NullableTranslation.Expected.nullable-class.ts")] public async Task TestNullableTranslationGenerationSpec(Type type, string expectedLocation) diff --git a/src/TypeGen/TypeGen.IntegrationTest/StructImplementsInterfaces/StructImplementsInterfacesTest.cs b/src/TypeGen/TypeGen.IntegrationTest/StructImplementsInterfaces/StructImplementsInterfacesTest.cs index 7848f5e9..00abaf1a 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/StructImplementsInterfaces/StructImplementsInterfacesTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/StructImplementsInterfaces/StructImplementsInterfacesTest.cs @@ -9,11 +9,14 @@ using TypeGen.IntegrationTest.StructImplementsInterfaces.Entities; using TypeGen.IntegrationTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.StructImplementsInterfaces; public class StructImplementsInterfacesTest : GenerationTestBase { + public StructImplementsInterfacesTest(ITestOutputHelper output) : base(output) {} + [Theory] [InlineData(typeof(ImplementsInterfaces), "TypeGen.IntegrationTest.StructImplementsInterfaces.Expected.implements-interfaces.ts")] public async Task TestStructImplementsInterfacesFromGenerationSpec(Type type, string expectedLocation) diff --git a/src/TypeGen/TypeGen.IntegrationTest/TestingUtils/GenerationTestBase.cs b/src/TypeGen/TypeGen.IntegrationTest/TestingUtils/GenerationTestBase.cs index c7933e43..d15d4561 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/TestingUtils/GenerationTestBase.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/TestingUtils/GenerationTestBase.cs @@ -1,19 +1,44 @@ using System; using System.Threading.Tasks; using TypeGen.Core.Generator; +using TypeGen.Core.Logging; using TypeGen.Core.SpecGeneration; using TypeGen.IntegrationTest.Extensions; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.TestingUtils; public class GenerationTestBase { + private class TestLogger : ILogger + { + private readonly ITestOutputHelper output; + + public TestLogger(ITestOutputHelper output) + { + this.output = output; + } + + void ILogger.Log(string message, LogLevel level) + { + output.WriteLine(message); + } + } + + private readonly ITestOutputHelper output; + private readonly ILogger logger; + protected GenerationTestBase(ITestOutputHelper output) + { + this.output = output; + this.logger = new TestLogger(output); + } + protected async Task TestFromAssembly(Type type, string expectedLocation) { var readExpectedTask = EmbededResourceReader.GetEmbeddedResourceAsync(expectedLocation); - var generator = new Generator(); + var generator = new Generator(logger); var interceptor = GeneratorOutputInterceptor.CreateInterceptor(generator); await generator.GenerateAsync(type.Assembly); @@ -23,11 +48,11 @@ protected async Task TestFromAssembly(Type type, string expectedLocation) Assert.Equal(expected, interceptor.GeneratedOutputs[type].Content.FormatOutput()); } - protected static async Task TestGenerationSpec(Type type, string expectedLocation, + protected async Task TestGenerationSpec(Type type, string expectedLocation, GenerationSpec generationSpec, GeneratorOptions generatorOptions) { var readExpectedTask = EmbededResourceReader.GetEmbeddedResourceAsync(expectedLocation); - var generator = new Core.Generator.Generator(generatorOptions); + var generator = new Core.Generator.Generator(generatorOptions, logger); var interceptor = GeneratorOutputInterceptor.CreateInterceptor(generator); await generator.GenerateAsync(new[] { generationSpec }); diff --git a/src/TypeGen/TypeGen.IntegrationTest/TypeGen.IntegrationTest.csproj b/src/TypeGen/TypeGen.IntegrationTest/TypeGen.IntegrationTest.csproj index 26996ebd..518d0755 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/TypeGen.IntegrationTest.csproj +++ b/src/TypeGen/TypeGen.IntegrationTest/TypeGen.IntegrationTest.csproj @@ -9,6 +9,8 @@ + + @@ -30,6 +32,8 @@ + + diff --git a/src/TypeGen/TypeGen.IntegrationTest/UseDefaultExportBreaksInterfaceInheritance/UseDefaultExportBreaksInterfaceInheritanceTest.cs b/src/TypeGen/TypeGen.IntegrationTest/UseDefaultExportBreaksInterfaceInheritance/UseDefaultExportBreaksInterfaceInheritanceTest.cs index 3169682b..b35e432b 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/UseDefaultExportBreaksInterfaceInheritance/UseDefaultExportBreaksInterfaceInheritanceTest.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/UseDefaultExportBreaksInterfaceInheritance/UseDefaultExportBreaksInterfaceInheritanceTest.cs @@ -4,11 +4,14 @@ using TypeGen.IntegrationTest.TestingUtils; using TypeGen.IntegrationTest.UseDefaultExportBreaksInterfaceInheritance.Entities; using Xunit; +using Xunit.Abstractions; namespace TypeGen.IntegrationTest.UseDefaultExportBreaksInterfaceInheritance; public class UseDefaultExportBreaksInterfaceInheritanceTest : GenerationTestBase { + public UseDefaultExportBreaksInterfaceInheritanceTest(ITestOutputHelper output) : base(output) { } + [Theory] [InlineData(typeof(ProductDto), "TypeGen.IntegrationTest.UseDefaultExportBreaksInterfaceInheritance.Expected.product-dto.ts")] public async Task TestUseDefaultExportBreaksInterfaceInheritanceGenerationSpec(Type type, string expectedLocation) From 177f80ec5442b57c5fcc5fec2eb430f8200c8262 Mon Sep 17 00:00:00 2001 From: tterrag1098 Date: Wed, 13 Dec 2023 14:09:47 -0500 Subject: [PATCH 4/6] Improve default value handling, add better folder support --- src/TypeGen/TypeGen.Cli/AppConfig.cs | 2 +- .../Business/GeneratorOptionsProvider.cs | 3 +- src/TypeGen/TypeGen.Cli/Models/TgConfig.cs | 2 + .../TypeGen.Core/Generator/Generator.cs | 96 ++++++++++--------- .../Generator/GeneratorOptions.cs | 6 ++ .../Generator/Services/ITsContentGenerator.cs | 3 +- .../Generator/Services/TsContentGenerator.cs | 93 ++++++++++++------ .../TypeGen.Core/Utils/FileSystemUtils.cs | 4 +- .../AddFolderConverter.cs | 18 ++++ .../TestingUtils/GenerationTestBase.cs | 5 +- .../TypeGen.IntegrationTest/tgconfig.json | 3 +- 11 files changed, 155 insertions(+), 80 deletions(-) create mode 100644 src/TypeGen/TypeGen.IntegrationTest/AddFolderConverter.cs diff --git a/src/TypeGen/TypeGen.Cli/AppConfig.cs b/src/TypeGen/TypeGen.Cli/AppConfig.cs index 3a6e3954..e4da7c11 100644 --- a/src/TypeGen/TypeGen.Cli/AppConfig.cs +++ b/src/TypeGen/TypeGen.Cli/AppConfig.cs @@ -8,6 +8,6 @@ namespace TypeGen.Cli { internal class AppConfig { - public static string Version => "4.3.0"; + public static string Version => "4.2.118"; } } diff --git a/src/TypeGen/TypeGen.Cli/Business/GeneratorOptionsProvider.cs b/src/TypeGen/TypeGen.Cli/Business/GeneratorOptionsProvider.cs index 828a23bf..2ebaf5ed 100644 --- a/src/TypeGen/TypeGen.Cli/Business/GeneratorOptionsProvider.cs +++ b/src/TypeGen/TypeGen.Cli/Business/GeneratorOptionsProvider.cs @@ -67,7 +67,8 @@ public GeneratorOptions GetGeneratorOptions(TgConfig config, IEnumerable public event EventHandler FileContentGenerated; - + /// /// A logger instance used to log messages raised by a Generator instance /// public ILogger Logger { get; } - + /// /// Generator options. Cannot be null. /// public GeneratorOptions Options { get; } - + private readonly MetadataReaderFactory _metadataReaderFactory; private readonly ITypeService _typeService; private readonly ITypeDependencyService _typeDependencyService; @@ -49,13 +49,13 @@ public class Generator public Generator(GeneratorOptions options, ILogger logger = null) { Requires.NotNull(options, nameof(options)); - + _generationContext = new GenerationContext(); FileContentGenerated += OnFileContentGenerated; - + Options = options; Logger = logger; - + var generatorOptionsProvider = new GeneratorOptionsProvider { GeneratorOptions = options }; var internalStorage = new InternalStorage(); @@ -73,7 +73,7 @@ public Generator(GeneratorOptions options, ILogger logger = null) generatorOptionsProvider, logger); } - + public Generator(ILogger logger) : this(new GeneratorOptions(), logger) { } @@ -91,7 +91,7 @@ internal Generator(GeneratorOptions options, IFileSystem fileSystem) : this(opti { _fileSystem = fileSystem; } - + /// /// The default event handler for the FileContentGenerated event /// @@ -110,7 +110,7 @@ public void SubscribeDefaultFileContentGeneratedHandler() FileContentGenerated -= OnFileContentGenerated; FileContentGenerated += OnFileContentGenerated; } - + /// /// Unsubscribes the default FileContentGenerated event handler, which saves generated sources to the file system /// @@ -142,11 +142,11 @@ public Task> GenerateAsync(IEnumerable gener public IEnumerable Generate(IEnumerable generationSpecs) { Requires.NotNullOrEmpty(generationSpecs, nameof(generationSpecs)); - + var files = new List(); - + // generate types - + _generationContext.InitializeGroupGeneratedTypes(); foreach (GenerationSpec generationSpec in generationSpecs) @@ -159,23 +159,23 @@ public IEnumerable Generate(IEnumerable generationSpecs) files.AddRange(GenerateTypeInit(kvp.Key)); } } - + files = files.Distinct().ToList(); - + _generationContext.ClearGroupGeneratedTypes(); - + // generate barrels - + if (Options.CreateIndexFile) { files.AddRange(GenerateIndexFile(files)); } - + foreach (GenerationSpec generationSpec in generationSpecs) { generationSpec.OnBeforeBarrelGeneration(new OnBeforeBarrelGenerationArgs(Options, files)); } - + foreach (GenerationSpec generationSpec in generationSpecs) { foreach (BarrelSpec barrelSpec in generationSpec.BarrelSpecs) @@ -190,20 +190,20 @@ public IEnumerable Generate(IEnumerable generationSpecs) private IEnumerable GenerateBarrel(BarrelSpec barrelSpec) { string directory = Path.Combine(Options.BaseOutputDirectory?.EnsurePostfix("/") ?? "", barrelSpec.Directory); - + var fileName = "index"; if (!string.IsNullOrWhiteSpace(Options.TypeScriptFileExtension)) fileName += $".{Options.TypeScriptFileExtension}"; string filePath = Path.Combine(directory.EnsurePostfix("/"), fileName); var entries = new List(); - + if (barrelSpec.BarrelScope.HasFlag(BarrelScope.Files)) { entries.AddRange(_fileSystem.GetDirectoryFiles(directory) .Where(x => Path.GetFileName(x) != fileName && x.EndsWith($".{Options.TypeScriptFileExtension}")) .Select(Path.GetFileNameWithoutExtension)); } - + if (barrelSpec.BarrelScope.HasFlag(BarrelScope.Directories)) { entries.AddRange( @@ -214,11 +214,11 @@ private IEnumerable GenerateBarrel(BarrelSpec barrelSpec) string indexExportsContent = entries.Aggregate("", (acc, entry) => acc += _templateService.FillIndexExportTemplate(entry)); string content = _templateService.FillIndexTemplate(indexExportsContent); - + FileContentGenerated?.Invoke(this, new FileContentGeneratedArgs(null, filePath, content)); return new[] { Path.Combine(barrelSpec.Directory.EnsurePostfix("/"), fileName) }; } - + /// /// DEPRECATED, will be removed in the future. /// Generates an `index.ts` file which exports all types within the generated files @@ -245,11 +245,11 @@ private IEnumerable GenerateIndexFile(IEnumerable generatedFiles return new[] { filename }; } - + private IEnumerable GenerateTypeInit(Type type) { IEnumerable files = Enumerable.Empty(); - + _generationContext.InitializeTypeGeneratedTypes(); _generationContext.Add(type); @@ -266,7 +266,7 @@ private IEnumerable GenerateTypeInit(Type type) return files.Distinct(); } - + /// /// Contains the actual logic of generating TypeScript files for a given type /// Should only be used inside GenerateTypeInit(), otherwise use GenerateTypeInit() @@ -296,7 +296,7 @@ private IEnumerable GenerateType(Type type) return GenerateNotMarked(type, Options.BaseOutputDirectory); } - + /// /// Generates TypeScript files from an assembly /// @@ -306,7 +306,7 @@ public Task> GenerateAsync(Assembly assembly) { return Task.Run(() => Generate(assembly)); } - + /// /// Generates TypeScript files from an assembly /// @@ -317,7 +317,7 @@ public IEnumerable Generate(Assembly assembly) Requires.NotNull(assembly, nameof(assembly)); return Generate(new[] { assembly }); } - + /// /// Generates TypeScript files from multiple assemblies /// @@ -327,7 +327,7 @@ public Task> GenerateAsync(IEnumerable assemblies) { return Task.Run(() => Generate(assemblies)); } - + /// /// Generates TypeScript files from multiple assemblies /// @@ -336,7 +336,7 @@ public Task> GenerateAsync(IEnumerable assemblies) public IEnumerable Generate(IEnumerable assemblies) { Requires.NotNullOrEmpty(assemblies, nameof(assemblies)); - + var generationSpecProvider = new GenerationSpecProvider(); GenerationSpec generationSpec = generationSpecProvider.GetGenerationSpec(assemblies); @@ -352,7 +352,7 @@ public Task> GenerateAsync(Type type) { return Task.Run(() => Generate(type)); } - + /// /// Generates TypeScript files from a type /// @@ -361,7 +361,7 @@ public Task> GenerateAsync(Type type) public IEnumerable Generate(Type type) { Requires.NotNull(type, nameof(type)); - + var generationSpecProvider = new GenerationSpecProvider(); GenerationSpec generationSpec = generationSpecProvider.GetGenerationSpec(type); @@ -383,7 +383,7 @@ private IEnumerable GenerateNotMarked(Type type, string outputDirectory) ? GenerateInterface(type, new ExportTsInterfaceAttribute { OutputDir = outputDirectory }) : GenerateClass(type, new ExportTsClassAttribute { OutputDir = outputDirectory }); } - + if (typeInfo.IsInterface) return GenerateInterface(type, new ExportTsInterfaceAttribute { OutputDir = outputDirectory }); @@ -430,11 +430,11 @@ private IEnumerable GenerateClassOrInterface(Type type, ExportTsClassAtt if (tsCustomBaseAttribute != null) { extendsText = string.IsNullOrEmpty(tsCustomBaseAttribute.Base) ? "" : _templateService.GetExtendsText(tsCustomBaseAttribute.Base); - + var implementedInterfaceNames = GetNotNullOrEmptyImplementedInterfaceNames(tsCustomBaseAttribute); if (interfaceAttribute != null && implementedInterfaceNames.Any()) throw new InvalidOperationException($"TS Interface type ({type.FullName}) cannot implement interfaces."); - + implementsText = interfaceAttribute != null || implementedInterfaceNames.None() ? "" : _templateService.GetImplementsText(implementedInterfaceNames); } else if (tsIgnoreBaseAttribute == null) @@ -458,7 +458,7 @@ private IEnumerable GenerateClassOrInterface(Type type, ExportTsClassAtt string customBody = _tsContentGenerator.GetCustomBody(filePath, Options.TabLength); string content; - + if (classAttribute != null) { content = _typeService.UseDefaultExport(type) ? @@ -483,7 +483,7 @@ private static List GetNotNullOrEmptyImplementedInterfaceNames(TsCustomB .Select(x => x.Name) .Where(x => !string.IsNullOrEmpty(x)) .ToList(); - + /// /// Generates a TypeScript enum file from a class type /// @@ -502,7 +502,7 @@ private IEnumerable GenerateEnum(Type type, ExportTsEnumAttribute enumAt string customHead = _tsContentGenerator.GetCustomHead(filePath); string customBody = _tsContentGenerator.GetCustomBody(filePath, Options.TabLength); - string enumText = _typeService.UseDefaultExport(type) ? + string enumText = _typeService.UseDefaultExport(type) ? _templateService.FillEnumDefaultExportTemplate("", tsEnumName, valuesText, enumAttribute.IsConst, enumAttribute.AsUnionType, Options.FileHeading) : _templateService.FillEnumTemplate("", tsEnumName, valuesText, enumAttribute.IsConst, enumAttribute.AsUnionType, customHead, customBody, Options.FileHeading); @@ -517,7 +517,7 @@ private bool IsStaticTsProperty(MemberInfo memberInfo) if (_metadataReaderFactory.GetInstance().GetAttribute(memberInfo) != null) return false; return _metadataReaderFactory.GetInstance().GetAttribute(memberInfo) != null || memberInfo.IsStatic(); } - + private bool IsReadonlyTsProperty(MemberInfo memberInfo) { if (_metadataReaderFactory.GetInstance().GetAttribute(memberInfo) != null) return false; @@ -532,7 +532,7 @@ private bool IsReadonlyTsProperty(MemberInfo memberInfo) private string GetClassPropertyText(MemberInfo memberInfo) { LogClassPropertyWarnings(memberInfo); - + string modifiers = Options.ExplicitPublicAccessor ? "public " : ""; if (IsStaticTsProperty(memberInfo)) modifiers += "static "; @@ -559,15 +559,19 @@ private string GetClassPropertyText(MemberInfo memberInfo) if (defaultValueAttribute != null) return _templateService.FillClassPropertyTemplate(modifiers, name, typeName, typeUnions, isOptional, defaultValueAttribute.DefaultValue); + string fallback = null; + // try to get default value from Options.DefaultValuesForTypes + if (Options.DefaultValuesForTypes.Any() && Options.DefaultValuesForTypes.ContainsKey(typeName)) + fallback = Options.DefaultValuesForTypes[typeName]; + // try to get default value from the member's default value - string valueText = _tsContentGenerator.GetMemberValueText(memberInfo); + string valueText = _tsContentGenerator.GetMemberValueText(memberInfo, fallback); + if (((string.IsNullOrWhiteSpace(valueText) || valueText == "null") && Options.StrictMode) && !typeUnions.Contains("null")) + typeUnions = typeUnions.Append("null"); + if (!string.IsNullOrWhiteSpace(valueText)) return _templateService.FillClassPropertyTemplate(modifiers, name, typeName, typeUnions, isOptional, valueText); - // try to get default value from Options.DefaultValuesForTypes - if (Options.DefaultValuesForTypes.Any() && Options.DefaultValuesForTypes.ContainsKey(typeName)) - return _templateService.FillClassPropertyTemplate(modifiers, name, typeName, typeUnions, isOptional, Options.DefaultValuesForTypes[typeName]); - return _templateService.FillClassPropertyTemplate(modifiers, name, typeName, typeUnions, isOptional); } diff --git a/src/TypeGen/TypeGen.Core/Generator/GeneratorOptions.cs b/src/TypeGen/TypeGen.Core/Generator/GeneratorOptions.cs index 7798f90a..44f9a13b 100644 --- a/src/TypeGen/TypeGen.Core/Generator/GeneratorOptions.cs +++ b/src/TypeGen/TypeGen.Core/Generator/GeneratorOptions.cs @@ -32,6 +32,7 @@ public class GeneratorOptions public static bool DefaultUseDefaultExport => false; public static string DefaultIndexFileExtension => DefaultTypeScriptFileExtension; public static bool DefaultExportTypesAsInterfacesByDefault => false; + public static bool DefaultStrictMode => false; /// /// A collection (chain) of converters used for converting C# file names to TypeScript file names @@ -155,5 +156,10 @@ public string BaseOutputDirectory /// Whether to export types as interfaces by default. For example affects member types which aren't explicitly selected to be generated. /// public bool ExportTypesAsInterfacesByDefault { get; set; } = DefaultExportTypesAsInterfacesByDefault; + + /// + /// Whether to append null to type unions where null is the default value. + /// + public bool StrictMode { get; set; } = DefaultStrictMode; } } diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs b/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs index 689a2b58..30bd97ef 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs @@ -42,8 +42,9 @@ internal interface ITsContentGenerator /// Gets text to be used as a member value /// /// + /// /// The text to be used as a member value. Null if the member has no value or value cannot be determined. - string GetMemberValueText(MemberInfo memberInfo); + string GetMemberValueText(MemberInfo memberInfo, string? fallback = null); string GetImplementsText(Type type); string GetExtendsForInterfacesText(Type type); string GetConstructorText(Type type); diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs index c8e1290f..3a47fdba 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices.ComTypes; using System.Text; using System.Xml.Linq; using Newtonsoft.Json; @@ -157,22 +158,47 @@ private string GetTypeDependencyImportsText(Type type, string outputDir) typeDependencies = typeDependencies.Where(td => !td.IsBase); } + var startFilePath = GeneratorOptions.FileNameConverters.Convert(type.Name.RemoveTypeArity(), type); + var startFileName = startFilePath; + + var startOutputDir = outputDir == null ? startFilePath : Path.Combine(outputDir, startFilePath); + if (startOutputDir.IndexOf(Path.DirectorySeparatorChar) != -1) + { + startFileName = startOutputDir.Substring(startOutputDir.LastIndexOf(Path.DirectorySeparatorChar) + 1); + startOutputDir = startOutputDir.Remove(startOutputDir.LastIndexOf(Path.DirectorySeparatorChar)); + } + else + { + startOutputDir = outputDir; + } + foreach (TypeDependencyInfo typeDependencyInfo in typeDependencies) { Type typeDependency = typeDependencyInfo.Type; - string dependencyOutputDir = GetTypeDependencyOutputDir(typeDependencyInfo, outputDir); - - // get path diff - string pathDiff = FileSystemUtils.GetPathDiff(outputDir, dependencyOutputDir); - pathDiff = pathDiff.StartsWith("..\\") || pathDiff.StartsWith("../") ? pathDiff : $"./{pathDiff}"; - // get type & file name string typeDependencyName = typeDependency.Name.RemoveTypeArity(); - string fileName = GeneratorOptions.FileNameConverters.Convert(typeDependencyName, typeDependency); + string endFilePath = GeneratorOptions.FileNameConverters.Convert(typeDependencyName, typeDependency); + string endFileName = endFilePath; + var endOutputDir = GetTypeDependencyOutputDir(typeDependencyInfo, outputDir); + endOutputDir = endOutputDir == null ? endFilePath : Path.Combine(endOutputDir, endFilePath); + if (endOutputDir.IndexOf(Path.DirectorySeparatorChar) != -1) + { + endFileName = endOutputDir.Substring(endOutputDir.LastIndexOf(Path.DirectorySeparatorChar) + 1); + endOutputDir = endOutputDir.Remove(endOutputDir.LastIndexOf(Path.DirectorySeparatorChar)); + } + else + { + endOutputDir = outputDir; + } + + // get path diff + string pathDiff = FileSystemUtils.GetPathDiff(startOutputDir, endOutputDir); + pathDiff = pathDiff.StartsWith("..\\") || pathDiff.StartsWith("../") ? pathDiff : $"./{pathDiff}"; + _logger.Log($"{startOutputDir} -> {endOutputDir} = {pathDiff}", LogLevel.Info); // get file path - string dependencyPath = Path.Combine(pathDiff.EnsurePostfix("/"), fileName); + string dependencyPath = Path.Combine(pathDiff.EnsurePostfix("/"), endFileName); dependencyPath = dependencyPath.Replace('\\', '/'); string typeName = GeneratorOptions.TypeNameConverters.Convert(typeDependencyName, typeDependency); @@ -312,11 +338,12 @@ public string GetCustomHead(string filePath) /// Gets text to be used as a member value /// /// + /// /// The text to be used as a member value. Null if the member has no value or value cannot be determined. - public string GetMemberValueText(MemberInfo memberInfo) + public string GetMemberValueText(MemberInfo memberInfo, string? fallback = null) { var temp = memberInfo.Name; - if (memberInfo.DeclaringType == null) return null; + if (memberInfo.DeclaringType == null) return fallback; try { @@ -329,22 +356,32 @@ public string GetMemberValueText(MemberInfo memberInfo) object valueObjGuard = valueObj; bool isConstant = false; - GetMemberValue(memberInfo, instance, out valueObj, out isConstant); + var valueType = GetMemberValue(memberInfo, instance, out valueObj, out isConstant); // if only default values for constants are allowed - if (GeneratorOptions.CsDefaultValuesForConstantsOnly && !isConstant) return null; + if (GeneratorOptions.CsDefaultValuesForConstantsOnly && !isConstant) return fallback; // if valueObj hasn't been assigned in the switch - if (valueObj == valueObjGuard) return null; - + if (valueObj == valueObjGuard) return fallback; + // if valueObj's value is the default value for its type - if (valueObj == null || valueObj.Equals(TypeUtils.GetDefaultValue(valueObj.GetType()))) return null; + var defaultValueForType = TypeUtils.GetDefaultValue(valueType); + if (_typeService.IsCollectionType(valueType)) + valueObj = new List(); + else if (_typeService.IsDictionaryType(valueType)) + valueObj = new Dictionary(); + else if (valueObj == null) + return _generatorOptionsProvider.GeneratorOptions.StrictMode ? fallback ?? "null" : fallback; + else if (valueObj.Equals(defaultValueForType)) + if (fallback != null || !_generatorOptionsProvider.GeneratorOptions.StrictMode) + return fallback; + else + valueObj = defaultValueForType; - var valueType = valueObj.GetType(); + valueType = valueObj.GetType(); string memberType = _typeService.GetTsTypeName(memberInfo).GetTsTypeUnion(0); string quote = GeneratorOptions.SingleQuotes ? "'" : "\""; - switch (valueObj) { case Guid valueGuid when memberType == "string": @@ -365,7 +402,7 @@ public string GetMemberValueText(MemberInfo memberInfo) !valueType.GetTypeInfo().IsValueType && // Ignore value types valueType.GetConstructor(Type.EmptyTypes) != null) // Make sure the type has a default constructor to use for this { - _logger?.Log($"Checking type {valueType.FullName} for constructor usage", LogLevel.Info); + //_logger?.Log($"Checking type {valueType.FullName} for constructor usage", LogLevel.Info); var defaultCtorValueType = Activator.CreateInstance(valueType); if (defaultCtorValueType != null && memberwiseEquals(valueObj, defaultCtorValueType)) { @@ -388,7 +425,7 @@ public string GetMemberValueText(MemberInfo memberInfo) _logger?.Log($"Cannot determine the default value for member '{memberInfo.DeclaringType.FullName}.{memberInfo.Name}', because an unknown exception occurred: '{e.Message}'", LogLevel.Debug); } - return null; + return fallback; } private bool memberwiseEquals(object a, object b) @@ -409,7 +446,7 @@ private bool memberwiseEquals(object a, object b) GetMemberValue(member, b, out var bVal, out var _); if (!memberwiseEquals(aVal, bVal)) { - _logger?.Log($"Difference found in member {type.FullName}.{member.Name}: {aVal} != {bVal}", LogLevel.Info); + //_logger?.Log($"Difference found in member {type.FullName}.{member.Name}: {aVal} != {bVal}", LogLevel.Info); return false; } } @@ -418,18 +455,18 @@ private bool memberwiseEquals(object a, object b) return true; } - private void GetMemberValue(MemberInfo memberInfo, object instance, out object valueObj, out bool isConstant) + private Type GetMemberValue(MemberInfo memberInfo, object instance, out object valueObj, out bool isConstant) { switch (memberInfo) { case FieldInfo fieldInfo: valueObj = fieldInfo.GetValue(instance); isConstant = fieldInfo.IsStatic && fieldInfo.IsLiteral && !fieldInfo.IsInitOnly; - break; + return fieldInfo.FieldType; case PropertyInfo propertyInfo: valueObj = propertyInfo.GetValue(instance); isConstant = false; - break; + return propertyInfo.PropertyType; default: throw new Exception(); } @@ -457,13 +494,15 @@ public string GetConstructorText(Type type) } else { + // try to get default value from Options.DefaultValuesForTypes + string fallback = null; + if (_generatorOptionsProvider.GeneratorOptions.DefaultValuesForTypes.Any() && _generatorOptionsProvider.GeneratorOptions.DefaultValuesForTypes.ContainsKey(typeName)) + fallback = _generatorOptionsProvider.GeneratorOptions.DefaultValuesForTypes[typeName]; + // try to get default value from the member's default value - string valueText = GetMemberValueText(m); + string valueText = GetMemberValueText(m, fallback); if (!string.IsNullOrWhiteSpace(valueText)) defaultValue = valueText; - // try to get default value from Options.DefaultValuesForTypes - else if (_generatorOptionsProvider.GeneratorOptions.DefaultValuesForTypes.Any() && _generatorOptionsProvider.GeneratorOptions.DefaultValuesForTypes.ContainsKey(typeName)) - defaultValue = _generatorOptionsProvider.GeneratorOptions.DefaultValuesForTypes[typeName]; else defaultValue = null; } diff --git a/src/TypeGen/TypeGen.Core/Utils/FileSystemUtils.cs b/src/TypeGen/TypeGen.Core/Utils/FileSystemUtils.cs index a5e40c97..a538b222 100644 --- a/src/TypeGen/TypeGen.Core/Utils/FileSystemUtils.cs +++ b/src/TypeGen/TypeGen.Core/Utils/FileSystemUtils.cs @@ -34,8 +34,8 @@ public static string[] SplitPathSeparator(string path) /// public static string GetPathDiff(string pathFrom, string pathTo) { - var pathFromUri = new Uri("file:///" + pathFrom?.Replace('\\', '/')); - var pathToUri = new Uri("file:///" + pathTo?.Replace('\\', '/')); + var pathFromUri = new Uri("file:///root/" + pathFrom?.Replace('\\', '/').EnsurePostfix("/")); + var pathToUri = new Uri("file:///root/" + pathTo?.Replace('\\', '/').EnsurePostfix("/")); return pathFromUri.MakeRelativeUri(pathToUri).ToString(); } diff --git a/src/TypeGen/TypeGen.IntegrationTest/AddFolderConverter.cs b/src/TypeGen/TypeGen.IntegrationTest/AddFolderConverter.cs new file mode 100644 index 00000000..18bd81cd --- /dev/null +++ b/src/TypeGen/TypeGen.IntegrationTest/AddFolderConverter.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TypeGen.Core.Converters; + +namespace TypeGen.IntegrationTest +{ + internal class AddFolderConverter : ITypeNameConverter + { + public string Convert(string name, Type type) + { + return "foobar" + Path.DirectorySeparatorChar + name; + } + } +} diff --git a/src/TypeGen/TypeGen.IntegrationTest/TestingUtils/GenerationTestBase.cs b/src/TypeGen/TypeGen.IntegrationTest/TestingUtils/GenerationTestBase.cs index d15d4561..cc615c8a 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/TestingUtils/GenerationTestBase.cs +++ b/src/TypeGen/TypeGen.IntegrationTest/TestingUtils/GenerationTestBase.cs @@ -38,7 +38,10 @@ protected async Task TestFromAssembly(Type type, string expectedLocation) { var readExpectedTask = EmbededResourceReader.GetEmbeddedResourceAsync(expectedLocation); - var generator = new Generator(logger); + GeneratorOptions options = new GeneratorOptions(); + options.FileNameConverters.Add(new AddFolderConverter()); + + var generator = new Generator(options, logger); var interceptor = GeneratorOutputInterceptor.CreateInterceptor(generator); await generator.GenerateAsync(type.Assembly); diff --git a/src/TypeGen/TypeGen.IntegrationTest/tgconfig.json b/src/TypeGen/TypeGen.IntegrationTest/tgconfig.json index 23fac4de..3f1b9601 100644 --- a/src/TypeGen/TypeGen.IntegrationTest/tgconfig.json +++ b/src/TypeGen/TypeGen.IntegrationTest/tgconfig.json @@ -1,7 +1,8 @@ { "externalAssemblyPaths": ["C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder"], "outputPath": "generated-typescript", - "propertyNameConverters": ["JsonMemberName", "PascalCaseToCamelCaseJson"], + "propertyNameConverters": [ "JsonMemberName", "PascalCaseToCamelCaseJson" ], + "fileNameConverters": [ "PascalCaseToKebabCase", "AddFolder" ], "csNullableTranslation": null, "createIndexFile": true } \ No newline at end of file From 9ca406e8a9e49670cc677bc34bb89b4c4f138ca4 Mon Sep 17 00:00:00 2001 From: tterrag1098 Date: Wed, 13 Dec 2023 23:16:38 -0500 Subject: [PATCH 5/6] Fixup merge and update tests --- src/TypeGen/TypeGen.Cli.Test/CliSmokeTest.cs | 13 +++++-- .../Services/TsContentGeneratorTest.cs | 2 +- .../Utils/FileSystemUtilsTest.cs | 14 ++++---- .../TypeGen.Core/Generator/Generator.cs | 16 ++++----- .../Generator/Services/ITemplateService.cs | 4 +-- .../Generator/Services/ITsContentGenerator.cs | 2 +- .../Generator/Services/ITypeService.cs | 8 +++++ .../Generator/Services/TsContentGenerator.cs | 35 +++++++++++-------- .../Generator/Services/TypeService.cs | 12 +++++++ .../Blacklist/BlacklistTest.cs | 3 ++ .../Comments/CommentsTest.cs | 3 ++ .../CommonCases/CommonCasesGenerationTest.cs | 6 ++++ .../CommonCases/Expected/array-of-nullable.ts | 2 +- .../dictionary-string-object-error-case.ts | 2 +- .../Expected/dictionary-with-enum-key.ts | 2 +- .../GenericInheritanceTest.cs | 3 ++ .../ImportType/ImportTypeTest.cs | 3 ++ .../TestingUtils/GenerationTestBase.cs | 5 +-- .../TsClassExtendsTsInterfaceTest.cs | 3 ++ .../TsInterfaceInheritanceTest.cs | 3 ++ 20 files changed, 100 insertions(+), 41 deletions(-) diff --git a/src/TypeGen/TypeGen.Cli.Test/CliSmokeTest.cs b/src/TypeGen/TypeGen.Cli.Test/CliSmokeTest.cs index 27fb6268..f4a63116 100644 --- a/src/TypeGen/TypeGen.Cli.Test/CliSmokeTest.cs +++ b/src/TypeGen/TypeGen.Cli.Test/CliSmokeTest.cs @@ -6,11 +6,16 @@ using System.Text; using FluentAssertions; using Xunit; +using Xunit.Abstractions; namespace TypeGen.Cli.Test { public class CliSmokeTest { + private readonly ITestOutputHelper logger; + + public CliSmokeTest(ITestOutputHelper output) => this.logger = output; + [Fact] public void Cli_should_finish_with_success() { @@ -19,8 +24,8 @@ public void Cli_should_finish_with_success() const string projectToGeneratePath = "../../../../TypeGen.FileContentTest"; const string cliFileName = "TypeGen.Cli.exe"; string[] cliPossibleDirectories = { - "../../../../TypeGen.Cli/bin/Debug/net7.0", - "../../../../TypeGen.Cli/bin/Release/net7.0", + "../../../../TypeGen.Cli/bin/Debug/net8.0", + "../../../../TypeGen.Cli/bin/Release/net8.0", }; var cliFilePath = GetCliDirectory(cliPossibleDirectories); @@ -47,7 +52,9 @@ public void Cli_should_finish_with_success() var outputBuilder = new StringBuilder(); while (!process.StandardOutput.EndOfStream) { - outputBuilder.AppendLine(process.StandardOutput.ReadLine()); + var line = process.StandardOutput.ReadLine(); + logger.WriteLine(line); + outputBuilder.AppendLine(line); } var output = outputBuilder.ToString().TrimEnd(); diff --git a/src/TypeGen/TypeGen.Core.Test/Generator/Services/TsContentGeneratorTest.cs b/src/TypeGen/TypeGen.Core.Test/Generator/Services/TsContentGeneratorTest.cs index 656e4f5b..f1b0f44e 100644 --- a/src/TypeGen/TypeGen.Core.Test/Generator/Services/TsContentGeneratorTest.cs +++ b/src/TypeGen/TypeGen.Core.Test/Generator/Services/TsContentGeneratorTest.cs @@ -280,7 +280,7 @@ public void GetMemberValueText_MemberGiven_CorrectValueReturned(MemberInfo membe var tsContentGenerator = new TsContentGenerator(_typeDependencyService, typeService, _templateService, _tsContentParser, _metadataReaderFactory, generatorOptionsProvider, null); //act - string actual = tsContentGenerator.GetMemberValueText(memberInfo); + string actual = tsContentGenerator.GetMemberValueText(memberInfo, false); //assert Assert.Equal(expected, actual); diff --git a/src/TypeGen/TypeGen.Core.Test/Utils/FileSystemUtilsTest.cs b/src/TypeGen/TypeGen.Core.Test/Utils/FileSystemUtilsTest.cs index 5087d996..a31f9182 100644 --- a/src/TypeGen/TypeGen.Core.Test/Utils/FileSystemUtilsTest.cs +++ b/src/TypeGen/TypeGen.Core.Test/Utils/FileSystemUtilsTest.cs @@ -21,14 +21,14 @@ public void SplitPathSeparator_PathGiven_PathSplit(string path) } [Theory] - [InlineData(@"path\to\file.txt", @"path\file.txt", @"../file.txt")] - [InlineData("path/to/file.txt", "path/file.txt", @"../file.txt")] - [InlineData("path/to/some/nested/file.txt", "path/file.txt", @"../../../file.txt")] - [InlineData("path/to/some/nested", "path/file.txt", @"../../file.txt")] + [InlineData(@"path\to\file.txt", @"path\file.txt", @"../file.txt/")] + [InlineData("path/to/file.txt", "path/file.txt", @"../file.txt/")] + [InlineData("path/to/some/nested/file.txt", "path/file.txt", @"../../../file.txt/")] + [InlineData("path/to/some/nested", "path/file.txt", @"../../file.txt/")] [InlineData("path/to/some/nested/", "path/", @"../../../")] - [InlineData(@"path\file.txt", "path/to/some/nested/file.txt", @"to/some/nested/file.txt")] - [InlineData("path/files/file.txt", @"path\to\some\nested\file.txt", @"../to/some/nested/file.txt")] - [InlineData("path/files/", @"path\to\some\nested\file.txt", @"../to/some/nested/file.txt")] + [InlineData(@"path\file.txt", "path/to/some/nested/file.txt", @"to/some/nested/file.txt/")] + [InlineData("path/files/file.txt", @"path\to\some\nested\file.txt", @"../to/some/nested/file.txt/")] + [InlineData("path/files/", @"path\to\some\nested\file.txt", @"../to/some/nested/file.txt/")] public void GetPathDiff_Test(string pathFrom, string pathTo, string expectedResult) { string actualResult = FileSystemUtils.GetPathDiff(pathFrom, pathTo); diff --git a/src/TypeGen/TypeGen.Core/Generator/Generator.cs b/src/TypeGen/TypeGen.Core/Generator/Generator.cs index 33a204a6..b591694f 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Generator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Generator.cs @@ -326,7 +326,7 @@ private IEnumerable GenerateIndexFile(IEnumerable generatedFiles return new[] { filename }; } - private IEnumerable GenerateTypeInit(Type type) + private IEnumerable GenerateMarkedType(Type type) { if (Options.IsTypeBlacklisted(type)) return Enumerable.Empty(); @@ -429,6 +429,7 @@ private IEnumerable GenerateClass(Type type, ExportTsClassAttribute clas string importsText = _tsContentGenerator.GetImportsText(type, outputDir); string propertiesText = GetClassPropertiesText(type); + string constructorText = _tsContentGenerator.GetConstructorText(type); // generate the file content @@ -445,8 +446,8 @@ private IEnumerable GenerateClass(Type type, ExportTsClassAttribute clas var tsDoc = GetTsDocForType(type); var content = _typeService.UseDefaultExport(type) ? - _templateService.FillClassDefaultExportTemplate(importsText, tsTypeName, tsTypeNameFirstPart, extendsText, implementsText, propertiesText, tsDoc, customHead, customBody, Options.FileHeading) : - _templateService.FillClassTemplate(importsText, tsTypeName, extendsText, implementsText, propertiesText, tsDoc, customHead, customBody, Options.FileHeading); + _templateService.FillClassDefaultExportTemplate(importsText, tsTypeName, tsTypeNameFirstPart, extendsText, implementsText, propertiesText, constructorText, tsDoc, customHead, customBody, Options.FileHeading) : + _templateService.FillClassTemplate(importsText, tsTypeName, extendsText, implementsText, propertiesText, constructorText, tsDoc, customHead, customBody, Options.FileHeading); // write TypeScript file FileContentGenerated?.Invoke(this, new FileContentGeneratedArgs(type, filePath, content)); @@ -482,7 +483,6 @@ private IEnumerable GenerateInterface(Type type, ExportTsInterfaceAttrib string importsText = _tsContentGenerator.GetImportsText(type, outputDir); string propertiesText = GetInterfacePropertiesText(type); - string constructorText = _tsContentGenerator.GetConstructorText(type); // generate the file content string tsTypeName = _typeService.GetTsTypeName(type, true); @@ -498,8 +498,8 @@ private IEnumerable GenerateInterface(Type type, ExportTsInterfaceAttrib var tsDoc = GetTsDocForType(type); var content = _typeService.UseDefaultExport(type) ? - _templateService.FillInterfaceDefaultExportTemplate(importsText, tsTypeName, tsTypeNameFirstPart, extendsText, propertiesText, constructorText, tsDoc, customHead, customBody, Options.FileHeading) : - _templateService.FillInterfaceTemplate(importsText, tsTypeName, extendsText, propertiesText, constructorText, tsDoc, customHead, customBody, Options.FileHeading); + _templateService.FillInterfaceDefaultExportTemplate(importsText, tsTypeName, tsTypeNameFirstPart, extendsText, propertiesText, tsDoc, customHead, customBody, Options.FileHeading) : + _templateService.FillInterfaceTemplate(importsText, tsTypeName, extendsText, propertiesText, tsDoc, customHead, customBody, Options.FileHeading); // write TypeScript file FileContentGenerated?.Invoke(this, new FileContentGeneratedArgs(type, filePath, content)); @@ -590,7 +590,7 @@ private string GetClassPropertyText(Type type, MemberInfo memberInfo) var ctorAttribute = _metadataReaderFactory.GetInstance().GetAttribute(memberInfo); if (ctorAttribute != null) - return _templateService.FillClassPropertyTemplate(modifiers, name, typeName, typeUnions, isOptional); + return _templateService.FillClassPropertyTemplate(modifiers, name, typeName, typeUnions, isOptional, tsDoc); // try to get default value from TsDefaultValueAttribute var defaultValueAttribute = _metadataReaderFactory.GetInstance().GetAttribute(memberInfo); @@ -603,7 +603,7 @@ private string GetClassPropertyText(Type type, MemberInfo memberInfo) fallback = Options.DefaultValuesForTypes[typeName]; // try to get default value from the member's default value - string valueText = _tsContentGenerator.GetMemberValueText(memberInfo, fallback); + string valueText = _tsContentGenerator.GetMemberValueText(memberInfo, isOptional, fallback); if (((string.IsNullOrWhiteSpace(valueText) || valueText == "null") && Options.StrictMode) && !typeUnions.Contains("null")) typeUnions = typeUnions.Append("null"); diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/ITemplateService.cs b/src/TypeGen/TypeGen.Core/Generator/Services/ITemplateService.cs index fafb4802..9fc3e560 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/ITemplateService.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/ITemplateService.cs @@ -4,8 +4,8 @@ namespace TypeGen.Core.Generator.Services { internal interface ITemplateService { - string FillClassTemplate(string imports, string name, string extends, string implements, string properties, string tsDoc, string customHead, string customBody, string fileHeading = null); - string FillClassDefaultExportTemplate(string imports, string name, string exportName, string extends, string implements, string properties, string tsDoc, string customHead, string customBody, string fileHeading = null); + string FillClassTemplate(string imports, string name, string extends, string implements, string properties, string constructor, string tsDoc, string customHead, string customBody, string fileHeading = null); + string FillClassDefaultExportTemplate(string imports, string name, string exportName, string extends, string implements, string properties, string constructor, string tsDoc, string customHead, string customBody, string fileHeading = null); string FillConstructorTemplate(string type, string parameters, string superCall, string body); string FillConstructorAssignmentTemplate(string name); string FillClassPropertyTemplate(string modifiers, string name, string type, IEnumerable typeUnions, bool isOptional, string tsDoc, string defaultValue = null); diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs b/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs index 8697389b..16225ffe 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/ITsContentGenerator.cs @@ -44,7 +44,7 @@ internal interface ITsContentGenerator /// /// /// The text to be used as a member value. Null if the member has no value or value cannot be determined. - string GetMemberValueText(MemberInfo memberInfo, string? fallback = null); + string GetMemberValueText(MemberInfo memberInfo, bool isOptional, string? fallback = null); string GetImplementsText(Type type); string GetExtendsForInterfacesText(Type type); string GetConstructorText(Type type); diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/ITypeService.cs b/src/TypeGen/TypeGen.Core/Generator/Services/ITypeService.cs index f39856e7..f88ab042 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/ITypeService.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/ITypeService.cs @@ -21,6 +21,14 @@ internal interface ITypeService /// TypeScript type name. Null if the passed type cannot be represented as a TypeScript simple type. string GetTsBuiltInTypeName(Type type); + /// + /// Determines whether the type represents a TypeScript class + /// + /// + /// True if the type represents a TypeScript class; false otherwise + /// Thrown if the type is null + bool IsTsClass(Type type); + /// /// Determines whether the type represents a TypeScript class /// diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs index cc887b44..ae2e7ca9 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs @@ -162,10 +162,10 @@ private string GetTypeDependencyImportsText(Type type, string outputDir) var startFileName = startFilePath; var startOutputDir = outputDir == null ? startFilePath : Path.Combine(outputDir, startFilePath); - if (startOutputDir.IndexOf(Path.DirectorySeparatorChar) != -1) + if (startOutputDir.IndexOf('/') != -1) { - startFileName = startOutputDir.Substring(startOutputDir.LastIndexOf(Path.DirectorySeparatorChar) + 1); - startOutputDir = startOutputDir.Remove(startOutputDir.LastIndexOf(Path.DirectorySeparatorChar)); + startFileName = startOutputDir.Substring(startOutputDir.LastIndexOf('/') + 1); + startOutputDir = startOutputDir.Remove(startOutputDir.LastIndexOf('/')); } else { @@ -183,20 +183,20 @@ private string GetTypeDependencyImportsText(Type type, string outputDir) var endOutputDir = GetTypeDependencyOutputDir(typeDependencyInfo, outputDir); endOutputDir = endOutputDir == null ? endFilePath : Path.Combine(endOutputDir, endFilePath); - if (endOutputDir.IndexOf(Path.DirectorySeparatorChar) != -1) + if (endOutputDir.IndexOf('/') != -1) { - endFileName = endOutputDir.Substring(endOutputDir.LastIndexOf(Path.DirectorySeparatorChar) + 1); - endOutputDir = endOutputDir.Remove(endOutputDir.LastIndexOf(Path.DirectorySeparatorChar)); + endFileName = endOutputDir.Substring(endOutputDir.LastIndexOf('/') + 1); + endOutputDir = endOutputDir.Remove(endOutputDir.LastIndexOf('/')); } else { - endOutputDir = outputDir; + endOutputDir = "./"; } // get path diff string pathDiff = FileSystemUtils.GetPathDiff(startOutputDir, endOutputDir); - pathDiff = pathDiff.StartsWith("..\\") || pathDiff.StartsWith("../") ? pathDiff : $"./{pathDiff}"; - _logger.Log($"{startOutputDir} -> {endOutputDir} = {pathDiff}", LogLevel.Info); + pathDiff = pathDiff.StartsWith("../") ? pathDiff : $"./{pathDiff}"; + _logger?.Log($"{startOutputDir} -> {endOutputDir} = {pathDiff}", LogLevel.Info); // get file path string dependencyPath = Path.Combine(pathDiff.EnsurePostfix("/"), endFileName); dependencyPath = dependencyPath.Replace('\\', '/'); @@ -340,7 +340,7 @@ public string GetCustomHead(string filePath) /// /// /// The text to be used as a member value. Null if the member has no value or value cannot be determined. - public string GetMemberValueText(MemberInfo memberInfo, string? fallback = null) + public string GetMemberValueText(MemberInfo memberInfo, bool isOptional, string? fallback = null) { var temp = memberInfo.Name; if (memberInfo.DeclaringType == null) return fallback; @@ -367,17 +367,18 @@ public string GetMemberValueText(MemberInfo memberInfo, string? fallback = null) // if valueObj's value is the default value for its type var defaultValueForType = TypeUtils.GetDefaultValue(valueType); if (_typeService.IsCollectionType(valueType)) - valueObj = new List(); + valueObj ??= isOptional ? null : new List(); else if (_typeService.IsDictionaryType(valueType)) - valueObj = new Dictionary(); + valueObj ??= isOptional ? null : new Dictionary(); else if (valueObj == null) return _generatorOptionsProvider.GeneratorOptions.StrictMode ? fallback ?? "null" : fallback; else if (valueObj.Equals(defaultValueForType)) if (fallback != null || !_generatorOptionsProvider.GeneratorOptions.StrictMode) return fallback; else - valueObj = defaultValueForType; + valueObj = isOptional ? null : defaultValueForType; + if (valueObj == null) return null; valueType = valueObj.GetType(); string memberType = _typeService.GetTsTypeName(memberInfo).GetTsTypeUnion(0); string quote = GeneratorOptions.SingleQuotes ? "'" : "\""; @@ -500,7 +501,13 @@ public string GetConstructorText(Type type) fallback = _generatorOptionsProvider.GeneratorOptions.DefaultValuesForTypes[typeName]; // try to get default value from the member's default value - string valueText = GetMemberValueText(m, fallback); + var isNullable = m.IsNullable(); + var isOptional = false; + if (isNullable && _generatorOptionsProvider.GeneratorOptions.CsNullableTranslation == StrictNullTypeUnionFlags.Optional) + { + isOptional = true; + } + string valueText = GetMemberValueText(m, isOptional, fallback); if (!string.IsNullOrWhiteSpace(valueText)) defaultValue = valueText; else diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/TypeService.cs b/src/TypeGen/TypeGen.Core/Generator/Services/TypeService.cs index 265280e9..6e24b18c 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/TypeService.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/TypeService.cs @@ -128,6 +128,18 @@ public string GetTsBuiltInTypeName(Type type) } } + /// + public bool IsTsClass(Type type) + { + Requires.NotNull(type, nameof(type)); + TypeInfo typeInfo = type.GetTypeInfo(); + + if (!typeInfo.IsClass) return false; + + var exportAttribute = MetadataReader.GetAttribute(type); + return exportAttribute == null || exportAttribute is ExportTsClassAttribute; + } + /// public bool IsTsInterface(Type type) { diff --git a/src/TypeGen/TypeGen.FileContentTest/Blacklist/BlacklistTest.cs b/src/TypeGen/TypeGen.FileContentTest/Blacklist/BlacklistTest.cs index ad24dec8..4a9c69c1 100644 --- a/src/TypeGen/TypeGen.FileContentTest/Blacklist/BlacklistTest.cs +++ b/src/TypeGen/TypeGen.FileContentTest/Blacklist/BlacklistTest.cs @@ -8,11 +8,14 @@ using TypeGen.FileContentTest.Blacklist.Entities; using TypeGen.FileContentTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.FileContentTest.Blacklist { public class BlacklistTest : GenerationTestBase { + public BlacklistTest(ITestOutputHelper output) : base(output) { } + [Fact] public async Task ClassWithBlacklistedBase_Test() { diff --git a/src/TypeGen/TypeGen.FileContentTest/Comments/CommentsTest.cs b/src/TypeGen/TypeGen.FileContentTest/Comments/CommentsTest.cs index 88374299..f04eac7f 100644 --- a/src/TypeGen/TypeGen.FileContentTest/Comments/CommentsTest.cs +++ b/src/TypeGen/TypeGen.FileContentTest/Comments/CommentsTest.cs @@ -5,11 +5,14 @@ using TypeGen.FileContentTest.Comments.Entities; using TypeGen.FileContentTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.FileContentTest.Comments; public class CommentsTest : GenerationTestBase { + public CommentsTest(ITestOutputHelper output) : base(output) { } + [Theory] [InlineData(typeof(TsClass), "TypeGen.FileContentTest.Comments.Expected.ts-class.ts", false)] [InlineData(typeof(TsClass), "TypeGen.FileContentTest.Comments.Expected.ts-class-default-export.ts", true)] diff --git a/src/TypeGen/TypeGen.FileContentTest/CommonCases/CommonCasesGenerationTest.cs b/src/TypeGen/TypeGen.FileContentTest/CommonCases/CommonCasesGenerationTest.cs index 1e6daaff..0efefbb2 100644 --- a/src/TypeGen/TypeGen.FileContentTest/CommonCases/CommonCasesGenerationTest.cs +++ b/src/TypeGen/TypeGen.FileContentTest/CommonCases/CommonCasesGenerationTest.cs @@ -4,7 +4,9 @@ using TypeGen.FileContentTest.CommonCases.Entities.Constants; using TypeGen.FileContentTest.CommonCases.Entities.ErrorCase; using TypeGen.FileContentTest.TestingUtils; +using TypeGen.IntegrationTest.CommonCases.Entities; using Xunit; +using Xunit.Abstractions; using CustomBaseClass = TypeGen.FileContentTest.CommonCases.Entities.CustomBaseClass; using CustomBaseCustomImport = TypeGen.FileContentTest.CommonCases.Entities.CustomBaseCustomImport; using CustomEmptyBaseClass = TypeGen.FileContentTest.CommonCases.Entities.CustomEmptyBaseClass; @@ -24,6 +26,8 @@ namespace TypeGen.FileContentTest.CommonCases { public class CommonCasesGenerationTest : GenerationTestBase { + public CommonCasesGenerationTest(ITestOutputHelper output) : base(output) { } + /// /// Tests if types are correctly translated to TypeScript. /// The tested types contain all major use cases that should be supported. @@ -38,6 +42,8 @@ public class CommonCasesGenerationTest : GenerationTestBase [InlineData(typeof(Entities.BaseClass<>), "TypeGen.FileContentTest.CommonCases.Expected.base-class.ts")] [InlineData(typeof(BaseClass2<>), "TypeGen.FileContentTest.CommonCases.Expected.base-class2.ts")] [InlineData(typeof(C), "TypeGen.FileContentTest.CommonCases.Expected.c.ts")] + [InlineData(typeof(ConstructorClass), "TypeGen.FileContentTest.CommonCases.Expected.constructor-class.ts")] + [InlineData(typeof(ConstructorChildClass), "TypeGen.FileContentTest.CommonCases.Expected.constructor-child-class.ts")] [InlineData(typeof(CustomBaseClass), "TypeGen.FileContentTest.CommonCases.Expected.custom-base-class.ts")] [InlineData(typeof(CustomEmptyBaseClass), "TypeGen.FileContentTest.CommonCases.Expected.custom-empty-base-class.ts")] [InlineData(typeof(D), "TypeGen.FileContentTest.CommonCases.Expected.d.ts")] diff --git a/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/array-of-nullable.ts b/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/array-of-nullable.ts index 21f7eccc..17ec1244 100644 --- a/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/array-of-nullable.ts +++ b/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/array-of-nullable.ts @@ -4,5 +4,5 @@ */ export class ArrayOfNullable { - password: number[]; + password: number[] = []; } diff --git a/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/dictionary-string-object-error-case.ts b/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/dictionary-string-object-error-case.ts index 814b77cc..494da025 100644 --- a/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/dictionary-string-object-error-case.ts +++ b/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/dictionary-string-object-error-case.ts @@ -4,5 +4,5 @@ */ export class DictionaryStringObjectErrorCase { - foo: { [key: string]: Object; }; + foo: { [key: string]: Object; } = {}; } diff --git a/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/dictionary-with-enum-key.ts b/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/dictionary-with-enum-key.ts index d05d0ebd..31224eb7 100644 --- a/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/dictionary-with-enum-key.ts +++ b/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/dictionary-with-enum-key.ts @@ -7,5 +7,5 @@ import { EnumAsUnionType } from "./enum-as-union-type"; import { CustomBaseClass } from "./custom-base-class"; export class DictionaryWithEnumKey { - enumDict: { [key in EnumAsUnionType]?: CustomBaseClass; }; + enumDict: { [key in EnumAsUnionType]?: CustomBaseClass; } = {}; } diff --git a/src/TypeGen/TypeGen.FileContentTest/GenericInheritance/GenericInheritanceTest.cs b/src/TypeGen/TypeGen.FileContentTest/GenericInheritance/GenericInheritanceTest.cs index 6a1d4bdb..a5fbd8f4 100644 --- a/src/TypeGen/TypeGen.FileContentTest/GenericInheritance/GenericInheritanceTest.cs +++ b/src/TypeGen/TypeGen.FileContentTest/GenericInheritance/GenericInheritanceTest.cs @@ -4,11 +4,14 @@ using TypeGen.FileContentTest.GenericInheritance.Entities; using TypeGen.FileContentTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.FileContentTest.GenericInheritance; public class GenericInheritanceTest : GenerationTestBase { + public GenericInheritanceTest(ITestOutputHelper output) : base(output) { } + [Theory] [InlineData(typeof(GetCustomersResponseDto), "TypeGen.FileContentTest.GenericInheritance.Expected.get-customers-response-dto.ts")] public async Task TestGenericInheritance(Type type, string expectedLocation) diff --git a/src/TypeGen/TypeGen.FileContentTest/ImportType/ImportTypeTest.cs b/src/TypeGen/TypeGen.FileContentTest/ImportType/ImportTypeTest.cs index e21bab14..5a49f5b7 100644 --- a/src/TypeGen/TypeGen.FileContentTest/ImportType/ImportTypeTest.cs +++ b/src/TypeGen/TypeGen.FileContentTest/ImportType/ImportTypeTest.cs @@ -5,11 +5,14 @@ using TypeGen.FileContentTest.ImportType.Entities; using TypeGen.FileContentTest.TestingUtils; using Xunit; +using Xunit.Abstractions; namespace TypeGen.FileContentTest.ImportType; public class ImportTypeTest : GenerationTestBase { + public ImportTypeTest(ITestOutputHelper output) : base(output) { } + [Theory] [InlineData(typeof(TsClass), "TypeGen.FileContentTest.ImportType.Expected.ts-class.ts", false)] [InlineData(typeof(TsClass), "TypeGen.FileContentTest.ImportType.Expected.ts-class-default-export.ts", true)] diff --git a/src/TypeGen/TypeGen.FileContentTest/TestingUtils/GenerationTestBase.cs b/src/TypeGen/TypeGen.FileContentTest/TestingUtils/GenerationTestBase.cs index 99428246..70237141 100644 --- a/src/TypeGen/TypeGen.FileContentTest/TestingUtils/GenerationTestBase.cs +++ b/src/TypeGen/TypeGen.FileContentTest/TestingUtils/GenerationTestBase.cs @@ -5,7 +5,7 @@ using TypeGen.Core.Logging; using TypeGen.Core.SpecGeneration; using TypeGen.FileContentTest.Extensions; -using TypeGen.IntegrationTest.Extensions; +using TypeGen.IntegrationTest; using Xunit; using Xunit.Abstractions; @@ -22,6 +22,8 @@ public TestLogger(ITestOutputHelper output) this.output = output; } + public LogLevel MinLevel { get; set; } = LogLevel.Debug; + void ILogger.Log(string message, LogLevel level) { output.WriteLine(message); @@ -41,7 +43,6 @@ protected async Task TestFromAssembly(Type type, string expectedLocation) var readExpectedTask = EmbededResourceReader.GetEmbeddedResourceAsync(expectedLocation); GeneratorOptions options = new GeneratorOptions(); - options.FileNameConverters.Add(new AddFolderConverter()); var generator = new Generator(options, logger); var interceptor = GeneratorOutputInterceptor.CreateInterceptor(generator); diff --git a/src/TypeGen/TypeGen.FileContentTest/TsClassExtendsTsInterface/TsClassExtendsTsInterfaceTest.cs b/src/TypeGen/TypeGen.FileContentTest/TsClassExtendsTsInterface/TsClassExtendsTsInterfaceTest.cs index bb3400f1..be61b1bf 100644 --- a/src/TypeGen/TypeGen.FileContentTest/TsClassExtendsTsInterface/TsClassExtendsTsInterfaceTest.cs +++ b/src/TypeGen/TypeGen.FileContentTest/TsClassExtendsTsInterface/TsClassExtendsTsInterfaceTest.cs @@ -4,11 +4,14 @@ using TypeGen.FileContentTest.TestingUtils; using TypeGen.FileContentTest.TsClassExtendsTsInterface.Entities; using Xunit; +using Xunit.Abstractions; namespace TypeGen.FileContentTest.TsClassExtendsTsInterface; public class TsClassExtendsTsInterfaceTest : GenerationTestBase { + public TsClassExtendsTsInterfaceTest(ITestOutputHelper output) : base(output) { } + [Fact] public async Task TsClassExtendsTsInterface_Test() { diff --git a/src/TypeGen/TypeGen.FileContentTest/TsInterfaceInheritance/TsInterfaceInheritanceTest.cs b/src/TypeGen/TypeGen.FileContentTest/TsInterfaceInheritance/TsInterfaceInheritanceTest.cs index 0a898080..86050fd0 100644 --- a/src/TypeGen/TypeGen.FileContentTest/TsInterfaceInheritance/TsInterfaceInheritanceTest.cs +++ b/src/TypeGen/TypeGen.FileContentTest/TsInterfaceInheritance/TsInterfaceInheritanceTest.cs @@ -4,11 +4,14 @@ using TypeGen.FileContentTest.TestingUtils; using TypeGen.FileContentTest.TsInterfaceInheritance.Entities; using Xunit; +using Xunit.Abstractions; namespace TypeGen.FileContentTest.TsInterfaceInheritance; public class TsInterfaceInheritanceTest : GenerationTestBase { + public TsInterfaceInheritanceTest(ITestOutputHelper output) : base(output) { } + [Fact] public async Task cs_classes_which_are_ts_interfaces_should_respect_ts_interface_inheritance() { From ed29752e20ed3c2faf98553703c2eb9faa16c085 Mon Sep 17 00:00:00 2001 From: tterrag1098 Date: Thu, 14 Dec 2023 22:01:07 -0500 Subject: [PATCH 6/6] Fix issues with tests, add recursive default ctor support Also rename nuget ID to CIERA namespace --- nuget-dotnetcli/dotnet-typegen.nuspec | 2 +- nuget/TypeGen.nuspec | 4 +- .../Utils/FileSystemUtilsTest.cs | 14 +- .../Generator/Services/TsContentGenerator.cs | 124 ++++++++++-------- .../TypeGen.Core/Utils/FileSystemUtils.cs | 4 +- .../Entities/DefaultMemberValues.cs | 5 + .../Expected/default-member-values.ts | 2 + 7 files changed, 91 insertions(+), 64 deletions(-) diff --git a/nuget-dotnetcli/dotnet-typegen.nuspec b/nuget-dotnetcli/dotnet-typegen.nuspec index 9a054666..14f91e8b 100644 --- a/nuget-dotnetcli/dotnet-typegen.nuspec +++ b/nuget-dotnetcli/dotnet-typegen.nuspec @@ -1,7 +1,7 @@ - dotnet-typegen + CIERA.dotnet-typegen 5.0.0 Jacek Burzynski Jacek Burzynski diff --git a/nuget/TypeGen.nuspec b/nuget/TypeGen.nuspec index 13d49ad9..f73074ce 100644 --- a/nuget/TypeGen.nuspec +++ b/nuget/TypeGen.nuspec @@ -1,10 +1,10 @@ - TypeGen + CIERA.TypeGen 5.0.0 Jacek Burzynski - Jacek Burzynski + NCMEC LICENSE https://github.com/jburzynski/TypeGen https://raw.githubusercontent.com/jburzynski/type-gen/master/docs/icon.png diff --git a/src/TypeGen/TypeGen.Core.Test/Utils/FileSystemUtilsTest.cs b/src/TypeGen/TypeGen.Core.Test/Utils/FileSystemUtilsTest.cs index a31f9182..5087d996 100644 --- a/src/TypeGen/TypeGen.Core.Test/Utils/FileSystemUtilsTest.cs +++ b/src/TypeGen/TypeGen.Core.Test/Utils/FileSystemUtilsTest.cs @@ -21,14 +21,14 @@ public void SplitPathSeparator_PathGiven_PathSplit(string path) } [Theory] - [InlineData(@"path\to\file.txt", @"path\file.txt", @"../file.txt/")] - [InlineData("path/to/file.txt", "path/file.txt", @"../file.txt/")] - [InlineData("path/to/some/nested/file.txt", "path/file.txt", @"../../../file.txt/")] - [InlineData("path/to/some/nested", "path/file.txt", @"../../file.txt/")] + [InlineData(@"path\to\file.txt", @"path\file.txt", @"../file.txt")] + [InlineData("path/to/file.txt", "path/file.txt", @"../file.txt")] + [InlineData("path/to/some/nested/file.txt", "path/file.txt", @"../../../file.txt")] + [InlineData("path/to/some/nested", "path/file.txt", @"../../file.txt")] [InlineData("path/to/some/nested/", "path/", @"../../../")] - [InlineData(@"path\file.txt", "path/to/some/nested/file.txt", @"to/some/nested/file.txt/")] - [InlineData("path/files/file.txt", @"path\to\some\nested\file.txt", @"../to/some/nested/file.txt/")] - [InlineData("path/files/", @"path\to\some\nested\file.txt", @"../to/some/nested/file.txt/")] + [InlineData(@"path\file.txt", "path/to/some/nested/file.txt", @"to/some/nested/file.txt")] + [InlineData("path/files/file.txt", @"path\to\some\nested\file.txt", @"../to/some/nested/file.txt")] + [InlineData("path/files/", @"path\to\some\nested\file.txt", @"../to/some/nested/file.txt")] public void GetPathDiff_Test(string pathFrom, string pathTo, string expectedResult) { string actualResult = FileSystemUtils.GetPathDiff(pathFrom, pathTo); diff --git a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs index ae2e7ca9..ec9877bf 100644 --- a/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs +++ b/src/TypeGen/TypeGen.Core/Generator/Services/TsContentGenerator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -71,10 +72,10 @@ public TsContentGenerator(ITypeDependencyService typeDependencyService, public string GetImportsText(Type type, string outputDir) { Requires.NotNull(type, nameof(type)); - + if (GeneratorOptions.FileNameConverters == null) throw new InvalidOperationException($"{nameof(GeneratorOptions.FileNameConverters)} should not be null."); - + if (GeneratorOptions.TypeNameConverters == null) throw new InvalidOperationException($"{nameof(GeneratorOptions.TypeNameConverters)} should not be null."); @@ -98,7 +99,7 @@ public string GetExtendsForClassesText(Type type) { Requires.NotNull(type, nameof(type)); Requires.NotNull(GeneratorOptions.TypeNameConverters, nameof(GeneratorOptions.TypeNameConverters)); - + Type baseType = _typeService.GetBaseType(type); if (baseType == null) return ""; @@ -139,7 +140,7 @@ public string GetImplementsText(Type type) IEnumerable baseTypeNames = implementedInterfaces.Select(baseType => _typeService.GetTsTypeName(baseType, true)); return _templateService.GetImplementsText(baseTypeNames); } - + /// /// Returns TypeScript imports source code related to type dependencies /// @@ -148,7 +149,7 @@ public string GetImplementsText(Type type) /// private string GetTypeDependencyImportsText(Type type, string outputDir) { - if (!string.IsNullOrEmpty(outputDir) && !outputDir.EndsWith("/") && !outputDir.EndsWith("\\")) outputDir += "\\"; + if (!string.IsNullOrEmpty(outputDir) && !outputDir.EndsWith("/") && !outputDir.EndsWith("\\")) outputDir += "/"; var result = ""; IEnumerable typeDependencies = _typeDependencyService.GetTypeDependencies(type); @@ -158,14 +159,14 @@ private string GetTypeDependencyImportsText(Type type, string outputDir) typeDependencies = typeDependencies.Where(td => !td.IsBase); } - var startFilePath = GeneratorOptions.FileNameConverters.Convert(type.Name.RemoveTypeArity(), type); + var startFilePath = GeneratorOptions.FileNameConverters.Convert(type.Name.RemoveTypeArity(), type)?.Replace("\\", "/"); var startFileName = startFilePath; - var startOutputDir = outputDir == null ? startFilePath : Path.Combine(outputDir, startFilePath); + var startOutputDir = outputDir == null ? startFilePath : outputDir.EnsurePostfix("/") + startFilePath; if (startOutputDir.IndexOf('/') != -1) { startFileName = startOutputDir.Substring(startOutputDir.LastIndexOf('/') + 1); - startOutputDir = startOutputDir.Remove(startOutputDir.LastIndexOf('/')); + startOutputDir = startOutputDir.Remove(startOutputDir.LastIndexOf('/') + 1); } else { @@ -181,12 +182,12 @@ private string GetTypeDependencyImportsText(Type type, string outputDir) string endFilePath = GeneratorOptions.FileNameConverters.Convert(typeDependencyName, typeDependency); string endFileName = endFilePath; - var endOutputDir = GetTypeDependencyOutputDir(typeDependencyInfo, outputDir); - endOutputDir = endOutputDir == null ? endFilePath : Path.Combine(endOutputDir, endFilePath); + var endOutputDir = GetTypeDependencyOutputDir(typeDependencyInfo, outputDir)?.Replace("\\", "/"); + endOutputDir = endOutputDir == null ? endFilePath : endOutputDir.EnsurePostfix("/") + endFilePath; if (endOutputDir.IndexOf('/') != -1) { endFileName = endOutputDir.Substring(endOutputDir.LastIndexOf('/') + 1); - endOutputDir = endOutputDir.Remove(endOutputDir.LastIndexOf('/')); + endOutputDir = endOutputDir.Remove(endOutputDir.LastIndexOf('/') + 1); } else { @@ -198,11 +199,11 @@ private string GetTypeDependencyImportsText(Type type, string outputDir) pathDiff = pathDiff.StartsWith("../") ? pathDiff : $"./{pathDiff}"; _logger?.Log($"{startOutputDir} -> {endOutputDir} = {pathDiff}", LogLevel.Info); // get file path - string dependencyPath = Path.Combine(pathDiff.EnsurePostfix("/"), endFileName); + string dependencyPath = pathDiff.EnsurePostfix("/") + endFileName; dependencyPath = dependencyPath.Replace('\\', '/'); string typeName = GeneratorOptions.TypeNameConverters.Convert(typeDependencyName, typeDependency); - + result += _typeService.UseDefaultExport(typeDependency) ? _templateService.FillImportDefaultExportTemplate(typeName, dependencyPath, GeneratorOptions.UseImportType) : _templateService.FillImportTemplate(typeName, "", dependencyPath, GeneratorOptions.UseImportType); @@ -268,8 +269,8 @@ private string FillCustomImportTemplate(string typeName, string importPath, stri string name = withOriginalTypeName ? originalTypeName : typeName; string typeAlias = withOriginalTypeName ? typeName : null; - - return isDefaultExport ? _templateService.FillImportDefaultExportTemplate(name, importPath, GeneratorOptions.UseImportType) : + + return isDefaultExport ? _templateService.FillImportDefaultExportTemplate(name, importPath, GeneratorOptions.UseImportType) : _templateService.FillImportTemplate(name, typeAlias, importPath, GeneratorOptions.UseImportType); } @@ -309,7 +310,7 @@ private string GetTypeDependencyOutputDir(TypeDependencyInfo typeDependencyInfo, public string GetCustomBody(string filePath, int indentSize) { Requires.NotNull(filePath, nameof(filePath)); - + string content = _tsContentParser.GetTagContent(filePath, indentSize, KeepTsTagName, CustomBodyTagName); string tab = StringUtils.GetTabText(indentSize); @@ -327,7 +328,7 @@ public string GetCustomBody(string filePath, int indentSize) public string GetCustomHead(string filePath) { Requires.NotNull(filePath, nameof(filePath)); - + string content = _tsContentParser.GetTagContent(filePath, 0, CustomHeadTagName); return string.IsNullOrEmpty(content) ? "" @@ -378,46 +379,13 @@ public string GetMemberValueText(MemberInfo memberInfo, bool isOptional, string? else valueObj = isOptional ? null : defaultValueForType; - if (valueObj == null) return null; - valueType = valueObj.GetType(); - string memberType = _typeService.GetTsTypeName(memberInfo).GetTsTypeUnion(0); - string quote = GeneratorOptions.SingleQuotes ? "'" : "\""; - - switch (valueObj) - { - case Guid valueGuid when memberType == "string": - return quote + valueGuid + quote; - case DateTime valueDateTime when memberType == "Date": - return $@"new Date({quote}{valueDateTime.ToString("o", CultureInfo.InvariantCulture)}{quote})"; - case DateTime valueDateTime when memberType == "string": - return quote + valueDateTime.ToString("o", CultureInfo.InvariantCulture) + quote; - case DateTimeOffset valueDateTimeOffset when memberType == "Date": - return $@"new Date({quote}{valueDateTimeOffset.ToString("o", CultureInfo.InvariantCulture)}{quote})"; - case DateTimeOffset valueDateTimeOffset when memberType == "string": - return quote + valueDateTimeOffset.ToString("o", CultureInfo.InvariantCulture) + quote; - default: - var serializedValue = JsonConvert.SerializeObject(valueObj, _jsonSerializerSettings).Replace("\"", quote); - if (!_typeService.IsCollectionType(valueType) && - !_typeService.IsDictionaryType(valueType) && - _typeService.IsTsClass(valueType) && // Make sure it's not a list, array, or other special type - !valueType.GetTypeInfo().IsValueType && // Ignore value types - valueType.GetConstructor(Type.EmptyTypes) != null) // Make sure the type has a default constructor to use for this - { - //_logger?.Log($"Checking type {valueType.FullName} for constructor usage", LogLevel.Info); - var defaultCtorValueType = Activator.CreateInstance(valueType); - if (defaultCtorValueType != null && memberwiseEquals(valueObj, defaultCtorValueType)) - { - return $@"new {_typeService.GetTsTypeName(memberInfo)}()"; - } - } - return serializedValue; - } + return SerializeObjectToTs(valueObj, memberInfo); } catch (MissingMethodException e) { _logger?.Log($"Cannot determine the default value for member '{memberInfo.DeclaringType.FullName}.{memberInfo.Name}', because type '{memberInfo.DeclaringType.FullName}' has no default constructor.", LogLevel.Debug); } - catch (ArgumentException e) when(e.InnerException is TypeLoadException) + catch (ArgumentException e) when (e.InnerException is TypeLoadException) { _logger?.Log($"Cannot determine the default value for member '{memberInfo.DeclaringType.FullName}.{memberInfo.Name}', because type '{memberInfo.DeclaringType.FullName}' has generic parameters with base class or interface constraints.", LogLevel.Debug); } @@ -429,6 +397,58 @@ public string GetMemberValueText(MemberInfo memberInfo, bool isOptional, string? return fallback; } + private string SerializeObjectToTs(object valueObj) + { + return SerializeObjectToTs(valueObj, null); + } + + private string SerializeObjectToTs(object valueObj, MemberInfo memberInfo) + { + if (valueObj == null) return null; + var valueType = valueObj.GetType(); + string memberType = memberInfo == null ? _typeService.GetTsTypeName(valueType).GetTsTypeUnion(0) : _typeService.GetTsTypeName(memberInfo).GetTsTypeUnion(0); + string quote = GeneratorOptions.SingleQuotes ? "'" : "\""; + + switch (valueObj) + { + case Guid valueGuid when memberType == "string": + return quote + valueGuid + quote; + case DateTime valueDateTime when memberType == "Date": + return $@"new Date({quote}{valueDateTime.ToString("o", CultureInfo.InvariantCulture)}{quote})"; + case DateTime valueDateTime when memberType == "string": + return quote + valueDateTime.ToString("o", CultureInfo.InvariantCulture) + quote; + case DateTimeOffset valueDateTimeOffset when memberType == "Date": + return $@"new Date({quote}{valueDateTimeOffset.ToString("o", CultureInfo.InvariantCulture)}{quote})"; + case DateTimeOffset valueDateTimeOffset when memberType == "string": + return quote + valueDateTimeOffset.ToString("o", CultureInfo.InvariantCulture) + quote; + case IEnumerable valueEnumerable when _typeService.IsCollectionType(valueType) && valueEnumerable.GetEnumerator().MoveNext(): + return $"[ {string.Join(", ", valueEnumerable.Cast().Select(SerializeObjectToTs))} ]"; + case IEnumerable valueDictEnumerable when _typeService.IsDictionaryType(valueType) && valueDictEnumerable.GetEnumerator().MoveNext(): + return $"{{ {string.Join(", ", valueDictEnumerable.Cast().Select(SerializeObjectToTs))} }}"; + case DictionaryEntry valueDictEntry: + return $"{SerializeObjectToTs(valueDictEntry.Key)}: {SerializeObjectToTs(valueDictEntry.Value)}"; + case var _ when valueType.IsGenericType && typeof(KeyValuePair<,>).IsAssignableFrom(valueType.GetGenericTypeDefinition()): + dynamic pair = valueObj; + return $"{SerializeObjectToTs(pair.Key)}: {SerializeObjectToTs(pair.Value)}"; + default: + var serializedValue = JsonConvert.SerializeObject(valueObj, _jsonSerializerSettings).Replace("\"", quote); + if (!_typeService.IsCollectionType(valueType) && + !_typeService.IsDictionaryType(valueType) && + _typeService.IsTsClass(valueType) && // Make sure it's not a list, array, or other special type + !valueType.GetTypeInfo().IsValueType && // Ignore value types + valueType.GetConstructor(Type.EmptyTypes) != null) // Make sure the type has a default constructor to use for this + { + //_logger?.Log($"Checking type {valueType.FullName} for constructor usage", LogLevel.Info); + var defaultCtorValueType = Activator.CreateInstance(valueType); + if (defaultCtorValueType != null && memberwiseEquals(valueObj, defaultCtorValueType)) + { + return $@"new {_typeService.GetTsTypeName(valueType)}()"; + } + } + return serializedValue; + } + } + private bool memberwiseEquals(object a, object b) { if (a == b || a.Equals(b)) return true; diff --git a/src/TypeGen/TypeGen.Core/Utils/FileSystemUtils.cs b/src/TypeGen/TypeGen.Core/Utils/FileSystemUtils.cs index a538b222..4268b196 100644 --- a/src/TypeGen/TypeGen.Core/Utils/FileSystemUtils.cs +++ b/src/TypeGen/TypeGen.Core/Utils/FileSystemUtils.cs @@ -34,8 +34,8 @@ public static string[] SplitPathSeparator(string path) /// public static string GetPathDiff(string pathFrom, string pathTo) { - var pathFromUri = new Uri("file:///root/" + pathFrom?.Replace('\\', '/').EnsurePostfix("/")); - var pathToUri = new Uri("file:///root/" + pathTo?.Replace('\\', '/').EnsurePostfix("/")); + var pathFromUri = new Uri("file:///root/" + pathFrom?.Replace('\\', '/')); + var pathToUri = new Uri("file:///root/" + pathTo?.Replace('\\', '/')); return pathFromUri.MakeRelativeUri(pathToUri).ToString(); } diff --git a/src/TypeGen/TypeGen.FileContentTest/CommonCases/Entities/DefaultMemberValues.cs b/src/TypeGen/TypeGen.FileContentTest/CommonCases/Entities/DefaultMemberValues.cs index 17cc141a..a2ad729d 100644 --- a/src/TypeGen/TypeGen.FileContentTest/CommonCases/Entities/DefaultMemberValues.cs +++ b/src/TypeGen/TypeGen.FileContentTest/CommonCases/Entities/DefaultMemberValues.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using TypeGen.Core.TypeAnnotations; namespace TypeGen.FileContentTest.CommonCases.Entities @@ -22,5 +23,9 @@ public class DefaultMemberValues public DefaultMemberComplexValues PropertyComplexDefaultValue { get; set; } = new(); public DefaultMemberComplexValues PropertyComplexNotDefaultValue { get; set; } = new() { Number = 4 }; + + public List PropertyListOfComplexDefaultValue { get; set; } = new() { new() }; + + public Dictionary PropertyDictOfComplexDefaultValue { get; set; } = new() { { "key", new() } }; } } \ No newline at end of file diff --git a/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/default-member-values.ts b/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/default-member-values.ts index 60199951..91ae628b 100644 --- a/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/default-member-values.ts +++ b/src/TypeGen/TypeGen.FileContentTest/CommonCases/Expected/default-member-values.ts @@ -15,4 +15,6 @@ export class DefaultMemberValues { static staticPropertyString: string = "StaticPropertyString"; propertyComplexDefaultValue: DefaultMemberComplexValues = new DefaultMemberComplexValues(); propertyComplexNotDefaultValue: DefaultMemberComplexValues = {"number":4,"numberNull":null,"string":"default","stringNull":null}; + propertyListOfComplexDefaultValue: DefaultMemberComplexValues[] = [ new DefaultMemberComplexValues() ]; + propertyDictOfComplexDefaultValue: { [key: string]: DefaultMemberComplexValues; } = { "key": new DefaultMemberComplexValues() }; }