Skip to content

Commit

Permalink
Merge pull request #157 from leancodepl/feature/add-records
Browse files Browse the repository at this point in the history
[COR-123] Support records
  • Loading branch information
wojtek2288 authored Dec 12, 2023
2 parents 8628cca + c24a606 commit 65f572f
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 11 deletions.
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

0 comments on commit 65f572f

Please sign in to comment.