Skip to content

Commit

Permalink
feat(lib): Improve handling of types with generic arguments #123
Browse files Browse the repository at this point in the history
  • Loading branch information
PerfectlyNormal committed Jan 14, 2025
1 parent 36202a1 commit 8a9e7b2
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 26 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

### Added

- Better support for generic types (#123)

## [0.16.0] - 2024-12-17

### Added
Expand Down
32 changes: 32 additions & 0 deletions TypeContractor.Tests/TypeScript/TypeScriptConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,26 @@ public void Handles_Nullable_Records_Inside_Other_Records()
second.IsNullable.Should().BeTrue();
}

[Fact]
public void Handles_Generics()
{
var result = Sut.Convert(typeof(ResponseWithOverrides));

result.Should().NotBeNull();
result.Properties.Should().NotBeNull();
result.Properties!.Should().HaveCount(2);
result.Properties!.First().DestinationType.Should().Be("Overridable<string>");
result.Properties!.Last().DestinationType.Should().Be("Overridable<boolean>");

Sut.CustomMappedTypes.Should().ContainSingle();
var overridableType = Sut.CustomMappedTypes.First().Value;
overridableType.Properties.Should().HaveCount(2);
overridableType.Properties!.First().DestinationName.Should().Be("value");
overridableType.Properties!.First().DestinationType.Should().Be("T");
overridableType.Properties!.Last().DestinationName.Should().Be("isOverridden");
overridableType.Properties!.Last().DestinationType.Should().Be("boolean");
}

#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
private record TopLevelRecord(string Name, SecondStoryRecord? SecondStoryRecord);
private record SecondStoryRecord(string Description, SomeOtherDeeplyNestedRecord? SomeOtherDeeplyNestedRecord);
Expand Down Expand Up @@ -466,6 +486,18 @@ private class TimeOnlyResponse
public TimeOnly MeetingTime { get; set; }
}

private class Overridable<T>
{
public T? Value { get; set; }
public bool IsOverridden { get; set; }
}

private class ResponseWithOverrides
{
public Overridable<string> Name { get; set; }
public Overridable<bool?> SomeBool { get; set; }
}

#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

private MetadataLoadContext BuildMetadataLoadContext()
Expand Down
106 changes: 103 additions & 3 deletions TypeContractor.Tests/TypeScript/TypeScriptWriterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class TypeScriptWriterTests : IDisposable
public TypeScriptWriterTests()
{
var assembly = typeof(TypeScriptWriterTests).Assembly;
_outputDirectory = Directory.CreateTempSubdirectory();
_outputDirectory = new DirectoryInfo("C:\\Users\\per.christian.bechst\\AppData\\Local\\Temp\\gtp5vhmw.mgi\\TypeContractor\\Tests\\TypeScript");
_configuration = TypeContractorConfiguration
.WithDefaultConfiguration()
.AddAssembly(assembly.FullName!, assembly.Location)
Expand Down Expand Up @@ -48,6 +48,80 @@ public void Can_Write_Simple_Types()
.And.Contain("someObject: any;");
}

[Fact]
public void Can_Write_Generic_Types()
{
// Arrange
var outputTypes = BuildOutputTypes(typeof(ResponseWithOverrides));

// Act
var responseResult = Sut.Write(outputTypes.First(x => x.Name == "ResponseWithOverrides"), outputTypes, true);
var overrideResult = Sut.Write(outputTypes.First(x => x.Name == "Overridable"), outputTypes, true);

// Assert
var responseFile = File.ReadAllLines(responseResult).Select(x => x.TrimStart());
responseFile.Should()
.NotBeEmpty()
.And.Contain("import { Overridable, OverridableSchema } from './Overridable';")

.And.Contain("export interface ResponseWithOverrides {")
.And.Contain("name: Overridable<string>;")
.And.Contain("someBool: Overridable<boolean>;")

.And.Contain("export const ResponseWithOverridesSchema = z.object({")
.And.Contain("name: OverridableSchema,")
.And.Contain("someBool: OverridableSchema,");

var overrideFile = File.ReadAllLines(overrideResult).Select(x => x.TrimStart());
overrideFile.Should()
.NotBeEmpty()
.And.Contain("export interface Overridable<T> {")
.And.Contain("value?: T;")
.And.Contain("isOverridden: boolean;")

.And.Contain("export const OverridableSchema = z.object({")
.And.Contain("value: z.any().nullable(),")
.And.Contain("isOverridden: z.boolean(),");
}

[Fact]
public void Only_Includes_Relevant_Zod_Schemas()
{
// Arrange
var outputTypes = BuildOutputTypes(typeof(ResponseWithOverridableCustomType));

// Act
var responseResult = Sut.Write(outputTypes.First(x => x.Name == "ResponseWithOverridableCustomType"), outputTypes, true);
var overrideResult = Sut.Write(outputTypes.First(x => x.Name == "Overridable"), outputTypes, true);
var requestResult = Sut.Write(outputTypes.First(x => x.Name == "MyCustomRequest"), outputTypes, true);

// Assert
var responseFile = File.ReadAllLines(responseResult).Select(x => x.TrimStart());
var overrideFile = File.ReadAllLines(overrideResult).Select(x => x.TrimStart());
var requestFile = File.ReadAllLines(requestResult).Select(x => x.TrimStart());

responseFile.Should()
.NotBeEmpty()
.And.Contain("import { Overridable, OverridableSchema } from './Overridable';")
.And.Contain("import { MyCustomRequest } from './MyCustomRequest';")

.And.Contain("export interface ResponseWithOverridableCustomType {")
.And.Contain("nestedRequest: Overridable<MyCustomRequest>;")

.And.Contain("export const ResponseWithOverridableCustomTypeSchema = z.object({")
.And.Contain("nestedRequest: OverridableSchema,");

overrideFile.Should()
.NotBeEmpty()
.And.Contain("export interface Overridable<T> {")
.And.Contain("value?: T;")
.And.Contain("isOverridden: boolean;")

.And.Contain("export const OverridableSchema = z.object({")
.And.Contain("value: z.any().nullable(),")
.And.Contain("isOverridden: z.boolean(),");
}

