Skip to content

Commit 7327f4b

Browse files
Merge pull request #24 from PerfectlyNormal/fix/writing-dictionaries
Fixes for writing complex dictionaries
2 parents 654d46c + 3ef7bc5 commit 7327f4b

File tree

7 files changed

+239
-5
lines changed

7 files changed

+239
-5
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [unreleased]
99

10+
### Added
11+
12+
- Add default map from `System.Object` to `any`
13+
14+
### Fixed
15+
16+
- Fix writing dictionaries with custom types as values wrapped inside lists.
17+
E.g. converting `Dictionary<Guid, IEnumerable<FormulaDto>>` to
18+
`{ [key: string]: FormulaDto[] }`
19+
- Fix writing dictionaries wrapped inside other dictionaries, e.g. making sure
20+
`Dictionary<Guid, Dictionary<string, IEnumerable<FormulaDto>>>` correctly
21+
translates to `{ [key: string]: { [key: string]: FormulaDto[] } }`.
22+
1023
## [0.9.0] - 2024-01-05
1124

1225
### Added

TypeContractor.Tests/TypeScript/TypeScriptConverterTests.cs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void Can_Convert_Simple_Types()
3333
result.FullName.Should().Be("TypeContractor.Tests.TypeScript.TypeScriptConverterTests+SimpleTypes");
3434
result.IsEnum.Should().BeFalse();
3535
result.EnumMembers.Should().BeNull();
36-
result.Properties.Should().HaveCount(5);
36+
result.Properties.Should().HaveCount(6);
3737
}
3838

3939
[Theory]
@@ -42,6 +42,7 @@ public void Can_Convert_Simple_Types()
4242
[InlineData(2)]
4343
[InlineData(3)]
4444
[InlineData(4)]
45+
[InlineData(5)]
4546
public void Converted_Simple_Properties_Looks_As_Expected(int propertyIndex)
4647
{
4748
var result = Sut.Convert(typeof(SimpleTypes));
@@ -104,6 +105,17 @@ public void Converted_Simple_Properties_Looks_As_Expected(int propertyIndex)
104105
prop.IsArray.Should().BeFalse();
105106
prop.IsNullable.Should().BeFalse();
106107
break;
108+
109+
case 5:
110+
prop.SourceName.Should().Be("SomeObject");
111+
prop.SourceType.Should().Be(typeof(object));
112+
prop.InnerSourceType.Should().BeNull();
113+
prop.DestinationName.Should().Be("someObject");
114+
prop.DestinationType.Should().Be("any");
115+
prop.IsBuiltin.Should().BeTrue();
116+
prop.IsArray.Should().BeFalse();
117+
prop.IsNullable.Should().BeFalse();
118+
break;
107119
}
108120
}
109121

@@ -123,13 +135,14 @@ public void Finds_Properties_From_Multiple_Base_Classes()
123135
var result = Sut.Convert(typeof(NestedInheritanceTest));
124136

125137
result.Properties.Should().NotBeNull();
126-
result.Properties.Should().HaveCount(7);
138+
result.Properties.Should().HaveCount(8);
127139
result.Properties.Should()
128140
.Contain(x => x.SourceName == "StringProperty")
129141
.And.Contain(x => x.SourceName == "NumberProperty")
130142
.And.Contain(x => x.SourceName == "NumbersProperty")
131143
.And.Contain(x => x.SourceName == "DoubleTime")
132144
.And.Contain(x => x.SourceName == "TimeyWimeySpan")
145+
.And.Contain(x => x.SourceName == "SomeObject")
133146
.And.Contain(x => x.SourceName == "InheritedProperty")
134147
.And.Contain(x => x.SourceName == "FinalProperty");
135148
}
@@ -168,6 +181,30 @@ public void Handles_Dictionary_With_Enumerable_Value()
168181
prop.DestinationType.Should().Be("{ [key: string]: string[] }");
169182
}
170183

184+
[Fact]
185+
public void Handles_Dictionary_With_Complex_Values()
186+
{
187+
var result = Sut.Convert(typeof(ComplexValueDictionary));
188+
189+
result.Should().NotBeNull();
190+
result.Properties.Should().HaveCount(1);
191+
var prop = result.Properties!.First();
192+
prop.DestinationName.Should().Be("formulas");
193+
prop.DestinationType.Should().Be("{ [key: string]: FormulaDto[] }");
194+
}
195+
196+
[Fact]
197+
public void Handles_Dictionary_With_Nested_Dictionary_Values()
198+
{
199+
var result = Sut.Convert(typeof(NestedValueDictionary));
200+
201+
result.Should().NotBeNull();
202+
result.Properties.Should().HaveCount(1);
203+
var prop = result.Properties!.First();
204+
prop.DestinationName.Should().Be("formulas");
205+
prop.DestinationType.Should().Be("{ [key: string]: { [key: string]: FormulaDto[] } }");
206+
}
207+
171208
[Fact]
172209
public void Handles_Simple_ValueTuple_Types()
173210
{
@@ -219,6 +256,7 @@ private class SimpleTypes
219256
public IEnumerable<int> NumbersProperty { get; set; }
220257
public double DoubleTime { get; set; }
221258
public TimeSpan TimeyWimeySpan { get; set; }
259+
public object SomeObject { get; set; }
222260
}
223261

