Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions src/TypeGen/TypeGen.Core/Generator/Generator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using TypeGen.Core.Conversion;
using TypeGen.Core.Converters;
using TypeGen.Core.Extensions;
using TypeGen.Core.Generator.Context;
using TypeGen.Core.Generator.Services;
Expand Down Expand Up @@ -428,6 +431,9 @@ private IEnumerable<string> GenerateClass(Type type, ExportTsClassAttribute clas
}

string importsText = _tsContentGenerator.GetImportsText(type, outputDir);

string constructorsText = GetClassConstructorsText(type);

string propertiesText = GetClassPropertiesText(type);

// generate the file content
Expand All @@ -445,8 +451,8 @@ private IEnumerable<string> 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, tsDoc, customHead, customBody, constructorsText, Options.FileHeading) :
_templateService.FillClassTemplate(importsText, tsTypeName, extendsText, implementsText, propertiesText, tsDoc, customHead, customBody, constructorsText, Options.FileHeading);

// write TypeScript file
FileContentGenerated?.Invoke(this, new FileContentGeneratedArgs(type, filePath, content));
Expand Down Expand Up @@ -644,6 +650,78 @@ private void LogClassPropertyWarnings(MemberInfo memberInfo)
Logger.Log($"TsOptionalAttribute used for a class property ({memberInfo.DeclaringType?.FullName}.{memberInfo.Name}). The attribute will be ignored.", LogLevel.Warning);
}

private string GetClassConstructorsText(Type type)
{
var constructorsText = "";
var constructors = type.GetConstructors();

//var isRecord = ((TypeInfo)type).DeclaredProperties.FirstOrDefault(x => x.Name == "EqualityContract")?.GetMethod?.GetCustomAttribute(typeof(CompilerGeneratedAttribute)) is object;

var classHasTsConstructorAttribute = type.GetCustomAttributes(typeof(TsConstructorAttribute)).Any();

var members = type.GetTsExportableMembers(_metadataReaderFactory.GetInstance()).Select(memberInfo =>
{
var nameAttribute = _metadataReaderFactory.GetInstance().GetAttribute<TsMemberNameAttribute>(memberInfo);
var memberName = nameAttribute?.Name ?? Options.PropertyNameConverters.Convert(memberInfo.Name, memberInfo);
var typeName = _typeService.GetTsTypeName(memberInfo);
Type memberType = null;

switch (memberInfo.MemberType)
{
case MemberTypes.Field:
memberType = ((FieldInfo)memberInfo).FieldType;
break;
case MemberTypes.Property:
memberType = ((PropertyInfo)memberInfo).PropertyType;
break;
}

return new TsMemberInfo(memberInfo.Name, memberType, memberName, typeName);
}).ToArray();

//if ctor params all match member names and types or is a Record
var matchedConstructors = constructors.Where(ctor =>
(classHasTsConstructorAttribute || ctor.GetCustomAttributes(typeof(TsConstructorAttribute)).Any()) &&
ctor.GetParameters().All(p => members.Any(m => m.DotNetMemberName.Equals(p.Name, StringComparison.InvariantCultureIgnoreCase) && m.DotNetMemberType == p.ParameterType))).ToArray();

//if single empty constructor, skip
if (matchedConstructors.Length == 1 && matchedConstructors[0].GetParameters().Length == 0)
{
return constructorsText;
}

constructorsText += matchedConstructors.Aggregate(constructorsText, (current, ctorInfo) => current + GetClassConstructorText(ctorInfo, members));

return constructorsText;
}