[Fact]
public void Handles_Dictionary_With_Complex_Values()
{
Expand Down Expand Up @@ -479,7 +553,33 @@ private MetadataLoadContext BuildMetadataLoadContext()

public void Dispose()
{
if (_outputDirectory.Exists)
_outputDirectory.Delete(true);
//if (_outputDirectory.Exists)
// _outputDirectory.Delete(true);
}
}

#region Test input
#pragma warning disable CS8618
public class Overridable<T>
{
public T? Value { get; set; }
public bool IsOverridden { get; set; }
}

public class ResponseWithOverrides
{
public Overridable<string> Name { get; set; }
public Overridable<bool?> SomeBool { get; set; }
}

public class ResponseWithOverridableCustomType
{
public Overridable<MyCustomRequest> NestedRequest { get; set; }
}

public class MyCustomRequest
{
public string Name { get; set; }
}
#pragma warning restore CS8618
#endregion
25 changes: 23 additions & 2 deletions TypeContractor/Output/DestinationType.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
namespace TypeContractor.Output;

public record DestinationType(string TypeName, string? FullName, string ImportType, bool IsBuiltin, bool IsArray, bool IsReadonly, bool IsNullable, Type? InnerType)
public record DestinationType(
string TypeName,
string? FullName,
string ImportType,
bool IsBuiltin,
bool IsArray,
bool IsReadonly,
bool IsNullable,
bool IsGeneric,
ICollection<DestinationType> GenericTypeArguments,
Type? SourceType,
Type? InnerType)
{
public DestinationType(string typeName, string? fullName, bool isBuiltin, bool isArray, bool isReadonly, bool isNullable, Type? innerType, string? importType = null) : this(typeName, fullName, importType ?? typeName, isBuiltin, isArray, isReadonly, isNullable, innerType)
public DestinationType(string typeName,
string? fullName,
bool isBuiltin,
bool isArray,
bool isReadonly,
bool isNullable,
bool isGeneric,
ICollection<DestinationType> genericTypeArguments,
Type? innerType,
Type? sourceType,
string? importType = null) : this(typeName, fullName, importType ?? typeName, isBuiltin, isArray, isReadonly, isNullable, isGeneric, genericTypeArguments, sourceType, innerType)
{
}

Expand Down
19 changes: 18 additions & 1 deletion TypeContractor/Output/OutputProperty.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
namespace TypeContractor.Output;

public class OutputProperty(string sourceName, Type sourceType, Type? innerSourceType, string destinationName, string destinationType, string importType, bool isBuiltin, bool isArray, bool isNullable, bool isReadonly)
public class OutputProperty(
string sourceName,
Type sourceType,
Type? innerSourceType,
string destinationName,
string destinationType,
string importType,
bool isBuiltin,
bool isArray,
bool isNullable,
bool isReadonly,
bool isGeneric,
ICollection<DestinationType> genericTypeArguments)
{
public string SourceName { get; set; } = sourceName;
public Type SourceType { get; set; } = sourceType;
Expand All @@ -12,6 +24,8 @@ public class OutputProperty(string sourceName, Type sourceType, Type? innerSourc
public bool IsArray { get; set; } = isArray;
public bool IsNullable { get; set; } = isNullable;
public bool IsReadonly { get; set; } = isReadonly;
public bool IsGeneric { get; set; } = isGeneric;
public ICollection<DestinationType> GenericTypeArguments { get; } = genericTypeArguments;
public ObsoleteInfo? Obsolete { get; set; }

/// <summary>
Expand All @@ -37,6 +51,8 @@ public override bool Equals(object? obj)
IsArray == property.IsArray &&
IsNullable == property.IsNullable &&
IsReadonly == property.IsReadonly &&
IsGeneric == property.IsGeneric &&
GenericTypeArguments.SequenceEqual(property.GenericTypeArguments) &&
EqualityComparer<ObsoleteInfo?>.Default.Equals(Obsolete, property.Obsolete);
}

Expand All @@ -53,6 +69,7 @@ public override int GetHashCode()
hash.Add(IsArray);
hash.Add(IsNullable);
hash.Add(IsReadonly);
hash.Add(IsGeneric);
hash.Add(Obsolete);
return hash.ToHashCode();
}
Expand Down
13 changes: 11 additions & 2 deletions TypeContractor/Output/OutputType.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
using System.Globalization;
using System.Globalization;
using System.Text;

namespace TypeContractor.Output;

public record OutputType(string Name, string FullName, string FileName, ContractedType ContractedType, bool IsEnum, ICollection<OutputProperty>? Properties, ICollection<OutputEnumMember>? EnumMembers)
public record OutputType(
string Name,
string FullName,
string FileName,
ContractedType ContractedType,
bool IsEnum,
bool IsGeneric,
ICollection<DestinationType> GenericTypeArguments,
ICollection<OutputProperty>? Properties,
ICollection<OutputEnumMember>? EnumMembers)
{
public override string ToString()
{
Expand Down
Loading

0 comments on commit 8a9e7b2

Please sign in to comment.