224262
private class TypeVisibility
@@ -262,6 +300,23 @@ private class ComplexDictionaryResponse
262300
public Dictionary<Guid, IEnumerable<string>> Messages { get; set; }
263301
}
264302

303+
private class ComplexValueDictionary
304+
{
305+
public Dictionary<Guid, IEnumerable<FormulaDto>> Formulas { get; set; }
306+
}
307+
308+
private class NestedValueDictionary
309+
{
310+
public Dictionary<Guid, Dictionary<string, IEnumerable<FormulaDto>>> Formulas { get; set; }
311+
}
312+
313+
private class FormulaDto
314+
{
315+
public Guid Id { get; set; }
316+
public string Name { get; set; }
317+
public string Definition { get; set; }
318+
}
319+
265320
private class ValueTupleResponse
266321
{
267322
public (List<string> Errors, List<string> Warnings) Messages { get; set; }
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
using System.Reflection;
2+
using System.Runtime.InteropServices;
3+
using TypeContractor.Output;
4+
using TypeContractor.TypeScript;
5+
6+
namespace TypeContractor.Tests.TypeScript;
7+
8+
public class TypeScriptWriterTests : IDisposable
9+
{
10+
private readonly DirectoryInfo _outputDirectory;
11+
private readonly TypeContractorConfiguration _configuration;
12+
private readonly TypeScriptConverter _converter;
13+
14+
public TypeScriptWriter Sut { get; }
15+
16+
public TypeScriptWriterTests()
17+
{
18+
_outputDirectory = Directory.CreateTempSubdirectory();
19+
_configuration = TypeContractorConfiguration.WithDefaultConfiguration().SetOutputDirectory(_outputDirectory.FullName);
20+
_converter = new TypeScriptConverter(_configuration, BuildMetadataLoadContext());
21+
Sut = new TypeScriptWriter(_configuration.OutputPath);
22+
}
23+
24+
[Fact]
25+
public void Can_Write_Simple_Types()
26+
{
27+
// Arrange
28+
var types = new[] { typeof(SimpleTypes) }
29+
.Select(t => ContractedType.FromName(t.FullName!, t, _configuration));
30+
31+
var outputTypes = types
32+
.Select(_converter.Convert)
33+
.ToList() // Needed so `converter.Convert` runs before we concat
34+
.Concat(_converter.CustomMappedTypes.Values)
35+
.ToList();
36+
37+
// Act
38+
var result = Sut.Write(outputTypes.First(), outputTypes);
39+
40+
// Assert
41+
var file = File.ReadAllLines(result).Select(x => x.TrimStart());
42+
file.Should()
43+
.NotBeEmpty()
44+
.And.NotContainMatch("import * from")
45+
.And.Contain("export interface SimpleTypes {")
46+
.And.Contain("stringProperty: string;")
47+
.And.Contain("numberProperty?: number;")
48+
.And.Contain("numbersProperty: number[];")
49+
.And.Contain("doubleTime: number;")
50+
.And.Contain("timeyWimeySpan: string;")
51+
.And.Contain("someObject: any;");
52+
}
53+
54+
[Fact]
55+
public void Handles_Dictionary_With_Complex_Values()
56+
{
57+
// Arrange
58+
var types = new[] { typeof(ComplexValueDictionary) }
59+
.Select(t => ContractedType.FromName(t.FullName!, t, _configuration));
60+
61+
var outputTypes = types
62+
.Select(_converter.Convert)
63+
.ToList() // Needed so `converter.Convert` runs before we concat
64+
.Concat(_converter.CustomMappedTypes.Values)
65+
.ToList();
66+
67+
// Act
68+
var result = Sut.Write(outputTypes.First(), outputTypes);
69+
70+
// Assert
71+
var file = File.ReadAllText(result);
72+
file.Should()
73+
.NotBeEmpty()
74+
.And.Contain("import { FormulaDto } from \"./FormulaDto\";")
75+
.And.Contain("formulas: { [key: string]: FormulaDto[] };");
76+
}
77+
78+
[Fact]
79+
public void Handles_Dictionary_With_Nested_Dictionary_Values()
80+
{
81+
// Arrange
82+
var types = new[] { typeof(NestedValueDictionary) }
83+
.Select(t => ContractedType.FromName(t.FullName!, t, _configuration));
84+
85+
var outputTypes = types
86+
.Select(_converter.Convert)
87+
.ToList() // Needed so `converter.Convert` runs before we concat
88+
.Concat(_converter.CustomMappedTypes.Values)
89+
.ToList();
90+
91+
// Act
92+
var result = Sut.Write(outputTypes.First(), outputTypes);
93+
94+
// Assert
95+
var file = File.ReadAllText(result);
96+
file.Should()
97+
.NotBeEmpty()
98+
.And.Contain("import { FormulaDto } from \"./FormulaDto\";")
99+
.And.Contain("formulas: { [key: string]: { [key: string]: FormulaDto[] } };");
100+
}
101+
102+
#region Test input
103+
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
104+
private class SimpleTypes
105+
{
106+
public string StringProperty { get; set; }
107+
public int? NumberProperty { get; set; }
108+
public IEnumerable<int> NumbersProperty { get; set; }
109+
public double DoubleTime { get; set; }
110+
public TimeSpan TimeyWimeySpan { get; set; }
111+
public object SomeObject { get; set; }
112+
}
113+
114+
private class ComplexValueDictionary
115+
{
116+
public Dictionary<Guid, IEnumerable<FormulaDto>> Formulas { get; set; }
117+
}
118+
119+
private class NestedValueDictionary
120+
{
121+
public Dictionary<Guid, Dictionary<string, IEnumerable<FormulaDto>>> Formulas { get; set; }
122+
}
123+
124+
private class FormulaDto
125+
{
126+
public Guid Id { get; set; }
127+
public string Name { get; set; }
128+
public string Definition { get; set; }
129+
}
130+
#endregion
131+
#pragma warning restore CS8618
132+
133+
private MetadataLoadContext BuildMetadataLoadContext()
134+
{
135+
// Get the array of runtime assemblies.
136+
var runtimeAssemblies = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll");
137+
138+
// Create the list of assembly paths consisting of runtime assemblies and the inspected assemblies.
139+
var paths = runtimeAssemblies.Concat(_configuration.Assemblies.Values);
140+
141+
var resolver = new PathAssemblyResolver(paths);
142+
143+
return new MetadataLoadContext(resolver);
144+
}
145+
146+
public void Dispose()
147+
{
148+
if (_outputDirectory.Exists)
149+
_outputDirectory.Delete(true);
150+
}
151+
}

TypeContractor/TypeContractorConfiguration.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ public TypeContractorConfiguration AddDefaultSuffixes()
5959
/// </summary>
6060
/// <returns>The configuration object for continued chaining</returns>
6161
public TypeContractorConfiguration AddDefaultTypeMaps()
62-
{
62+
{
63+
AddCustomMap(typeof(object), DestinationTypes.AnyType);
6364
AddCustomMap(typeof(string), DestinationTypes.StringType);
6465
AddCustomMap(typeof(DateTime), DestinationTypes.StringType);
6566
AddCustomMap(typeof(DateTimeOffset), DestinationTypes.StringType);

TypeContractor/TypeScript/DestinationTypes.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
namespace TypeContractor.TypeScript;
22

33
public static class DestinationTypes
4-
{
4+
{
5+
public const string AnyType = "any";
56
public const string StringType = "string";
67
public const string Boolean = "boolean";
78
public const string Number = "number";

TypeContractor/TypeScript/TypeScriptConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ private DestinationType GetDestinationType(in Type sourceType, IEnumerable<Custo
106106

107107
var isBuiltin = keyType.IsBuiltin && valueDestinationType.IsBuiltin;
108108

109-
return new DestinationType($"{{ [key: {keyType.TypeName}]: {valueDestinationType.FullTypeName} }}", isBuiltin, false, isReadonly, valueType, valueDestinationType.TypeName);
109+
return new DestinationType($"{{ [key: {keyType.TypeName}]: {valueDestinationType.FullTypeName} }}", isBuiltin, false, isReadonly, valueType, valueDestinationType.ImportType);
110110
}
111111

112112
if (TypeChecks.ImplementsIEnumerable(sourceType))

TypeContractor/TypeScript/TypeScriptWriter.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,19 @@ private static List<OutputType> GetImportedTypes(IEnumerable<OutputType> allType
137137
{
138138
var keyType = TypeChecks.GetGenericType(sourceType, 0);
139139
var valueType = TypeChecks.GetGenericType(sourceType, 1);
140+
while (TypeChecks.ImplementsIEnumerable(valueType))
141+
valueType = TypeChecks.GetGenericType(valueType);
142+
143+
if (TypeChecks.ImplementsIDictionary(valueType))
144+
{
145+
var nestedKeyType = TypeChecks.GetGenericType(valueType, 0);
146+
var nestedValueType = TypeChecks.GetGenericType(valueType, 1);
147+
while (TypeChecks.ImplementsIEnumerable(nestedValueType))
148+
nestedValueType = TypeChecks.GetGenericType(nestedValueType);
149+
150+
var names = new[] { keyType.FullName, nestedKeyType.FullName, nestedValueType.FullName };
151+
return allTypes.Where(x => names.Contains(x.FullName)).ToList();
152+
}
140153

141154
return allTypes.Where(x => x.FullName == keyType.FullName || x.FullName == valueType.FullName).ToList();
142155
}

0 commit comments

Comments
 (0)