From 605f827c3f468b1411c88aacb6d8bc52a7bf4736 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Thu, 22 Oct 2020 22:36:27 +0200 Subject: [PATCH] Tests, docs --- README.md | 107 ++++++++++-------- src/WrapperValueObject.Generator/Generator.cs | 11 ++ .../WrapperValueObject.TestConsole/Program.cs | 1 - .../WrapperValueObject.TestConsole.csproj | 8 +- .../MetricTypesTests.cs | 77 +++++++++++++ .../MoneyTypeTests.cs | 62 ++++++++++ .../ProductIdTypeTests.cs | 25 ++++ test/WrapperValueObject.Tests/SimpleTypes.cs | 22 ---- 8 files changed, 237 insertions(+), 76 deletions(-) create mode 100644 test/WrapperValueObject.Tests/MetricTypesTests.cs create mode 100644 test/WrapperValueObject.Tests/MoneyTypeTests.cs create mode 100644 test/WrapperValueObject.Tests/ProductIdTypeTests.cs diff --git a/README.md b/README.md index 14dde13..53bb713 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ A .NET source generator for creating * Simple value objects wrapping other type(s), without the hassle of manual `Equals`/`GetHashCode` * Value objects wrapping math primitives - * I.e. `[WrapperValueObject(typeof(int))] partial readonly struct MeterLength` - the type is implicitly castable to `int`, and you can create your own math operations + * I.e. `[WrapperValueObject(typeof(int))] readonly partial struct MeterLength { }` - the type is implicitly castable to `int`, and you can create your own math operations * Strongly typed ID's - * Similar to F# `type ProductId = ProductId of Guid`, here it becomes `[WrapperValueObject] partial readonly struct ProductId` + * Similar to F# `type ProductId = ProductId of Guid`, here it becomes `[WrapperValueObject] readonly partial struct ProductId { }` Note that record type feature for structs is planned for C# 10, in which cases some of the use cases this library supports will be easier to achieve without this libray. @@ -35,85 +35,94 @@ dotnet add package WrapperValueObject.Generator --version 0.0.1-alpha04 1. Use the attribute to specify the underlying type. 2. Declare the struct or class with the `partial` keyword. (and does not support nested types) -### Simple example +### Strongly typed ID + +```csharp +[WrapperValueObject] readonly partial struct ProductId { } + +var id = ProductId.New(); // Strongly typed Guid wrapper, i.e. {1658db8c-89a4-46ea-b97e-8cf966cfb3f1} + +Assert.NotEqual(ProductId.New(), id); +Assert.False(ProductId.New() == id); +``` + +### Money type + +```csharp +[WrapperValueObject(typeof(decimal))] readonly partial struct Money { } + +Money money = 2m; + +var result = money + 2m; // 4.0 +var result2 = money + new Money(2m); + +Assert.True(result == result2); +Assert.Equal(4m, (decimal)result); +``` + + +### Metric types ```csharp [WrapperValueObject(typeof(int))] public readonly partial struct MeterLength { - public static implicit operator CentimeterLength(MeterLength meter) => meter.Value * 100; + public static implicit operator CentimeterLength(MeterLength meter) => meter.Value * 100; // .Value is the inner type, in this case int } [WrapperValueObject(typeof(int))] public readonly partial struct CentimeterLength { - public static implicit operator MeterLength(CentimeterLength centiMeter) => centiMeter / 100; + public static implicit operator MeterLength(CentimeterLength centiMeter) => centiMeter.Value / 100; } MeterLength meters = 2; -CentimeterLength centiMeters = meters; +CentimeterLength centiMeters = meters; // 200 Assert.Equal(200, (int)centiMeters); ``` -### Full example +### Complex types ```csharp -using System; -using System.Diagnostics; +[WrapperValueObject] // Is Guid ID by default +readonly partial struct MatchId { } -namespace WrapperValueObject.TestConsole -{ - [WrapperValueObject] // Is Guid by default - public readonly partial struct MatchId - { - public static MatchId New() => Guid.NewGuid(); - } +[WrapperValueObject("HomeGoals", typeof(byte), "AwayGoals", typeof(byte))] +readonly partial struct MatchResult { } - [WrapperValueObject("HomeGoals", typeof(byte), "AwayGoals", typeof(byte))] - public readonly partial struct MatchResult - { - } - - public partial struct Match - { - public readonly MatchId MatchId { get; } +partial struct Match +{ + public readonly MatchId MatchId { get; } - public MatchResult Result { get; private set; } + public MatchResult Result { get; private set; } - public void SetResult(MatchResult result) => Result = result; + public void SetResult(MatchResult result) => Result = result; - public Match(in MatchId matchId) - { - MatchId = matchId; - Result = default; - } + public Match(in MatchId matchId) + { + MatchId = matchId; + Result = default; } +} - public static class Program - { - static void Main() - { - var match = new Match(MatchId.New()); +var match = new Match(MatchId.New()); - match.SetResult((1, 2)); - match.SetResult(new MatchResult(1, 2)); +match.SetResult((1, 2)); // Complex types use value tuples underneath, so can be implicitly converted +match.SetResult(new MatchResult(1, 2)); // Or the full constructor - var otherResult = new MatchResult(2, 1); +var otherResult = new MatchResult(2, 1); - Debug.Assert(otherResult != match.Result); +Debug.Assert(otherResult != match.Result); - match.SetResult((2, 1)); - Debug.Assert(otherResult == match.Result); +match.SetResult((2, 1)); +Debug.Assert(otherResult == match.Result); - Debug.Assert(match.MatchId != default); - Debug.Assert(match.Result != default); - Debug.Assert(match.Result.HomeGoals == 2); - Debug.Assert(match.Result.AwayGoals == 1); - } - } -} +Debug.Assert(match.MatchId != default); +Debug.Assert(match.Result != default); +Debug.Assert(match.Result.HomeGoals == 2); +Debug.Assert(match.Result.AwayGoals == 1); ``` ## TODO/under consideration diff --git a/src/WrapperValueObject.Generator/Generator.cs b/src/WrapperValueObject.Generator/Generator.cs index 46c7523..56d4d52 100755 --- a/src/WrapperValueObject.Generator/Generator.cs +++ b/src/WrapperValueObject.Generator/Generator.cs @@ -210,12 +210,16 @@ private bool GenerateWrapper(in GenerationContext context) var innerType = string.Empty; var isMathType = false; var isSingleType = context.InnerTypes.Count() == 1; + var isDefaultIdCase = false; if (isSingleType) { // If we have 1 type, we might be able to safely generate math operations var singleType = context.InnerTypes.Single(); innerType = $"{singleType.Type!.ContainingNamespace}.{singleType.Type.Name}"; isMathType = MathTypes!.Contains(innerType); + isDefaultIdCase = innerType == "System.Guid"; + + //System.Diagnostics.Debugger.Launch(); } else { @@ -268,6 +272,13 @@ namespace {context.Type.ContainingNamespace} "); + if (isDefaultIdCase) + { + context.SourceBuilder.AppendLine(@$" + public static {context.Type.Name} New() => Guid.NewGuid(); +"); + } + if (isSingleType) { context.SourceBuilder.AppendLine(@$" diff --git a/test/WrapperValueObject.TestConsole/Program.cs b/test/WrapperValueObject.TestConsole/Program.cs index 255c7be..0503d16 100755 --- a/test/WrapperValueObject.TestConsole/Program.cs +++ b/test/WrapperValueObject.TestConsole/Program.cs @@ -6,7 +6,6 @@ namespace WrapperValueObject.TestConsole [WrapperValueObject] public readonly partial struct MatchId { - public static MatchId New() => Guid.NewGuid(); } [WrapperValueObject("HomeGoals", typeof(byte), "AwayGoals", typeof(byte))] diff --git a/test/WrapperValueObject.TestConsole/WrapperValueObject.TestConsole.csproj b/test/WrapperValueObject.TestConsole/WrapperValueObject.TestConsole.csproj index 2fd4c5e..660e8f4 100755 --- a/test/WrapperValueObject.TestConsole/WrapperValueObject.TestConsole.csproj +++ b/test/WrapperValueObject.TestConsole/WrapperValueObject.TestConsole.csproj @@ -8,15 +8,15 @@ false - + - + diff --git a/test/WrapperValueObject.Tests/MetricTypesTests.cs b/test/WrapperValueObject.Tests/MetricTypesTests.cs new file mode 100644 index 0000000..71221f6 --- /dev/null +++ b/test/WrapperValueObject.Tests/MetricTypesTests.cs @@ -0,0 +1,77 @@ +using Xunit; + +namespace WrapperValueObject.Tests +{ + [WrapperValueObject(typeof(int))] + public readonly partial struct MeterLength + { + public static implicit operator CentimeterLength(MeterLength meter) => meter.Value * 100; + } + + [WrapperValueObject(typeof(int))] + public readonly partial struct CentimeterLength + { + public static implicit operator MeterLength(CentimeterLength centiMeter) => centiMeter.Value / 100; + } + + public class MetricTypesTests + { + [Fact] + public void Test_Conversion() + { + MeterLength meters = 2; + + CentimeterLength centiMeters = meters; + + Assert.Equal(200, (int)centiMeters); + } + + [Fact] + public void Test_Add() + { + MeterLength meters = 2; + + var result = meters + 2; + + Assert.Equal(((int)meters) + 2, (int)result); + Assert.True(meters != result); + Assert.True(meters == 2); + } + + [Fact] + public void Test_Subtract() + { + MeterLength meters = 5; + + var result = meters - 2; + + Assert.Equal(((int)meters) - 2, (int)result); + Assert.True(meters != result); + Assert.True(meters == 5); + } + + [Fact] + public void Test_Multiply() + { + MeterLength meters = 5; + + var result = meters * 2; + + Assert.Equal(((int)meters) * 2, (int)result); + Assert.True(meters != result); + Assert.True(meters == 5); + } + + [Fact] + public void Test_Divide() + { + MeterLength meters = 2; + + var result = meters / 2; + + Assert.Equal(((int)meters) / 2, (int)result); + Assert.True(meters != result); + Assert.True(meters == 2); + } + } +} diff --git a/test/WrapperValueObject.Tests/MoneyTypeTests.cs b/test/WrapperValueObject.Tests/MoneyTypeTests.cs new file mode 100644 index 0000000..e435449 --- /dev/null +++ b/test/WrapperValueObject.Tests/MoneyTypeTests.cs @@ -0,0 +1,62 @@ +using Xunit; + +namespace WrapperValueObject.Tests +{ + [WrapperValueObject(typeof(decimal))] + public readonly partial struct Money + { + } + + public class MoneyTypeTests + { + [Fact] + public void Test_Add() + { + Money money = 2m; + + var result = money + 2m; + var result2 = money + new Money(2m); + + Assert.True(result == result2); + Assert.Equal(((decimal)money) + 2m, (decimal)result); + Assert.True(money != result); + Assert.True(money == 2m); + } + + [Fact] + public void Test_Subtract() + { + Money money = 5m; + + var result = money - 2m; + + Assert.Equal(((decimal)money) - 2m, (decimal)result); + Assert.True(money != result); + Assert.True(money == 5m); + } + + [Fact] + public void Test_Multiply() + { + Money money = 5m; + + var result = money * 2m; + + Assert.Equal(((decimal)money) * 2m, (decimal)result); + Assert.True(money != result); + Assert.True(money == 5m); + } + + [Fact] + public void Test_Divide() + { + Money money = 2m; + + var result = money / 2m; + + Assert.Equal(((decimal)money) / 2m, (decimal)result); + Assert.True(money != result); + Assert.True(money == 2m); + } + } +} diff --git a/test/WrapperValueObject.Tests/ProductIdTypeTests.cs b/test/WrapperValueObject.Tests/ProductIdTypeTests.cs new file mode 100644 index 0000000..459d546 --- /dev/null +++ b/test/WrapperValueObject.Tests/ProductIdTypeTests.cs @@ -0,0 +1,25 @@ +using System; +using Xunit; + +namespace WrapperValueObject.Tests +{ + [WrapperValueObject] readonly partial struct ProductId { } + + public class ProductIdTypeTests + { + [Fact] + public void Test_New() + { + var id = ProductId.New(); + + Assert.NotEqual(Guid.Empty, (Guid)id); + + var id2 = id; + + Assert.Equal(id2, id); + Assert.True(id2 == id); + Assert.NotEqual(ProductId.New(), id); + Assert.True(ProductId.New() != id); + } + } +} diff --git a/test/WrapperValueObject.Tests/SimpleTypes.cs b/test/WrapperValueObject.Tests/SimpleTypes.cs index d553ffb..134373b 100644 --- a/test/WrapperValueObject.Tests/SimpleTypes.cs +++ b/test/WrapperValueObject.Tests/SimpleTypes.cs @@ -13,30 +13,8 @@ public readonly partial struct LeagueId { } - [WrapperValueObject(typeof(int))] - public readonly partial struct MeterLength - { - public static implicit operator CentimeterLength(MeterLength meter) => meter.Value * 100; - } - - [WrapperValueObject(typeof(int))] - public readonly partial struct CentimeterLength - { - public static implicit operator MeterLength(CentimeterLength centiMeter) => centiMeter.Value / 100; - } - public class SimpleTypes { - [Fact] - public void Test_Metric_Types() - { - MeterLength meters = 2; - - CentimeterLength centiMeters = meters; - - Assert.Equal(200, (int)centiMeters); - } - [Fact] public void Test_Guid_Type_Equals() {