Skip to content

Commit e02ac1c

Browse files
author
Per Christian B. Viken
committed
feat(lib): Add support for readonly output properties
Fixes #1
1 parent 25c6287 commit e02ac1c

File tree

6 files changed

+63
-23
lines changed

6 files changed

+63
-23
lines changed

CHANGELOG.md

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

88
## [unreleased]
99

10+
### Added
11+
12+
- Add support for `readonly` output properties (#1)
13+
1014
### Fixed
1115

1216
- Properly handle `IEnumerable<T>` as values in a Dictionary (#2)

TypeContractor.Tests/TypeScript/TypeScriptConverterTests.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,24 @@ public void Handles_Dynamic_Types()
167167
prop.IsBuiltin.Should().BeTrue();
168168
}
169169

170+
[Theory]
171+
[InlineData(0, "success", "boolean", true)]
172+
[InlineData(1, "created", "string", true)]
173+
[InlineData(2, "errors", "string[]", false)]
174+
[InlineData(3, "warnings", "{ [key: string]: string[] }", true)]
175+
public void Adds_Readonly_Modifier(int index, string destinationName, string destinationType, bool isReadonly)
176+
{
177+
var result = Sut.Convert(typeof(ReadonlyResponse));
178+
179+
result.Should().NotBeNull();
180+
result.Properties.Should().HaveCount(4);
181+
182+
var prop = result.Properties!.ElementAt(index);
183+
prop.DestinationName.Should().Be(destinationName);
184+
prop.FullDestinationType.Should().Be(destinationType);
185+
prop.IsReadonly.Should().Be(isReadonly);
186+
}
187+
170188
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
171189
private class SimpleTypes
172190
{
@@ -226,6 +244,14 @@ private class DynamicResponse
226244
public dynamic Result { get; set; }
227245
}
228246

247+
private class ReadonlyResponse
248+
{
249+
public bool Success => !Errors.Any();
250+
public DateTime Created { get; }
251+
public IEnumerable<string> Errors { get; set; }
252+
public Dictionary<string, IEnumerable<string>> Warnings { get; }
253+
}
254+
229255
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
230256

231257
private MetadataLoadContext BuildMetadataLoadContext()

TypeContractor/Output/DestinationType.cs

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

3-
public record DestinationType(string TypeName, string ImportType, bool IsBuiltin, bool IsArray, Type? InnerType)
3+
public record DestinationType(string TypeName, string ImportType, bool IsBuiltin, bool IsArray, bool IsReadonly, Type? InnerType)
44
{
5-
public DestinationType(string typeName, bool isBuiltin, bool isArray, Type? innerType, string? importType = null) : this(typeName, importType ?? typeName, isBuiltin, isArray, innerType)
5+
public DestinationType(string typeName, bool isBuiltin, bool isArray, bool isReadonly, Type? innerType, string? importType = null) : this(typeName, importType ?? typeName, isBuiltin, isArray, isReadonly, innerType)
66
{
77
}
88

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
namespace TypeContractor.Output;
22

3-
public record OutputProperty(string SourceName, Type SourceType, Type? InnerSourceType, string DestinationName, string DestinationType, string ImportType, bool IsBuiltin, bool IsArray, bool IsNullable)
4-
{
3+
public record OutputProperty(string SourceName, Type SourceType, Type? InnerSourceType, string DestinationName, string DestinationType, string ImportType, bool IsBuiltin, bool IsArray, bool IsNullable, bool IsReadonly)
4+
{
55
public override string ToString()
66
{
7-
return $"{DestinationName}{(IsNullable ? "?" : "")}: {DestinationType}{(IsArray ? "[]" : "")} (import {ImportType} from {SourceType}, {(IsBuiltin ? "builtin" : "custom")})";
8-
}
7+
return $"{(IsReadonly ? "readonly" : "")}{DestinationName}{(IsNullable ? "?" : "")}: {FullDestinationType} (import {ImportType} from {SourceType}, {(IsBuiltin ? "builtin" : "custom")})";
8+
}
9+
10+
/// <summary>
11+
/// Returns the <see cref="DestinationType"/> and array brackets if the type is an array
12+
/// </summary>
13+
public string FullDestinationType => $"{DestinationType}{(IsArray ? "[]" : "")}";
914
}

TypeContractor/TypeScript/TypeScriptConverter.cs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,13 @@ private ICollection<OutputProperty> GetProperties(Type type)
6868
var getter = property.GetGetMethod(false);
6969
if (getter is null) continue;
7070

71+
// Check if we have a setter
72+
var setter = property.GetSetMethod(false);
73+
var isReadonly = !property.CanWrite || setter is null;
74+
7175
var destinationName = GetDestinationName(property.Name);
72-
var destinationType = GetDestinationType(property.PropertyType, property.CustomAttributes);
73-
outputProperties.Add(new OutputProperty(property.Name, property.PropertyType, destinationType.InnerType, destinationName, destinationType.TypeName, destinationType.ImportType, destinationType.IsBuiltin, destinationType.IsArray, TypeChecks.IsNullable(property.PropertyType)));
76+
var destinationType = GetDestinationType(property.PropertyType, property.CustomAttributes, isReadonly);
77+
outputProperties.Add(new OutputProperty(property.Name, property.PropertyType, destinationType.InnerType, destinationName, destinationType.TypeName, destinationType.ImportType, destinationType.IsBuiltin, destinationType.IsArray, TypeChecks.IsNullable(property.PropertyType), destinationType.IsReadonly));
7478
}
7579

7680
// Look at base classes
@@ -86,57 +90,57 @@ private ICollection<OutputProperty> GetProperties(Type type)
8690

8791
private static string GetDestinationName(string name) => name.ToTypeScriptName();
8892

89-
private DestinationType GetDestinationType(in Type sourceType, IEnumerable<CustomAttributeData> customAttributes)
93+
private DestinationType GetDestinationType(in Type sourceType, IEnumerable<CustomAttributeData> customAttributes, bool isReadonly)
9094
{
9195
if (_configuration.TypeMaps.TryGetValue(sourceType.FullName!, out string? destType))
92-
return new DestinationType(destType, true, false, null);
96+
return new DestinationType(destType, true, false, isReadonly, null);
9397

9498
if (CustomMappedTypes.TryGetValue(sourceType, out OutputType? customType))
95-
return new DestinationType(customType.Name, false, false, null);
99+
return new DestinationType(customType.Name, false, false, isReadonly, null);
96100

97101
if (TypeChecks.ImplementsIDictionary(sourceType))
98102
{
99-
var keyType = GetDestinationType(TypeChecks.GetGenericType(sourceType, 0), customAttributes);
103+
var keyType = GetDestinationType(TypeChecks.GetGenericType(sourceType, 0), customAttributes, isReadonly);
100104
var valueType = TypeChecks.GetGenericType(sourceType, 1);
101-
var valueDestinationType = GetDestinationType(valueType, customAttributes);
105+
var valueDestinationType = GetDestinationType(valueType, customAttributes, isReadonly);
102106

103107
var isBuiltin = keyType.IsBuiltin && valueDestinationType.IsBuiltin;
104108

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

108112
if (TypeChecks.ImplementsIEnumerable(sourceType))
109113
{
110114
var innerType = TypeChecks.GetGenericType(sourceType);
111115

112-
var (TypeName, _, IsBuiltin, _, _) = GetDestinationType(innerType, customAttributes);
113-
return new DestinationType(TypeName, IsBuiltin, true, innerType);
116+
var (TypeName, _, IsBuiltin, _, IsReadonly, _) = GetDestinationType(innerType, customAttributes, isReadonly);
117+
return new DestinationType(TypeName, IsBuiltin, true, IsReadonly, innerType);
114118
}
115119

116120
if (TypeChecks.IsValueTuple(sourceType))
117121
{
118122
var arguments = sourceType.GenericTypeArguments;
119-
var argumentDestinationTypes = arguments.Select(arg => GetDestinationType(arg, customAttributes));
123+
var argumentDestinationTypes = arguments.Select(arg => GetDestinationType(arg, customAttributes, isReadonly));
120124
var isBuiltin = argumentDestinationTypes.All(arg => arg.IsBuiltin);
121125

122126
var argumentList = argumentDestinationTypes.Select((arg, idx) => $"item{idx+1}: {arg.FullTypeName}");
123127
var typeName = $"{{ {string.Join(", ", argumentList)} }}";
124128

125-
return new DestinationType(typeName, isBuiltin, false, null);
129+
return new DestinationType(typeName, isBuiltin, false, isReadonly, null);
126130
}
127131

128132
if (TypeChecks.IsNullable(sourceType))
129133
{
130-
return GetDestinationType(sourceType.GenericTypeArguments.First(), customAttributes);
134+
return GetDestinationType(sourceType.GenericTypeArguments.First(), customAttributes, isReadonly);
131135
}
132136

133137
if (customAttributes.Any(x => x.AttributeType.FullName == "System.Runtime.CompilerServices.DynamicAttribute"))
134-
return new DestinationType(DestinationTypes.Dynamic, true, false, null);
138+
return new DestinationType(DestinationTypes.Dynamic, true, false, isReadonly, null);
135139

136140
// FIXME: Check if this is one of our types?
137141
var outputType = Convert(sourceType);
138142
CustomMappedTypes.Add(sourceType, outputType);
139-
return new DestinationType(outputType.Name, false, false, null);
143+
return new DestinationType(outputType.Name, false, false, isReadonly, null);
140144

141145
// throw new ArgumentException($"Unexpected type: {sourceType}");
142146
}

TypeContractor/TypeScript/TypeScriptWriter.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,9 @@ private void BuildBody(OutputType type)
101101
foreach (var property in type.Properties ?? Enumerable.Empty<OutputProperty>())
102102
{
103103
var nullable = property.IsNullable ? "?" : "";
104-
var array = property.IsArray ? "[]" : "";
105-
_builder.AppendFormat(CultureInfo.InvariantCulture, " {0}{1}: {2}{3};\r\n", property.DestinationName, nullable, property.DestinationType, array);
104+
var array = property.IsArray ? "[]" : "";
105+
var isReadonly = property.IsReadonly ? "readonly " : "";
106+
_builder.AppendFormat(CultureInfo.InvariantCulture, " {4}{0}{1}: {2}{3};\r\n", property.DestinationName, nullable, property.DestinationType, array, isReadonly);
106107
}
107108

108109
foreach (var member in type.EnumMembers ?? Enumerable.Empty<OutputEnumMember>())

0 commit comments

Comments
 (0)