Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[COR-123] Support records #157

Merged
merged 10 commits into from
Dec 12, 2023
Merged
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

#### Added

- `ITopic` and `IProduceNotification<>` interfaces and their support in the generator.
- `ITopic` and `IProduceNotification<>` interfaces and their support in the generator,
- Support for `records`.

#### Breaking changes

Expand Down
5 changes: 0 additions & 5 deletions docs/guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@
The properties are generated as-is, even if they don't have getter/setter or if getter/setter is not public. Using
anything else results in C#/contracts mismatch because client types _will_ have public getters and setters.

### Do not use records

Records introduce logic (structural equality) to your code, which cannot be translated. This might lead to broken
contracts usage.

### Prefer `List` over array

### Prefer concrete types instead of interfaces
Expand Down
7 changes: 7 additions & 0 deletions examples/properties/inner_record.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
public class DTO
{
InnerDTO A { get; set; }
InnerDTO? B { get; set; }
}

public record InnerDTO();
1 change: 1 addition & 0 deletions examples/properties/record.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public record DTO(int A);
4 changes: 4 additions & 0 deletions examples/simple/inheritance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ public class C : B, A
public int PropA { get; set; }
public int PropC { get; set; }
}

public record D(int PropD);

public record E(int PropD, int PropE) : D(PropD);
1 change: 1 addition & 0 deletions examples/simple/record.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public record DTO();
1 change: 1 addition & 0 deletions examples/simple/record_class.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public record class DTO();
1 change: 1 addition & 0 deletions examples/simple/record_struct.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
public record struct DTO();
11 changes: 11 additions & 0 deletions examples/supported_use_cases/records_as_cqrs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using LeanCode.Contracts;

public record DTO1(int Property);

public record DTO2(string Property);

public record Command(DTO1 DTO1) : ICommand;

public record Query(DTO1 DTO1) : IQuery<DTO2>;

public record Operation(DTO1 DTO1) : IOperation<DTO2>;
20 changes: 20 additions & 0 deletions src/LeanCode.ContractsGenerator.Tests/ExampleBased/Properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,24 @@ public void Properties_with_struct_types()
.WithProperty("A", TypeRefExtensions.Internal("InnerDTO"))
.WithProperty("B", TypeRefExtensions.Internal("InnerDTO").Nullable());
}

[Fact]
public void Properties_inside_record_types()
{
"properties/record.cs"
.Compiles()
.WithDto("DTO")
.WithProperty("A", Known(KnownType.Int32))
.WithoutProperty("EqualityContract");
}

[Fact]
public void Properties_with_record_types()
{
"properties/inner_record.cs"
.Compiles()
.WithDto("DTO")
.WithProperty("A", TypeRefExtensions.Internal("InnerDTO"))
.WithProperty("B", TypeRefExtensions.Internal("InnerDTO").Nullable());
}
}
25 changes: 24 additions & 1 deletion src/LeanCode.ContractsGenerator.Tests/ExampleBased/Simple.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,24 @@ public void Simple_Struct()
"simple/struct.cs".Compiles().WithSingle().Dto("DTO");
}

[Fact]
public void Simple_Record()
{
"simple/record.cs".Compiles().WithSingle().Dto("DTO");
}

[Fact]
public void Record_Struct()
{
"simple/record_struct.cs".Compiles().WithSingle().Dto("DTO");
}

[Fact]
public void Record_Class()
{
"simple/record_class.cs".Compiles().WithSingle().Dto("DTO");
}

[Fact]
public void Simple_Enum()
{
Expand All @@ -88,7 +106,12 @@ public void Inherited_properties()
.WithDto("C")
.WithProperty("PropC", Known(KnownType.Int32))
.WithoutProperty("PropA")
.WithoutProperty("PropB");
.WithoutProperty("PropB")
.WithDto("D")
.WithProperty("PropD", Known(KnownType.Int32))
.WithDto("E")
.WithProperty("PropE", Known(KnownType.Int32))
.WithoutProperty("PropD");
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,4 +177,16 @@ public void Produce_notification_from_inherited_type()
)
);
}