private string GetClassConstructorText(ConstructorInfo ctor, TsMemberInfo[] tsMemberTypes)
{
string argumentsText = "";
string assignmentsText = "";

var parameters = ctor.GetParameters();

foreach (var param in parameters)
{
var matchingMemberInfo = tsMemberTypes.FirstOrDefault(k => k.DotNetMemberName.Equals(param.Name, StringComparison.InvariantCultureIgnoreCase));

if(matchingMemberInfo == null)
{
//not all parameters have a matching member name, skip constructor building
return string.Empty;
}

argumentsText += _templateService.FillClassConstructorArgumentTemplate(matchingMemberInfo.TsMemberName, matchingMemberInfo.TsMemberType);
assignmentsText += _templateService.FillClassConstructorAssignmentTemplate(matchingMemberInfo.TsMemberName, matchingMemberInfo.TsMemberName);
}

argumentsText = argumentsText.TrimEnd(',', ' ');
assignmentsText = RemoveLastLineEnding(assignmentsText);

return _templateService.FillClassConstructorTemplate(argumentsText, assignmentsText);
}

/// <summary>
/// Gets TypeScript class properties definition source code
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/TypeGen/TypeGen.Core/Generator/GeneratorOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using TypeGen.Core.Converters;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 tsDoc, string customHead, string customBody, string constructorsText, string fileHeading = null);
string FillClassDefaultExportTemplate(string imports, string name, string exportName, string extends, string implements, string properties, string tsDoc, string customHead, string customBody, string constructorsText, string fileHeading = null);
string FillClassPropertyTemplate(string modifiers, string name, string type, IEnumerable<string> typeUnions, bool isOptional, string tsDoc, string defaultValue = null);
string FillInterfaceTemplate(string imports, string name, string extends, string properties, string tsDoc, string customHead, string customBody, string fileHeading = null);
string FillInterfaceDefaultExportTemplate(string imports, string name, string exportName, string extends, string properties, string tsDoc, string customHead, string customBody, string fileHeading = null);
Expand All @@ -21,5 +21,8 @@ internal interface ITemplateService
string GetExtendsText(string name);
string GetExtendsText(IEnumerable<string> names);
string GetImplementsText(IEnumerable<string> names);
string FillClassConstructorTemplate(string arguments, string assignments);
string FillClassConstructorArgumentTemplate(string argumentName, string argumentType);
string FillClassConstructorAssignmentTemplate(string memberName, string argumentName);
}
}
37 changes: 33 additions & 4 deletions src/TypeGen/TypeGen.Core/Generator/Services/TemplateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ internal class TemplateService : ITemplateService
private readonly string _indexTemplate;
private readonly string _indexExportTemplate;
private readonly string _headingTemplate;
private readonly string _classConstructorTemplate;
private readonly string _classConstructorArgumentTemplate;
private readonly string _classConstructorAssignmentTemplate;

private GeneratorOptions GeneratorOptions => _generatorOptionsProvider.GeneratorOptions;

Expand All @@ -58,10 +61,13 @@ public TemplateService(IInternalStorage internalStorage, IGeneratorOptionsProvid
_indexTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.Index.tpl");
_indexExportTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.IndexExport.tpl");
_headingTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.Heading.tpl");
_classConstructorTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.ClassConstructor.tpl");
_classConstructorArgumentTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.ClassConstructorArgument.tpl");
_classConstructorAssignmentTemplate = _internalStorage.GetEmbeddedResource("TypeGen.Core.Templates.ClassConstructorAssignment.tpl");
}