[Fact]
public void Records_as_cqrs()
{
"supported_use_cases/records_as_cqrs.cs"
.Compiles()
.WithDto("DTO1")
.WithDto("DTO2")
.WithQuery("Query")
.WithCommand("Command")
.WithOperation("Operation");
}
}
22 changes: 22 additions & 0 deletions src/LeanCode.ContractsGenerator/Compilation/ContractTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ namespace LeanCode.ContractsGenerator.Compilation;

public sealed class ContractTypes
{
private const string RecordEqualityContractPropertyName = "EqualityContract";

private HashSet<INamedTypeSymbol> QueryType { get; }
private HashSet<INamedTypeSymbol> CommandType { get; }
private HashSet<INamedTypeSymbol> OperationType { get; }
Expand All @@ -26,6 +28,8 @@ public sealed class ContractTypes
private HashSet<INamedTypeSymbol> ReadOnlyDictionary { get; }
private HashSet<INamedTypeSymbol> Dictionary { get; }

private HashSet<INamedTypeSymbol> Equatable { get; }

public ContractTypes(IReadOnlyCollection<CSharpCompilation> compilations)
{
QueryType = GetUnboundTypeSymbols(compilations, typeof(IQuery<>));
Expand All @@ -51,6 +55,8 @@ public ContractTypes(IReadOnlyCollection<CSharpCompilation> compilations)
AttributeUsageAttribute = GetTypeSymbols<AttributeUsageAttribute>(compilations);
ReadOnlyDictionary = GetUnboundTypeSymbols(compilations, typeof(IReadOnlyDictionary<,>));
Dictionary = GetUnboundTypeSymbols(compilations, typeof(IDictionary<,>));

Equatable = GetUnboundTypeSymbols(compilations, typeof(IEquatable<>));
}

private static HashSet<INamedTypeSymbol> GetTypeSymbols<T>(
Expand Down Expand Up @@ -222,4 +228,20 @@ public bool IsReadOnlyDictionary(ITypeSymbol i)
|| Dictionary.Contains(ns.ConstructUnboundGenericType())
);
}

public bool IsRecordEquatable(ITypeSymbol i)
{
return i is INamedTypeSymbol ns
&& ns.IsGenericType
&& ns.TypeArguments is [INamedTypeSymbol namedType]
&& namedType.IsRecord
&& Equatable.Contains(ns.ConstructUnboundGenericType());
}

public static bool IsRecordEqualityContract(IPropertySymbol i)
{
return i.ContainingType is INamedTypeSymbol ns
&& ns.IsRecord
&& i.Name == RecordEqualityContractPropertyName;
}
}
10 changes: 6 additions & 4 deletions src/LeanCode.ContractsGenerator/Generation/ContractsGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,16 @@ private bool IsIgnored(
|| symbol.SpecialType == SpecialType.System_Enum
|| ErrorCodes.IsErrorCode(symbol)
|| contracts.Types.IsProduceNotificationType(symbol)
|| contracts.Types.IsAttributeUsageType(symbol);
|| contracts.Types.IsAttributeUsageType(symbol)
|| contracts.Types.IsRecordEquatable(symbol);
}

private bool IsExcluded(ISymbol symbol)
{
return symbol
.GetAttributes()
.Any(a => contracts.Types.IsExcludeFromContractsGenerationType(a.AttributeClass));
return (symbol is IPropertySymbol ps && ContractTypes.IsRecordEqualityContract(ps))
|| symbol
.GetAttributes()
.Any(a => contracts.Types.IsExcludeFromContractsGenerationType(a.AttributeClass));
}

private static HashSet<string> GatherBaseProperties(INamedTypeSymbol ns)
Expand Down
Loading