public string FillClassTemplate(string imports, string name, string extends, string implements, string properties,
string tsDoc, string customHead, string customBody, string fileHeading = null)
string tsDoc, string customHead, string customBody, string constructorsText, string fileHeading = null)
{
if (fileHeading == null) fileHeading = _headingTemplate;

Expand All @@ -74,11 +80,12 @@ public string FillClassTemplate(string imports, string name, string extends, str
.Replace(GetTag("tsDoc"), tsDoc)
.Replace(GetTag("customHead"), customHead)
.Replace(GetTag("customBody"), customBody)
.Replace(GetTag("fileHeading"), fileHeading);
.Replace(GetTag("fileHeading"), fileHeading)
.Replace(GetTag("constructors"), constructorsText);
}

public string FillClassDefaultExportTemplate(string imports, string name, string exportName, string extends, string implements,
string properties, string tsDoc, string customHead, string customBody, string fileHeading = null)
string properties, string tsDoc, string customHead, string customBody, string constructorsText, string fileHeading = null)
{
if (fileHeading == null) fileHeading = _headingTemplate;

Expand All @@ -92,7 +99,8 @@ public string FillClassDefaultExportTemplate(string imports, string name, string
.Replace(GetTag("tsDoc"), tsDoc)
.Replace(GetTag("customHead"), customHead)
.Replace(GetTag("customBody"), customBody)
.Replace(GetTag("fileHeading"), fileHeading);
.Replace(GetTag("fileHeading"), fileHeading)
.Replace(GetTag("constructors"), constructorsText);
}

public string FillClassPropertyTemplate(string modifiers, string name, string type, IEnumerable<string> typeUnions,
Expand Down Expand Up @@ -236,6 +244,27 @@ public string FillIndexExportTemplate(string filename)
return ReplaceSpecialChars(_indexExportTemplate)
.Replace(GetTag("filename"), filename);
}

public string FillClassConstructorTemplate(string arguments, string assignments)
{
return ReplaceSpecialChars(_classConstructorTemplate)
.Replace(GetTag("arguments"), arguments)
.Replace(GetTag("assignments"), assignments);
}

public string FillClassConstructorArgumentTemplate(string argumentName, string argumentType)
{
return ReplaceSpecialChars(_classConstructorArgumentTemplate)
.Replace(GetTag("argName"), argumentName)
.Replace(GetTag("argType"), argumentType);
}

public string FillClassConstructorAssignmentTemplate(string memberName, string argumentName)
{
return ReplaceSpecialChars(_classConstructorAssignmentTemplate)
.Replace(GetTag("memberName"), memberName)
.Replace(GetTag("argumentName"), argumentName);
}

public string GetExtendsText(string name) => $" extends {name}";
public string GetExtendsText(IEnumerable<string> names) => $" extends {string.Join(", ", names)}";
Expand Down
5 changes: 5 additions & 0 deletions src/TypeGen/TypeGen.Core/Generator/TsMemberInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System;

namespace TypeGen.Core.Generator;

public record TsMemberInfo(string DotNetMemberName, Type DotNetMemberType, string TsMemberName, string TsMemberType);
2 changes: 1 addition & 1 deletion src/TypeGen/TypeGen.Core/Templates/Class.tpl
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
$tg{fileHeading}$tg{imports}$tg{customHead}$tg{tsDoc}export class $tg{name}$tg{extends}$tg{implements} {
$tg{properties}$tg{customBody}
$tg{constructors}$tg{properties}$tg{customBody}
}
3 changes: 3 additions & 0 deletions src/TypeGen/TypeGen.Core/Templates/ClassConstructor.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
$tg{tab}constructor($tg{arguments}){
$tg{assignments}
$tg{tab}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$tg{argName}: $tg{argType},
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$tg{tab}$tg{tab}this.$tg{memberName}: $tg{argumentName};
2 changes: 1 addition & 1 deletion src/TypeGen/TypeGen.Core/Templates/ClassDefaultExport.tpl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
$tg{fileHeading}$tg{imports}$tg{customHead}$tg{tsDoc}class $tg{name}$tg{extends}$tg{implements} {
$tg{properties}$tg{customBody}
$tg{constructors}$tg{properties}$tg{customBody}
}
export default $tg{exportName};
19 changes: 19 additions & 0 deletions src/TypeGen/TypeGen.Core/TypeAnnotations/TsConstructorAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace TypeGen.Core.TypeAnnotations;

/// <summary>
/// Identifies a TypeScript class constructor.
/// Constructor implementation will be generated if empty or if the arguments match members of the class.
/// ie: ctor(string name) => public string Name { get; set; }
/// generated as :
/// constructor(name: string)
/// {
/// this.Name: name;
/// }
/// </summary>
[AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Class)]
public class TsConstructorAttribute: Attribute
{

}
10 changes: 8 additions & 2 deletions src/TypeGen/TypeGen.Core/TypeGen.Core.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net7.0</TargetFrameworks>
<DocumentationFile>TypeGen.Core.xml</DocumentationFile>
Expand All @@ -21,6 +21,9 @@
<None Remove="Templates\InterfaceProperty.tpl" />
<None Remove="Templates\Index.tpl" />
<None Remove="Templates\IndexExport.tpl" />
<None Remove="Templates\ClassConstructor.tpl" />
<None Remove="Templates\ClassConstructorAssigment.tpl" />
<None Remove="Templates\ClassConstructorArgument.tpl" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Templates\Class.tpl" />
Expand All @@ -39,7 +42,10 @@
<EmbeddedResource Include="Templates\InterfaceDefaultExport.tpl" />
<EmbeddedResource Include="Templates\InterfaceProperty.tpl" />
<EmbeddedResource Include="Templates\Index.tpl" />
<EmbeddedResource Include="Templates\IndexExport.tpl" />
<EmbeddedResource Include="Templates\IndexExport.tpl" />
<EmbeddedResource Include="Templates\ClassConstructor.tpl" />
<EmbeddedResource Include="Templates\ClassConstructorAssignment.tpl" />
<EmbeddedResource Include="Templates\ClassConstructorArgument.tpl" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Namotion.Reflection" Version="2.1.1" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
using TypeGen.FileContentTest.TestingUtils;
using Xunit;

namespace TypeGen.FileContentTest.Constructor;

public class ConstructorGenerationTests: GenerationTestBase
{
[Theory]
[InlineData(typeof(Entities.ClassWithConstructorMatchingMembers), "TypeGen.FileContentTest.Constructor.Expected.test-constructors.ts")]
[InlineData(typeof(Entities.ClassWithConstructorMatchingMembersMismatch), "TypeGen.FileContentTest.Constructor.Expected.test-constructors-mismatch.ts")]
[InlineData(typeof(Entities.ClassWithConstructorMatchingMembersAndEmpty), "TypeGen.FileContentTest.Constructor.Expected.test-empty-constructors.ts")]
[InlineData(typeof(Entities.MyRecord), "TypeGen.FileContentTest.Constructor.Expected.test-record-constructors.ts")]
public async Task TestConstructor(Type type, string expectedLocation)
{
await TestFromAssembly(type, expectedLocation);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
using TypeGen.Core.TypeAnnotations;
using Xunit;

namespace TypeGen.FileContentTest.Constructor.Entities;

[ExportTsClass]
public class ClassWithConstructorMatchingMembers
{
public string MyProp { get; set; }
public int myField;
public SubClass SubClass { get; set; }

[TsConstructor]
public ClassWithConstructorMatchingMembers(string myProp, int myField, SubClass subClass)
{ }
}

[ExportTsClass]
public class ClassWithConstructorMatchingMembersMismatch
{
public string MyProp { get; set; }
public int myField;
public SubClass SubClass { get; set; }

[TsConstructor]
public ClassWithConstructorMatchingMembersMismatch(string myProp1, int myField, SubClass subClass)
{ }
}

[ExportTsClass]
[TsConstructor]
public class ClassWithConstructorMatchingMembersAndEmpty
{
public string MyProp { get; set; }
public int myField;
public SubClass SubClass { get; set; }

public ClassWithConstructorMatchingMembersAndEmpty(string myProp, int myField, SubClass subClass)
{ }

public ClassWithConstructorMatchingMembersAndEmpty()
{ }
}

[ExportTsClass]
[TsConstructor]
public record MyRecord(string Name);

public class SubClass
{
public string Name { get; set; }

public SubClass(string name)
{
Name = name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* This is a TypeGen auto-generated file.
* Any changes made to this file can be lost when this file is regenerated.
*/

import { SubClass } from "./sub-class";

export class ClassWithConstructorMatchingMembersMismatch {
myField: number;
myProp: string;
subClass: SubClass;
}
Loading