From 7879ba6afc2c462e17bbef590b827bc34f5980e3 Mon Sep 17 00:00:00 2001 From: Jean-Sebastien Carle <29762210+jscarle@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:32:56 -0400 Subject: [PATCH] Added GeneratedIdentifier. --- .github/workflows/publish.yml | 5 + .../ComparisonBenchmarks.csproj | 10 +- LightResults.Extensions.sln | 20 + README.md | 5 + docfx/docs/generatedidentifier.md | 7 + .../Common/Declaration.cs | 59 ++ .../Common/DeclarationExtensions.cs | 53 ++ .../Common/DeclarationType.cs | 23 + .../Common/EquatableImmutableArray.cs | 15 + .../Common/EquatableImmutableArray`1.cs | 87 +++ .../IncrementalValueProviderExtensions.cs | 13 + .../Common/SymbolExtensions.cs | 100 ++++ .../GeneratedIdentifierSourceGenerator.cs | 527 ++++++++++++++++++ ...ults.Extensions.GeneratedIdentifier.csproj | 85 +++ .../Properties/launchSettings.json | 9 + .../Identifiers/TestGuidId.cs | 6 + .../Identifiers/TestIntId.cs | 6 + .../Identifiers/TestShortId.cs | 6 + .../Interfaces/ICloneableValueObject.cs | 9 + .../Interfaces/IConvertibleValueObject.cs | 17 + .../Interfaces/ICreatableValueObject.cs | 17 + .../Interfaces/IParsableValueObject.cs | 19 + .../Interfaces/IValueObject.cs | 14 + .../ValueObjectValidationException.cs | 27 + ...nsions.GeneratedIdentifier.Fixtures.csproj | 28 + .../Program.cs | 8 + .../Properties/launchSettings.json | 12 + ...eGuidIdentifier_WithNamespace.verified.txt | 230 ++++++++ ...idIdentifier_WithoutNamespace.verified.txt | 228 ++++++++ ...teIntIdentifier_WithNamespace.verified.txt | 233 ++++++++ ...ntIdentifier_WithoutNamespace.verified.txt | 231 ++++++++ ...ShortIdentifier_WithNamespace.verified.txt | 233 ++++++++ ...rtIdentifier_WithoutNamespace.verified.txt | 231 ++++++++ ...GeneratedIdentifierSourceGeneratorTests.cs | 104 ++++ ...xtensions.GeneratedIdentifier.Tests.csproj | 66 +++ .../ModuleInitializer.cs | 16 + .../TestGuidIdTest.cs | 329 +++++++++++ .../TestIntIdTest.cs | 353 ++++++++++++ .../TestShortIdTest.cs | 353 ++++++++++++ tools/Benchmarks/Benchmarks.csproj | 4 +- 40 files changed, 3791 insertions(+), 7 deletions(-) create mode 100644 docfx/docs/generatedidentifier.md create mode 100644 src/LightResults.Extensions.GeneratedIdentifier/Common/Declaration.cs create mode 100644 src/LightResults.Extensions.GeneratedIdentifier/Common/DeclarationExtensions.cs create mode 100644 src/LightResults.Extensions.GeneratedIdentifier/Common/DeclarationType.cs create mode 100644 src/LightResults.Extensions.GeneratedIdentifier/Common/EquatableImmutableArray.cs create mode 100644 src/LightResults.Extensions.GeneratedIdentifier/Common/EquatableImmutableArray`1.cs create mode 100644 src/LightResults.Extensions.GeneratedIdentifier/Common/IncrementalValueProviderExtensions.cs create mode 100644 src/LightResults.Extensions.GeneratedIdentifier/Common/SymbolExtensions.cs create mode 100644 src/LightResults.Extensions.GeneratedIdentifier/GeneratedIdentifierSourceGenerator.cs create mode 100644 src/LightResults.Extensions.GeneratedIdentifier/LightResults.Extensions.GeneratedIdentifier.csproj create mode 100644 src/LightResults.Extensions.GeneratedIdentifier/Properties/launchSettings.json create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestGuidId.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestIntId.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestShortId.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ICloneableValueObject.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IConvertibleValueObject.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ICreatableValueObject.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IParsableValueObject.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IValueObject.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ValueObjectValidationException.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/LightResults.Extensions.GeneratedIdentifier.Fixtures.csproj create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Program.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Properties/launchSettings.json create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateGuidIdentifier_WithNamespace.verified.txt create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateGuidIdentifier_WithoutNamespace.verified.txt create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateIntIdentifier_WithNamespace.verified.txt create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateIntIdentifier_WithoutNamespace.verified.txt create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateShortIdentifier_WithNamespace.verified.txt create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateShortIdentifier_WithoutNamespace.verified.txt create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/LightResults.Extensions.GeneratedIdentifier.Tests.csproj create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/ModuleInitializer.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestGuidIdTest.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestIntIdTest.cs create mode 100644 tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestShortIdTest.cs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 478276d..f8e6e1a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -60,5 +60,10 @@ jobs: dotnet build ./src/LightResults.Extensions.Json/LightResults.Extensions.Json.csproj --configuration Release --no-restore dotnet pack ./src/LightResults.Extensions.Json/LightResults.Extensions.Json.csproj --configuration Release --no-build --output . + - name: Pack GeneratedIdentifier + run: | + dotnet build ./src/LightResults.Extensions.Json/LightResults.Extensions.GeneratedIdentifier.csproj --configuration Release --no-restore + dotnet pack ./src/LightResults.Extensions.Json/LightResults.Extensions.GeneratedIdentifier.csproj --configuration Release --no-build --output . + - name: Push to NuGet run: dotnet nuget push "*.nupkg" --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/ComparisonBenchmarks/ComparisonBenchmarks.csproj b/ComparisonBenchmarks/ComparisonBenchmarks.csproj index bf78d27..8ae5d28 100644 --- a/ComparisonBenchmarks/ComparisonBenchmarks.csproj +++ b/ComparisonBenchmarks/ComparisonBenchmarks.csproj @@ -11,14 +11,14 @@ - - - - + + + + - + diff --git a/LightResults.Extensions.sln b/LightResults.Extensions.sln index dacdd2c..4fb9a40 100644 --- a/LightResults.Extensions.sln +++ b/LightResults.Extensions.sln @@ -20,6 +20,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmarks", "tools\Benchma EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightResults.Extensions.Operations.Tests", "tests\LightResults.Extensions.Operations.Tests\LightResults.Extensions.Operations.Tests.csproj", "{D4E8F259-596E-412D-A757-769383865764}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightResults.Extensions.GeneratedIdentifier", "src\LightResults.Extensions.GeneratedIdentifier\LightResults.Extensions.GeneratedIdentifier.csproj", "{2D558C96-F052-4959-B996-5B03A66E4FCF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightResults.Extensions.GeneratedIdentifier.Fixtures", "tests\LightResults.Extensions.GeneratedIdentifier.Fixtures\LightResults.Extensions.GeneratedIdentifier.Fixtures.csproj", "{06BDEA4C-9036-44B7-AB44-DBD088556726}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightResults.Extensions.GeneratedIdentifier.Tests", "tests\LightResults.Extensions.GeneratedIdentifier.Tests\LightResults.Extensions.GeneratedIdentifier.Tests.csproj", "{45AF3136-2F1A-48C5-A76F-52635D7F0B90}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,6 +64,18 @@ Global {D4E8F259-596E-412D-A757-769383865764}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4E8F259-596E-412D-A757-769383865764}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4E8F259-596E-412D-A757-769383865764}.Release|Any CPU.Build.0 = Release|Any CPU + {2D558C96-F052-4959-B996-5B03A66E4FCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D558C96-F052-4959-B996-5B03A66E4FCF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D558C96-F052-4959-B996-5B03A66E4FCF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D558C96-F052-4959-B996-5B03A66E4FCF}.Release|Any CPU.Build.0 = Release|Any CPU + {06BDEA4C-9036-44B7-AB44-DBD088556726}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06BDEA4C-9036-44B7-AB44-DBD088556726}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06BDEA4C-9036-44B7-AB44-DBD088556726}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06BDEA4C-9036-44B7-AB44-DBD088556726}.Release|Any CPU.Build.0 = Release|Any CPU + {45AF3136-2F1A-48C5-A76F-52635D7F0B90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45AF3136-2F1A-48C5-A76F-52635D7F0B90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45AF3136-2F1A-48C5-A76F-52635D7F0B90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45AF3136-2F1A-48C5-A76F-52635D7F0B90}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {93C8BC67-7F34-4935-A671-F0B12AEDB072} = {A6C9FED0-42B6-488C-8961-DE13F291434B} @@ -65,5 +83,7 @@ Global {93C8BC67-7F34-4935-A671-F0B12AEDB072} = {A6C9FED0-42B6-488C-8961-DE13F291434B} {6DC31D8F-B71A-4A08-A892-5593F54EB82C} = {D10E009A-B3B8-4FB3-8B01-F21C383CD513} {D4E8F259-596E-412D-A757-769383865764} = {A6C9FED0-42B6-488C-8961-DE13F291434B} + {06BDEA4C-9036-44B7-AB44-DBD088556726} = {A6C9FED0-42B6-488C-8961-DE13F291434B} + {45AF3136-2F1A-48C5-A76F-52635D7F0B90} = {A6C9FED0-42B6-488C-8961-DE13F291434B} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 9f88748..38714d0 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,11 @@ Extensions for [LightResults](https://github.com/jscarle/LightResults), an extre [![nuget](https://img.shields.io/nuget/v/LightResults.Extensions.Json)](https://www.nuget.org/packages/LightResults.Extensions.Json) [![downloads](https://img.shields.io/nuget/dt/LightResults.Extensions.Json)](https://www.nuget.org/packages/LightResults.Extensions.Json) +- [Json](https://jscarle.github.io/LightResults.Extensions/docs/generatedidentifier.html) - Provides strongly-typed identifiers. + + [![nuget](https://img.shields.io/nuget/v/LightResults.Extensions.GeneratedIdentifier)](https://www.nuget.org/packages/LightResults.Extensions.GeneratedIdentifier) + [![downloads](https://img.shields.io/nuget/dt/LightResults.Extensions.GeneratedIdentifier)](https://www.nuget.org/packages/LightResults.Extensions.GeneratedIdentifier) + ## Documentation Make sure to [read the docs](https://jscarle.github.io/LightResults.Extensions/) for the full API. diff --git a/docfx/docs/generatedidentifier.md b/docfx/docs/generatedidentifier.md new file mode 100644 index 0000000..01b4f9f --- /dev/null +++ b/docfx/docs/generatedidentifier.md @@ -0,0 +1,7 @@ +# GeneratedIdentifier + +A Roslyn source generator that provides strongly-typed identifiers using LightResults. + +[![main](https://img.shields.io/github/actions/workflow/status/jscarle/LightResults.Extensions/main.yml?logo=github)](https://github.com/jscarle/LightResults.Extensions) +[![nuget](https://img.shields.io/nuget/v/LightResults.Extensions.GeneratedIdentifier)](https://www.nuget.org/packages/LightResults.Extensions.GeneratedIdentifier) +[![downloads](https://img.shields.io/nuget/dt/LightResults.Extensions.GeneratedIdentifier)](https://www.nuget.org/packages/LightResults.Extensions.GeneratedIdentifier) diff --git a/src/LightResults.Extensions.GeneratedIdentifier/Common/Declaration.cs b/src/LightResults.Extensions.GeneratedIdentifier/Common/Declaration.cs new file mode 100644 index 0000000..e562850 --- /dev/null +++ b/src/LightResults.Extensions.GeneratedIdentifier/Common/Declaration.cs @@ -0,0 +1,59 @@ +namespace LightResults.Extensions.GeneratedIdentifier.Common; + +/// Represents a declaration. +public sealed record Declaration +{ + /// Initializes a new instance of the class with the specified type, name, and generic parameters. + /// The type of declaration. + /// The name of the declaration. + /// A read-only list of generic parameter names, or an empty list if not generic. + internal Declaration(DeclarationType type, string name, EquatableImmutableArray genericParameters) + { + Type = type; + Name = name; + GenericParameters = genericParameters; + } + + /// Gets the type of declaration. + public DeclarationType Type { get; } + + /// Gets the name of the declaration. + public string Name { get; } + + /// Gets a read-only list of generic parameter names for generic declarations, or an empty list otherwise. + public EquatableImmutableArray GenericParameters { get; } + + /// Returns a string representation of the declaration in the appropriate format for its type. + /// A string representation of the declaration. + public override string ToString() + { + switch (Type) + { + case DeclarationType.Namespace: + return $"namespace {Name}"; + case DeclarationType.Interface: + case DeclarationType.Class: + case DeclarationType.Record: + case DeclarationType.Struct: + case DeclarationType.RecordStruct: + var keyword = ToKeyword(Type); + return GenericParameters.Count == 0 ? $"{keyword} {Name}" : $"{keyword} {Name}<{string.Join(", ", GenericParameters)}>"; + default: + return base.ToString(); + } + } + + private static string ToKeyword(DeclarationType declarationType) + { + return declarationType switch + { + DeclarationType.Namespace => "namespace", + DeclarationType.Interface => "interface", + DeclarationType.Class => "class", + DeclarationType.Record => "record", + DeclarationType.Struct => "struct", + DeclarationType.RecordStruct => "record struct", + _ => throw new InvalidOperationException(), + }; + } +} diff --git a/src/LightResults.Extensions.GeneratedIdentifier/Common/DeclarationExtensions.cs b/src/LightResults.Extensions.GeneratedIdentifier/Common/DeclarationExtensions.cs new file mode 100644 index 0000000..16db73b --- /dev/null +++ b/src/LightResults.Extensions.GeneratedIdentifier/Common/DeclarationExtensions.cs @@ -0,0 +1,53 @@ +using System.Text; + +namespace LightResults.Extensions.GeneratedIdentifier.Common; + +/// Provides extension methods for working with declarations. +internal static class DeclarationExtensions +{ + /// Converts a list of declarations to their corresponding namespace. + /// The list of declarations to convert. + /// The namespace represented by the declarations. + public static string ToNamespace(this EquatableImmutableArray declarations) + { + var builder = new StringBuilder(); + + for (var index = 0; index < declarations.Count; index++) + { + var declaration = declarations[index]; + if (declaration.Type != DeclarationType.Namespace) + continue; + + if (builder.Length > 0) + builder.Append('.'); + builder.Append(declaration.Name); + } + + return builder.ToString(); + } + + /// Converts a list of declarations to their fully qualified name. + /// The list of declarations to convert. + /// The fully qualified name represented by the declarations. + public static string ToFullyQualifiedName(this EquatableImmutableArray declarations) + { + var builder = new StringBuilder(); + + for (var index = 0; index < declarations.Count; index++) + { + var declaration = declarations[index]; + + if (builder.Length > 0) + builder.Append('.'); + builder.Append(declaration.Name); + + if (declaration.GenericParameters.Count == 0) + continue; + + builder.Append('`'); + builder.Append(declaration.GenericParameters.Count); + } + + return builder.ToString(); + } +} diff --git a/src/LightResults.Extensions.GeneratedIdentifier/Common/DeclarationType.cs b/src/LightResults.Extensions.GeneratedIdentifier/Common/DeclarationType.cs new file mode 100644 index 0000000..7d971d1 --- /dev/null +++ b/src/LightResults.Extensions.GeneratedIdentifier/Common/DeclarationType.cs @@ -0,0 +1,23 @@ +namespace LightResults.Extensions.GeneratedIdentifier.Common; + +/// Specifies the kind of declaration. +public enum DeclarationType +{ + /// Represents a namespace declaration. + Namespace = 0, + + /// Represents an interface declaration. + Interface = 1, + + /// Represents a class declaration. + Class = 2, + + /// Represents a record declaration. + Record = 3, + + /// Represents a struct declaration. + Struct = 4, + + /// Represents a record struct declaration. + RecordStruct = 5, +} diff --git a/src/LightResults.Extensions.GeneratedIdentifier/Common/EquatableImmutableArray.cs b/src/LightResults.Extensions.GeneratedIdentifier/Common/EquatableImmutableArray.cs new file mode 100644 index 0000000..6664e9d --- /dev/null +++ b/src/LightResults.Extensions.GeneratedIdentifier/Common/EquatableImmutableArray.cs @@ -0,0 +1,15 @@ +using System.Collections.Immutable; + +namespace LightResults.Extensions.GeneratedIdentifier.Common; + +/// Provides extension methods to convert various collections to an . +internal static class EquatableImmutableArray +{ + /// Converts an to an . + /// The to convert. + /// An containing the same elements as the original enumerable. + public static EquatableImmutableArray ToEquatableImmutableArray(this IEnumerable enumerable) + { + return new EquatableImmutableArray(enumerable.ToImmutableArray()); + } +} diff --git a/src/LightResults.Extensions.GeneratedIdentifier/Common/EquatableImmutableArray`1.cs b/src/LightResults.Extensions.GeneratedIdentifier/Common/EquatableImmutableArray`1.cs new file mode 100644 index 0000000..ccd0315 --- /dev/null +++ b/src/LightResults.Extensions.GeneratedIdentifier/Common/EquatableImmutableArray`1.cs @@ -0,0 +1,87 @@ +using System.Collections; +using System.Collections.Immutable; + +namespace LightResults.Extensions.GeneratedIdentifier.Common; + +/// Represents an immutable array that implements . +/// The type of elements in the array. +public readonly struct EquatableImmutableArray : IEquatable>, IReadOnlyList +{ + /// Gets an empty . + internal static EquatableImmutableArray Empty { get; } = new(ImmutableArray.Empty); + + /// Gets the number of elements in the array. + public int Count => Array.Length; + + /// Gets the element at the specified index. + /// The zero-based index of the element to get. + /// The element at the specified index. + public T this[int index] => Array[index]; + + private ImmutableArray Array => _array ?? ImmutableArray.Empty; + private readonly ImmutableArray? _array; + + internal EquatableImmutableArray(ImmutableArray? array) + { + _array = array; + } + + /// + public bool Equals(EquatableImmutableArray other) + { + return this.SequenceEqual(other); + } + + /// + public override bool Equals(object? obj) + { + return obj is EquatableImmutableArray other && Equals(other); + } + + /// + public override int GetHashCode() + { + var hashCode = new HashCode(); + + foreach (var item in Array) + hashCode.Add(item); + + return hashCode.ToHashCode(); + } + + /// Determines whether two instances are equal. + /// The first to compare. + /// The second to compare. + /// if the two instances are equal; otherwise, . + public static bool operator ==(EquatableImmutableArray left, EquatableImmutableArray right) + { + return left.Equals(right); + } + + /// Determines whether two instances are not equal. + /// The first to compare. + /// The second to compare. + /// if the two instances are not equal; otherwise, . + public static bool operator !=(EquatableImmutableArray left, EquatableImmutableArray right) + { + return !left.Equals(right); + } + + /// + public IEnumerator GetEnumerator() + { + // ReSharper disable once ForCanBeConvertedToForeach + // ReSharper disable once LoopCanBeConvertedToQuery + for (var index = 0; index < Array.Length; index++) + { + var item = Array[index]; + yield return item; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/src/LightResults.Extensions.GeneratedIdentifier/Common/IncrementalValueProviderExtensions.cs b/src/LightResults.Extensions.GeneratedIdentifier/Common/IncrementalValueProviderExtensions.cs new file mode 100644 index 0000000..2f18bd3 --- /dev/null +++ b/src/LightResults.Extensions.GeneratedIdentifier/Common/IncrementalValueProviderExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; + +namespace LightResults.Extensions.GeneratedIdentifier.Common; + +internal static class IncrementalValueProviderExtensions +{ + public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider source) + where TSource : struct + { + return source.Where(x => x is not null) + .Select((x, _) => x!.Value); + } +} diff --git a/src/LightResults.Extensions.GeneratedIdentifier/Common/SymbolExtensions.cs b/src/LightResults.Extensions.GeneratedIdentifier/Common/SymbolExtensions.cs new file mode 100644 index 0000000..7460ab5 --- /dev/null +++ b/src/LightResults.Extensions.GeneratedIdentifier/Common/SymbolExtensions.cs @@ -0,0 +1,100 @@ +using Microsoft.CodeAnalysis; + +namespace LightResults.Extensions.GeneratedIdentifier.Common; + +/// Provides extension methods for working with symbols. +internal static class SymbolExtensions +{ + /// Gets a list of declarations representing the hierarchy containing the given symbol. + /// The to get the containing declarations for. + /// The cancellation token. + /// An of objects representing the hierarchy. + public static EquatableImmutableArray GetContainingDeclarations(this ISymbol symbol, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var declarations = new Stack(); + + BuildContainingSymbolHierarchy(symbol, in declarations, cancellationToken); + + return declarations.ToEquatableImmutableArray(); + } + + private static void BuildContainingSymbolHierarchy(ISymbol symbol, in Stack declarations, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + switch (symbol.ContainingSymbol) + { + case INamespaceSymbol namespaceSymbol: + BuildNamespaceHierarchy(namespaceSymbol, declarations, cancellationToken); + break; + case INamedTypeSymbol namedTypeSymbol: + BuildTypeHierarchy(namedTypeSymbol, declarations, cancellationToken); + break; + } + } + + private static void BuildNamespaceHierarchy(INamespaceSymbol symbol, in Stack declarations, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!symbol.IsGlobalNamespace) + { + var namespaceDeclaration = new Declaration(DeclarationType.Namespace, symbol.Name, EquatableImmutableArray.Empty); + declarations.Push(namespaceDeclaration); + } + + if (symbol.ContainingNamespace is not null && !symbol.ContainingNamespace.IsGlobalNamespace) + BuildNamespaceHierarchy(symbol.ContainingNamespace, declarations, cancellationToken); + } + + private static void BuildTypeHierarchy(INamedTypeSymbol symbol, in Stack declarations, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var declarationType = symbol.GetDeclarationType(cancellationToken); + if (declarationType is null) + return; + + var genericTypeParameters = symbol.GetGenericTypeParameters(cancellationToken); + + var typeDeclaration = new Declaration(declarationType.Value, symbol.Name, genericTypeParameters); + declarations.Push(typeDeclaration); + + BuildContainingSymbolHierarchy(symbol, declarations, cancellationToken); + } + + private static EquatableImmutableArray GetGenericTypeParameters(this INamedTypeSymbol symbol, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (!symbol.IsGenericType) + return EquatableImmutableArray.Empty; + + var genericTypeParameters = new List(); + + for (var index = 0; index < symbol.TypeParameters.Length; index++) + { + var typeParameter = symbol.TypeParameters[index]; + genericTypeParameters.Add(typeParameter.Name); + } + + return genericTypeParameters.ToEquatableImmutableArray(); + } + + private static DeclarationType? GetDeclarationType(this ITypeSymbol symbol, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return symbol switch + { + { IsReferenceType: true, TypeKind: TypeKind.Interface } => DeclarationType.Interface, + { IsReferenceType: true, IsRecord: true } => DeclarationType.Record, + { IsReferenceType: true } => DeclarationType.Class, + { IsValueType: true, IsRecord: true } => DeclarationType.RecordStruct, + { IsValueType: true } => DeclarationType.Struct, + _ => null, + }; + } +} diff --git a/src/LightResults.Extensions.GeneratedIdentifier/GeneratedIdentifierSourceGenerator.cs b/src/LightResults.Extensions.GeneratedIdentifier/GeneratedIdentifierSourceGenerator.cs new file mode 100644 index 0000000..59025a1 --- /dev/null +++ b/src/LightResults.Extensions.GeneratedIdentifier/GeneratedIdentifierSourceGenerator.cs @@ -0,0 +1,527 @@ +using System.Collections.Immutable; +using System.Text; +using LightResults.Extensions.GeneratedIdentifier.Common; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace LightResults.Extensions.GeneratedIdentifier; + +[Generator] +public sealed class GeneratedIdentifierSourceGenerator : IIncrementalGenerator +{ + private const string AttributesNamespace = "LightResults.Extensions.GeneratedIdentifier"; + private const string GeneratedIdentifierAttributeName = "GeneratedIdentifierAttribute"; + private const string GeneratedIdentifierAttributeFullyQualifiedName = $"{AttributesNamespace}.{GeneratedIdentifierAttributeName}`1"; + private const string GeneratedIdentifierAttributeHint = $"{GeneratedIdentifierAttributeFullyQualifiedName}.g.cs"; + + private static readonly string FileHeader = $""" + //----------------------------------------------------------------------------- + // + // This code was generated by {nameof(GeneratedIdentifierSourceGenerator)} which + // can be found in the {typeof(GeneratedIdentifierSourceGenerator).Namespace} namespace. + // + // Changes to this file may cause incorrect behavior + // and will be lost if the code is regenerated. + // + //----------------------------------------------------------------------------- + #nullable enable + """; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(RegisterAttributes); + + var generatedIdentifiers = context.SyntaxProvider + .ForAttributeWithMetadataName(GeneratedIdentifierAttributeFullyQualifiedName, Filter, Transform) + .WhereNotNull() + .Collect(); + + context.RegisterSourceOutput(generatedIdentifiers, GenerateIdentifier); + } + + private static void RegisterAttributes(IncrementalGeneratorPostInitializationContext context) + { + var source = $""" + {FileHeader} + + using System; + + namespace {AttributesNamespace}; + + [AttributeUsage(AttributeTargets.Struct)] + public sealed class {GeneratedIdentifierAttributeName} : Attribute; + """; + context.AddSource(GeneratedIdentifierAttributeHint, SourceText.From(source, Encoding.UTF8)); + } + + private static bool Filter(SyntaxNode syntaxNode, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return syntaxNode.IsKind(SyntaxKind.StructDeclaration); + } + + private static Identifier? Transform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (context.TargetSymbol is not INamedTypeSymbol namedTypeSymbol) + return null; + + var containingDeclarations = namedTypeSymbol.GetContainingDeclarations(cancellationToken); + var symbolName = namedTypeSymbol.Name; + + var attribute = context.Attributes[0].AttributeClass!; + if (attribute.TypeArguments.Length != 1) + return null; + + var typeArgument = attribute.TypeArguments[0]; + + string? declaredValueType; + string? fullValueType; + switch (typeArgument.SpecialType) + { + case SpecialType.System_Int16: + declaredValueType = "short"; + fullValueType = "Int16"; + break; + case SpecialType.System_Int32: + declaredValueType = "int"; + fullValueType = "Int32"; + break; + case SpecialType.System_Int64: + declaredValueType = "long"; + fullValueType = "Int64"; + break; + default: + if (typeArgument is not { Name: "Guid", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } }) + return null; + declaredValueType = "Guid"; + fullValueType = "Guid"; + break; + } + + var symbol = new Identifier(containingDeclarations, symbolName, declaredValueType, fullValueType); + + return symbol; + } + + private static void GenerateIdentifier(SourceProductionContext context, ImmutableArray generatedIdentifiers) + { + foreach (var symbol in generatedIdentifiers) + { + var structNamespace = symbol.ContainingDeclarations.ToNamespace(); + var structName = symbol.Name; + var declaredValueType = symbol.DeclaredValueType; + var fullValueType = symbol.FullValueType; + + var source = new StringBuilder(); + + source.AppendLine($""" + //----------------------------------------------------------------------------- + // + // This code was generated by {nameof(GeneratedIdentifierSourceGenerator)} which + // can be found in the {typeof(GeneratedIdentifierSourceGenerator).Namespace} namespace. + // + // Changes to this file may cause incorrect behavior + // and will be lost if the code is regenerated. + // + //----------------------------------------------------------------------------- + + #nullable enable + + using System.ComponentModel; + using System.Globalization; + using System.Text.Json; + using System.Text.Json.Serialization; + using LightResults; + using GeneratedIdentifier.Common.ValueObjects; + + """ + ); + + if (structNamespace.Length > 0) + source.AppendLine($""" + namespace {structNamespace}; + + """ + ); + + source.AppendLine($$""" + [TypeConverter(typeof({{structName}}TypeConverter))] + [JsonConverter(typeof({{structName}}JsonConverter))] + readonly partial struct {{structName}} : + ICreatableValueObject<{{declaredValueType}}, {{structName}}>, + IParsableValueObject<{{structName}}>, + IValueObject<{{declaredValueType}}, {{structName}}>, + IComparable<{{structName}}>, + IComparable + { + """ + ); + + source.AppendLine(""" + /// Gets whether this identifier is the default value. + public bool IsDefault => _value == default; + + """ + ); + + source.AppendLine($""" + {declaredValueType} IValueObject<{declaredValueType}, {structName}>.Value => _value; + + private readonly {declaredValueType} _value; + + """ + ); + + source.AppendLine($$""" + private {{structName}}({{declaredValueType}} value, bool skipValidation = false) + { + if (!skipValidation) + ValueObjectException.ThrowIfFailed(Validate(value)); + + _value = value; + } + + """ + ); + + source.AppendLine($$""" + /// + public static {{structName}} Create({{declaredValueType}} value) + { + var result = TryCreate(value); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + """ + ); + + source.AppendLine($$""" + /// + public static Result<{{structName}}> TryCreate({{declaredValueType}} value) + { + var validation = Validate(value); + if (validation.IsFailed(out var error)) + return Result.Fail<{{structName}}>(error); + + return Result.Ok<{{structName}}>(new {{structName}}(value, true)); + } + + """ + ); + + source.AppendLine($$""" + /// + public static {{structName}} Parse(string s) + { + var result = TryParse(s); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + """ + ); + + source.AppendLine($$""" + /// + public static Result<{{structName}}> TryParse(string s) + { + if ({{declaredValueType}}.TryParse(s, out var value)) + return TryCreate(value); + + return Result.Fail<{{structName}}>("The string is not a valid identifier."); + } + + """ + ); + + source.AppendLine($$""" + /// + public static bool TryParse(string s, out {{structName}} identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + """ + ); + + source.AppendLine($$""" + /// + public static bool TryParse(string s, IFormatProvider provider, out {{structName}} identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + """ + ); + + source.AppendLine($$""" + /// + public bool Equals({{structName}} other) + { + return _value == other._value; + } + + """ + ); + + source.AppendLine($$""" + /// + public override bool Equals(object? obj) + { + return obj is {{structName}} other && Equals(other); + } + + """ + ); + + if (declaredValueType == "Guid") + source.AppendLine(""" + /// + public override int GetHashCode() + { + return _value.GetHashCode(); + } + + """ + ); + else + source.AppendLine(""" + /// + public override int GetHashCode() + { + return _value; + } + + """ + ); + + source.AppendLine($$""" + /// Determines whether two instances of are equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are equal; otherwise, false. + public static bool operator ==({{structName}} left, {{structName}} right) + { + return left.Equals(right); + } + + """ + ); + + source.AppendLine($$""" + /// Determines whether two instances of are not equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are not equal; otherwise, false. + public static bool operator !=({{structName}} left, {{structName}} right) + { + return !left.Equals(right); + } + + """ + ); + + source.AppendLine($$""" + /// + public int CompareTo({{structName}} other) + { + return _value.CompareTo(other._value); + } + + """ + ); + + source.AppendLine($$""" + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) return 1; + return obj is {{structName}} other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof({{structName}})}"); + } + + """ + ); + + source.AppendLine($$""" + /// Determines whether the first instance of is less than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than the second instance; otherwise, false. + public static bool operator <({{structName}} left, {{structName}} right) + { + return left.CompareTo(right) < 0; + } + + """ + ); + + source.AppendLine($$""" + /// Determines whether the first instance of is greater than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than the second instance; otherwise, false. + public static bool operator >({{structName}} left, {{structName}} right) + { + return left.CompareTo(right) > 0; + } + + """ + ); + + source.AppendLine($$""" + /// Determines whether the first instance of is less than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than or equal to the second instance; otherwise, false. + public static bool operator <=({{structName}} left, {{structName}} right) + { + return left.CompareTo(right) <= 0; + } + + """ + ); + + source.AppendLine($$""" + /// Determines whether the first instance of is greater than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than or equal to the second instance; otherwise, false. + public static bool operator >=({{structName}} left, {{structName}} right) + { + return left.CompareTo(right) >= 0; + } + + """ + ); + + source.AppendLine($$""" + /// Gets the underlying value of the . + /// The underlying value of the . + public {{declaredValueType}} To{{fullValueType}}() + { + return _value; + } + + """ + ); + + if (declaredValueType == "Guid") + source.AppendLine(""" + /// + public override string ToString() + { + return _value.ToString(); + } + + """ + ); + else + source.AppendLine(""" + /// + public override string ToString() + { + return _value.ToString(CultureInfo.InvariantCulture); + } + + """ + ); + + if (declaredValueType == "Guid") + source.AppendLine($$""" + private static Result Validate({{declaredValueType}} value) + { + return Result.Ok(); + } + """ + ); + else + source.AppendLine($$""" + private static Result Validate({{declaredValueType}} value) + { + if (value < 0) + return Result.Fail("The value must be equal to or greater than zero."); + + return Result.Ok(); + } + """ + ); + + source.AppendLine(""" + } + + """ + ); + + source.AppendLine($$""" + public class {{structName}}TypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof({{declaredValueType}}) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is {{declaredValueType}} identifierValue) + return {{structName}}.Create(identifierValue); + + return base.ConvertFrom(context, culture, value); + } + } + + """ + ); + source.AppendLine($$""" + public class {{structName}}JsonConverter : JsonConverter<{{structName}}> + { + public override void Write(Utf8JsonWriter writer, {{structName}} identifier, JsonSerializerOptions options) + { + var value = ((IValueObject<{{declaredValueType}}, {{structName}}>)identifier).Value; + """ + ); + + if (declaredValueType == "Guid") + source.AppendLine(""" writer.WriteStringValue(value.ToString());"""); + else + source.AppendLine(""" writer.WriteNumberValue(value);"""); + + source.Append($$""" + } + + public override {{structName}} Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.Get{{fullValueType}}(); + return {{structName}}.Create(value); + } + } + + """ + ); + + var hint = $"{symbol.ContainingDeclarations.ToFullyQualifiedName()}.{symbol.Name}.g.cs"; + context.AddSource(hint, source.ToString()); + } + } + + private readonly record struct Identifier( + EquatableImmutableArray ContainingDeclarations, + string Name, + string DeclaredValueType, + string FullValueType + ) + { + public EquatableImmutableArray ContainingDeclarations { get; } = ContainingDeclarations; + public string Name { get; } = Name; + public string DeclaredValueType { get; } = DeclaredValueType; + public string FullValueType { get; } = FullValueType; + } +} diff --git a/src/LightResults.Extensions.GeneratedIdentifier/LightResults.Extensions.GeneratedIdentifier.csproj b/src/LightResults.Extensions.GeneratedIdentifier/LightResults.Extensions.GeneratedIdentifier.csproj new file mode 100644 index 0000000..f2bcf04 --- /dev/null +++ b/src/LightResults.Extensions.GeneratedIdentifier/LightResults.Extensions.GeneratedIdentifier.csproj @@ -0,0 +1,85 @@ + + + + + LightResults.Extensions.GeneratedIdentifier + netstandard2.0 + enable + enable + latest + + + + + latest-all + true + true + true + $(NoWarn);NU5128 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + LightResults.Extensions.GeneratedIdentifier + 9.0.0-preview.1 + 9.0.0.0 + 9.0.0.0 + en-US + false + + + + + true + LightResults.Extensions.GeneratedIdentifier + LightResults.Extensions.GeneratedIdentifier + A Roslyn source generator that automatically creates strongly-typed identifier structs. + README.md + https://github.com/jscarle/LightResults.Extensions + result results pattern lightresults source-generator strongly-typed identifie + https://github.com/jscarle/LightResults.Extensions + git + Jean-Sebastien Carle + Copyright © Jean-Sebastien Carle 2024 + LICENSE.md + Icon.png + true + snupkg + + + + + + + + + + + + + true + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + diff --git a/src/LightResults.Extensions.GeneratedIdentifier/Properties/launchSettings.json b/src/LightResults.Extensions.GeneratedIdentifier/Properties/launchSettings.json new file mode 100644 index 0000000..29c3748 --- /dev/null +++ b/src/LightResults.Extensions.GeneratedIdentifier/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "DebugRoslynSourceGenerator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../GeneratedIdentifier.Sample/GeneratedIdentifier.Sample.csproj" + } + } +} \ No newline at end of file diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestGuidId.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestGuidId.cs new file mode 100644 index 0000000..ecfebad --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestGuidId.cs @@ -0,0 +1,6 @@ +using GeneratedIdentifier; + +namespace LightResults.Extensions.GeneratedIdentifier.Fixtures.Identifiers; + +[GeneratedIdentifier] +public partial struct TestGuidId; diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestIntId.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestIntId.cs new file mode 100644 index 0000000..e6c515b --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestIntId.cs @@ -0,0 +1,6 @@ +using GeneratedIdentifier; + +namespace LightResults.Extensions.GeneratedIdentifier.Fixtures.Identifiers; + +[GeneratedIdentifier] +public partial struct TestIntId; diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestShortId.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestShortId.cs new file mode 100644 index 0000000..baee8e1 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Identifiers/TestShortId.cs @@ -0,0 +1,6 @@ +using GeneratedIdentifier; + +namespace LightResults.Extensions.GeneratedIdentifier.Fixtures.Identifiers; + +[GeneratedIdentifier] +public partial struct TestShortId; diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ICloneableValueObject.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ICloneableValueObject.cs new file mode 100644 index 0000000..7651e14 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ICloneableValueObject.cs @@ -0,0 +1,9 @@ +// Resharper disable CheckNamespace + +namespace GeneratedIdentifier.Common.ValueObjects; + +public interface ICloneableValueObject : IValueObject + where TSelf : notnull +{ + TSelf Clone(); +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IConvertibleValueObject.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IConvertibleValueObject.cs new file mode 100644 index 0000000..0e1e792 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IConvertibleValueObject.cs @@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; +using LightResults; + +// Resharper disable CheckNamespace +namespace GeneratedIdentifier.Common.ValueObjects; + +[SuppressMessage( + "Design", + "CA1000: Do not declare static members on generic types", + Justification = "This is required for handling value objects generically." +)] +public interface IConvertibleValueObject : IValueObject + where TSelf : notnull +{ + static abstract TSelf Convert(TSource source); + static abstract Result TryConvert(TSource source); +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ICreatableValueObject.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ICreatableValueObject.cs new file mode 100644 index 0000000..a203424 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ICreatableValueObject.cs @@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; +using LightResults; + +// Resharper disable CheckNamespace +namespace GeneratedIdentifier.Common.ValueObjects; + +[SuppressMessage( + "Design", + "CA1000: Do not declare static members on generic types", + Justification = "This is required for handling value objects generically." +)] +public interface ICreatableValueObject : IValueObject + where TSelf : notnull +{ + static abstract TSelf Create(TValue value); + static abstract Result TryCreate(TValue value); +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IParsableValueObject.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IParsableValueObject.cs new file mode 100644 index 0000000..9979b84 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IParsableValueObject.cs @@ -0,0 +1,19 @@ +using System.Diagnostics.CodeAnalysis; +using LightResults; + +// Resharper disable CheckNamespace +namespace GeneratedIdentifier.Common.ValueObjects; + +[SuppressMessage( + "Design", + "CA1000: Do not declare static members on generic types", + Justification = "This is required for handling value objects generically." +)] +public interface IParsableValueObject : IValueObject + where TSelf : notnull +{ + static abstract TSelf Parse(string s); + static abstract Result TryParse(string s); + static abstract bool TryParse(string s, out TSelf result); + static abstract bool TryParse(string s, IFormatProvider provider, out TSelf result); +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IValueObject.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IValueObject.cs new file mode 100644 index 0000000..a9591f5 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/IValueObject.cs @@ -0,0 +1,14 @@ +// Resharper disable CheckNamespace + +namespace GeneratedIdentifier.Common.ValueObjects; + +public interface IValueObject : IValueObject + where TSelf : notnull +{ + TValue Value { get; } +} + +public interface IValueObject : IValueObject, IEquatable, IComparable, IComparable + where TSelf : notnull; + +public interface IValueObject; diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ValueObjectValidationException.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ValueObjectValidationException.cs new file mode 100644 index 0000000..23fd614 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Interfaces/ValueObjectValidationException.cs @@ -0,0 +1,27 @@ +using IResult = LightResults.IResult; + +// Resharper disable CheckNamespace +namespace GeneratedIdentifier.Common.ValueObjects; + +public sealed class ValueObjectException : Exception +{ + public ValueObjectException() + { + } + + public ValueObjectException(string message) + : base(message) + { + } + + public ValueObjectException(string message, Exception innerException) + : base(message, innerException) + { + } + + public static void ThrowIfFailed(IResult result) + { + if (result.IsFailed(out var error)) + throw new ValueObjectException(error.Message); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/LightResults.Extensions.GeneratedIdentifier.Fixtures.csproj b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/LightResults.Extensions.GeneratedIdentifier.Fixtures.csproj new file mode 100644 index 0000000..931a9f0 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/LightResults.Extensions.GeneratedIdentifier.Fixtures.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + latest + LightResults.Extensions.GeneratedIdentifier.Fixtures + LightResults.Extensions.GeneratedIdentifier.Fixtures + latest-Default + false + false + + + + + + + + + + + + diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Program.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Program.cs new file mode 100644 index 0000000..5d25b47 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Program.cs @@ -0,0 +1,8 @@ +namespace LightResults.Extensions.GeneratedIdentifier.Fixtures; + +public static class Program +{ + public static void Main() + { + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Properties/launchSettings.json b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Properties/launchSettings.json new file mode 100644 index 0000000..b7cdb7f --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Fixtures/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Api.SourceGenerators.Fixtures": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:59000;http://localhost:59001" + } + } +} \ No newline at end of file diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateGuidIdentifier_WithNamespace.verified.txt b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateGuidIdentifier_WithNamespace.verified.txt new file mode 100644 index 0000000..0436efc --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateGuidIdentifier_WithNamespace.verified.txt @@ -0,0 +1,230 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by GeneratedIdentifierSourceGenerator which +// can be found in the LightResults.Extensions.GeneratedIdentifier namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using System.ComponentModel; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using LightResults; +using GeneratedIdentifier.Common.ValueObjects; + +namespace MyProject.Identifiers; + +[TypeConverter(typeof(TestGuidIdTypeConverter))] +[JsonConverter(typeof(TestGuidIdJsonConverter))] +readonly partial struct TestGuidId : + ICreatableValueObject, + IParsableValueObject, + IValueObject, + IComparable, + IComparable +{ + /// Gets whether this identifier is the default value. + public bool IsDefault => _value == default; + + Guid IValueObject.Value => _value; + + private readonly Guid _value; + + private TestGuidId(Guid value, bool skipValidation = false) + { + if (!skipValidation) + ValueObjectException.ThrowIfFailed(Validate(value)); + + _value = value; + } + + /// + public static TestGuidId Create(Guid value) + { + var result = TryCreate(value); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryCreate(Guid value) + { + var validation = Validate(value); + if (validation.IsFailed(out var error)) + return Result.Fail(error); + + return Result.Ok(new TestGuidId(value, true)); + } + + /// + public static TestGuidId Parse(string s) + { + var result = TryParse(s); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryParse(string s) + { + if (Guid.TryParse(s, out var value)) + return TryCreate(value); + + return Result.Fail("The string is not a valid identifier."); + } + + /// + public static bool TryParse(string s, out TestGuidId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public static bool TryParse(string s, IFormatProvider provider, out TestGuidId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public bool Equals(TestGuidId other) + { + return _value == other._value; + } + + /// + public override bool Equals(object? obj) + { + return obj is TestGuidId other && Equals(other); + } + + /// + public override int GetHashCode() + { + return _value.GetHashCode(); + } + + /// Determines whether two instances of are equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are equal; otherwise, false. + public static bool operator ==(TestGuidId left, TestGuidId right) + { + return left.Equals(right); + } + + /// Determines whether two instances of are not equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are not equal; otherwise, false. + public static bool operator !=(TestGuidId left, TestGuidId right) + { + return !left.Equals(right); + } + + /// + public int CompareTo(TestGuidId other) + { + return _value.CompareTo(other._value); + } + + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) return 1; + return obj is TestGuidId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(TestGuidId)}"); + } + + /// Determines whether the first instance of is less than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than the second instance; otherwise, false. + public static bool operator <(TestGuidId left, TestGuidId right) + { + return left.CompareTo(right) < 0; + } + + /// Determines whether the first instance of is greater than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than the second instance; otherwise, false. + public static bool operator >(TestGuidId left, TestGuidId right) + { + return left.CompareTo(right) > 0; + } + + /// Determines whether the first instance of is less than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than or equal to the second instance; otherwise, false. + public static bool operator <=(TestGuidId left, TestGuidId right) + { + return left.CompareTo(right) <= 0; + } + + /// Determines whether the first instance of is greater than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than or equal to the second instance; otherwise, false. + public static bool operator >=(TestGuidId left, TestGuidId right) + { + return left.CompareTo(right) >= 0; + } + + /// Gets the underlying value of the . + /// The underlying value of the . + public Guid ToGuid() + { + return _value; + } + + /// + public override string ToString() + { + return _value.ToString(); + } + + private static Result Validate(Guid value) + { + return Result.Ok(); + } +} + +public class TestGuidIdTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(Guid) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is Guid identifierValue) + return TestGuidId.Create(identifierValue); + + return base.ConvertFrom(context, culture, value); + } +} + +public class TestGuidIdJsonConverter : JsonConverter +{ + public override void Write(Utf8JsonWriter writer, TestGuidId identifier, JsonSerializerOptions options) + { + var value = ((IValueObject)identifier).Value; + writer.WriteStringValue(value.ToString()); + } + + public override TestGuidId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetGuid(); + return TestGuidId.Create(value); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateGuidIdentifier_WithoutNamespace.verified.txt b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateGuidIdentifier_WithoutNamespace.verified.txt new file mode 100644 index 0000000..98ca6ee --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateGuidIdentifier_WithoutNamespace.verified.txt @@ -0,0 +1,228 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by GeneratedIdentifierSourceGenerator which +// can be found in the LightResults.Extensions.GeneratedIdentifier namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using System.ComponentModel; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using LightResults; +using GeneratedIdentifier.Common.ValueObjects; + +[TypeConverter(typeof(TestGuidIdTypeConverter))] +[JsonConverter(typeof(TestGuidIdJsonConverter))] +readonly partial struct TestGuidId : + ICreatableValueObject, + IParsableValueObject, + IValueObject, + IComparable, + IComparable +{ + /// Gets whether this identifier is the default value. + public bool IsDefault => _value == default; + + Guid IValueObject.Value => _value; + + private readonly Guid _value; + + private TestGuidId(Guid value, bool skipValidation = false) + { + if (!skipValidation) + ValueObjectException.ThrowIfFailed(Validate(value)); + + _value = value; + } + + /// + public static TestGuidId Create(Guid value) + { + var result = TryCreate(value); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryCreate(Guid value) + { + var validation = Validate(value); + if (validation.IsFailed(out var error)) + return Result.Fail(error); + + return Result.Ok(new TestGuidId(value, true)); + } + + /// + public static TestGuidId Parse(string s) + { + var result = TryParse(s); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryParse(string s) + { + if (Guid.TryParse(s, out var value)) + return TryCreate(value); + + return Result.Fail("The string is not a valid identifier."); + } + + /// + public static bool TryParse(string s, out TestGuidId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public static bool TryParse(string s, IFormatProvider provider, out TestGuidId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public bool Equals(TestGuidId other) + { + return _value == other._value; + } + + /// + public override bool Equals(object? obj) + { + return obj is TestGuidId other && Equals(other); + } + + /// + public override int GetHashCode() + { + return _value.GetHashCode(); + } + + /// Determines whether two instances of are equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are equal; otherwise, false. + public static bool operator ==(TestGuidId left, TestGuidId right) + { + return left.Equals(right); + } + + /// Determines whether two instances of are not equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are not equal; otherwise, false. + public static bool operator !=(TestGuidId left, TestGuidId right) + { + return !left.Equals(right); + } + + /// + public int CompareTo(TestGuidId other) + { + return _value.CompareTo(other._value); + } + + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) return 1; + return obj is TestGuidId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(TestGuidId)}"); + } + + /// Determines whether the first instance of is less than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than the second instance; otherwise, false. + public static bool operator <(TestGuidId left, TestGuidId right) + { + return left.CompareTo(right) < 0; + } + + /// Determines whether the first instance of is greater than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than the second instance; otherwise, false. + public static bool operator >(TestGuidId left, TestGuidId right) + { + return left.CompareTo(right) > 0; + } + + /// Determines whether the first instance of is less than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than or equal to the second instance; otherwise, false. + public static bool operator <=(TestGuidId left, TestGuidId right) + { + return left.CompareTo(right) <= 0; + } + + /// Determines whether the first instance of is greater than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than or equal to the second instance; otherwise, false. + public static bool operator >=(TestGuidId left, TestGuidId right) + { + return left.CompareTo(right) >= 0; + } + + /// Gets the underlying value of the . + /// The underlying value of the . + public Guid ToGuid() + { + return _value; + } + + /// + public override string ToString() + { + return _value.ToString(); + } + + private static Result Validate(Guid value) + { + return Result.Ok(); + } +} + +public class TestGuidIdTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(Guid) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is Guid identifierValue) + return TestGuidId.Create(identifierValue); + + return base.ConvertFrom(context, culture, value); + } +} + +public class TestGuidIdJsonConverter : JsonConverter +{ + public override void Write(Utf8JsonWriter writer, TestGuidId identifier, JsonSerializerOptions options) + { + var value = ((IValueObject)identifier).Value; + writer.WriteStringValue(value.ToString()); + } + + public override TestGuidId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetGuid(); + return TestGuidId.Create(value); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateIntIdentifier_WithNamespace.verified.txt b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateIntIdentifier_WithNamespace.verified.txt new file mode 100644 index 0000000..8d23b64 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateIntIdentifier_WithNamespace.verified.txt @@ -0,0 +1,233 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by GeneratedIdentifierSourceGenerator which +// can be found in the LightResults.Extensions.GeneratedIdentifier namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using System.ComponentModel; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using LightResults; +using GeneratedIdentifier.Common.ValueObjects; + +namespace MyProject.Identifiers; + +[TypeConverter(typeof(TestIntIdTypeConverter))] +[JsonConverter(typeof(TestIntIdJsonConverter))] +readonly partial struct TestIntId : + ICreatableValueObject, + IParsableValueObject, + IValueObject, + IComparable, + IComparable +{ + /// Gets whether this identifier is the default value. + public bool IsDefault => _value == default; + + int IValueObject.Value => _value; + + private readonly int _value; + + private TestIntId(int value, bool skipValidation = false) + { + if (!skipValidation) + ValueObjectException.ThrowIfFailed(Validate(value)); + + _value = value; + } + + /// + public static TestIntId Create(int value) + { + var result = TryCreate(value); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryCreate(int value) + { + var validation = Validate(value); + if (validation.IsFailed(out var error)) + return Result.Fail(error); + + return Result.Ok(new TestIntId(value, true)); + } + + /// + public static TestIntId Parse(string s) + { + var result = TryParse(s); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryParse(string s) + { + if (int.TryParse(s, out var value)) + return TryCreate(value); + + return Result.Fail("The string is not a valid identifier."); + } + + /// + public static bool TryParse(string s, out TestIntId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public static bool TryParse(string s, IFormatProvider provider, out TestIntId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public bool Equals(TestIntId other) + { + return _value == other._value; + } + + /// + public override bool Equals(object? obj) + { + return obj is TestIntId other && Equals(other); + } + + /// + public override int GetHashCode() + { + return _value; + } + + /// Determines whether two instances of are equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are equal; otherwise, false. + public static bool operator ==(TestIntId left, TestIntId right) + { + return left.Equals(right); + } + + /// Determines whether two instances of are not equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are not equal; otherwise, false. + public static bool operator !=(TestIntId left, TestIntId right) + { + return !left.Equals(right); + } + + /// + public int CompareTo(TestIntId other) + { + return _value.CompareTo(other._value); + } + + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) return 1; + return obj is TestIntId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(TestIntId)}"); + } + + /// Determines whether the first instance of is less than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than the second instance; otherwise, false. + public static bool operator <(TestIntId left, TestIntId right) + { + return left.CompareTo(right) < 0; + } + + /// Determines whether the first instance of is greater than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than the second instance; otherwise, false. + public static bool operator >(TestIntId left, TestIntId right) + { + return left.CompareTo(right) > 0; + } + + /// Determines whether the first instance of is less than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than or equal to the second instance; otherwise, false. + public static bool operator <=(TestIntId left, TestIntId right) + { + return left.CompareTo(right) <= 0; + } + + /// Determines whether the first instance of is greater than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than or equal to the second instance; otherwise, false. + public static bool operator >=(TestIntId left, TestIntId right) + { + return left.CompareTo(right) >= 0; + } + + /// Gets the underlying value of the . + /// The underlying value of the . + public int ToInt32() + { + return _value; + } + + /// + public override string ToString() + { + return _value.ToString(CultureInfo.InvariantCulture); + } + + private static Result Validate(int value) + { + if (value < 0) + return Result.Fail("The value must be equal to or greater than zero."); + + return Result.Ok(); + } +} + +public class TestIntIdTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(int) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is int identifierValue) + return TestIntId.Create(identifierValue); + + return base.ConvertFrom(context, culture, value); + } +} + +public class TestIntIdJsonConverter : JsonConverter +{ + public override void Write(Utf8JsonWriter writer, TestIntId identifier, JsonSerializerOptions options) + { + var value = ((IValueObject)identifier).Value; + writer.WriteNumberValue(value); + } + + public override TestIntId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetInt32(); + return TestIntId.Create(value); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateIntIdentifier_WithoutNamespace.verified.txt b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateIntIdentifier_WithoutNamespace.verified.txt new file mode 100644 index 0000000..a90adba --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateIntIdentifier_WithoutNamespace.verified.txt @@ -0,0 +1,231 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by GeneratedIdentifierSourceGenerator which +// can be found in the LightResults.Extensions.GeneratedIdentifier namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using System.ComponentModel; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using LightResults; +using GeneratedIdentifier.Common.ValueObjects; + +[TypeConverter(typeof(TestIntIdTypeConverter))] +[JsonConverter(typeof(TestIntIdJsonConverter))] +readonly partial struct TestIntId : + ICreatableValueObject, + IParsableValueObject, + IValueObject, + IComparable, + IComparable +{ + /// Gets whether this identifier is the default value. + public bool IsDefault => _value == default; + + int IValueObject.Value => _value; + + private readonly int _value; + + private TestIntId(int value, bool skipValidation = false) + { + if (!skipValidation) + ValueObjectException.ThrowIfFailed(Validate(value)); + + _value = value; + } + + /// + public static TestIntId Create(int value) + { + var result = TryCreate(value); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryCreate(int value) + { + var validation = Validate(value); + if (validation.IsFailed(out var error)) + return Result.Fail(error); + + return Result.Ok(new TestIntId(value, true)); + } + + /// + public static TestIntId Parse(string s) + { + var result = TryParse(s); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryParse(string s) + { + if (int.TryParse(s, out var value)) + return TryCreate(value); + + return Result.Fail("The string is not a valid identifier."); + } + + /// + public static bool TryParse(string s, out TestIntId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public static bool TryParse(string s, IFormatProvider provider, out TestIntId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public bool Equals(TestIntId other) + { + return _value == other._value; + } + + /// + public override bool Equals(object? obj) + { + return obj is TestIntId other && Equals(other); + } + + /// + public override int GetHashCode() + { + return _value; + } + + /// Determines whether two instances of are equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are equal; otherwise, false. + public static bool operator ==(TestIntId left, TestIntId right) + { + return left.Equals(right); + } + + /// Determines whether two instances of are not equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are not equal; otherwise, false. + public static bool operator !=(TestIntId left, TestIntId right) + { + return !left.Equals(right); + } + + /// + public int CompareTo(TestIntId other) + { + return _value.CompareTo(other._value); + } + + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) return 1; + return obj is TestIntId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(TestIntId)}"); + } + + /// Determines whether the first instance of is less than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than the second instance; otherwise, false. + public static bool operator <(TestIntId left, TestIntId right) + { + return left.CompareTo(right) < 0; + } + + /// Determines whether the first instance of is greater than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than the second instance; otherwise, false. + public static bool operator >(TestIntId left, TestIntId right) + { + return left.CompareTo(right) > 0; + } + + /// Determines whether the first instance of is less than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than or equal to the second instance; otherwise, false. + public static bool operator <=(TestIntId left, TestIntId right) + { + return left.CompareTo(right) <= 0; + } + + /// Determines whether the first instance of is greater than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than or equal to the second instance; otherwise, false. + public static bool operator >=(TestIntId left, TestIntId right) + { + return left.CompareTo(right) >= 0; + } + + /// Gets the underlying value of the . + /// The underlying value of the . + public int ToInt32() + { + return _value; + } + + /// + public override string ToString() + { + return _value.ToString(CultureInfo.InvariantCulture); + } + + private static Result Validate(int value) + { + if (value < 0) + return Result.Fail("The value must be equal to or greater than zero."); + + return Result.Ok(); + } +} + +public class TestIntIdTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(int) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is int identifierValue) + return TestIntId.Create(identifierValue); + + return base.ConvertFrom(context, culture, value); + } +} + +public class TestIntIdJsonConverter : JsonConverter +{ + public override void Write(Utf8JsonWriter writer, TestIntId identifier, JsonSerializerOptions options) + { + var value = ((IValueObject)identifier).Value; + writer.WriteNumberValue(value); + } + + public override TestIntId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetInt32(); + return TestIntId.Create(value); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateShortIdentifier_WithNamespace.verified.txt b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateShortIdentifier_WithNamespace.verified.txt new file mode 100644 index 0000000..c676b2c --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateShortIdentifier_WithNamespace.verified.txt @@ -0,0 +1,233 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by GeneratedIdentifierSourceGenerator which +// can be found in the LightResults.Extensions.GeneratedIdentifier namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using System.ComponentModel; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using LightResults; +using GeneratedIdentifier.Common.ValueObjects; + +namespace MyProject.Identifiers; + +[TypeConverter(typeof(TestShortIdTypeConverter))] +[JsonConverter(typeof(TestShortIdJsonConverter))] +readonly partial struct TestShortId : + ICreatableValueObject, + IParsableValueObject, + IValueObject, + IComparable, + IComparable +{ + /// Gets whether this identifier is the default value. + public bool IsDefault => _value == default; + + short IValueObject.Value => _value; + + private readonly short _value; + + private TestShortId(short value, bool skipValidation = false) + { + if (!skipValidation) + ValueObjectException.ThrowIfFailed(Validate(value)); + + _value = value; + } + + /// + public static TestShortId Create(short value) + { + var result = TryCreate(value); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryCreate(short value) + { + var validation = Validate(value); + if (validation.IsFailed(out var error)) + return Result.Fail(error); + + return Result.Ok(new TestShortId(value, true)); + } + + /// + public static TestShortId Parse(string s) + { + var result = TryParse(s); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryParse(string s) + { + if (short.TryParse(s, out var value)) + return TryCreate(value); + + return Result.Fail("The string is not a valid identifier."); + } + + /// + public static bool TryParse(string s, out TestShortId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public static bool TryParse(string s, IFormatProvider provider, out TestShortId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public bool Equals(TestShortId other) + { + return _value == other._value; + } + + /// + public override bool Equals(object? obj) + { + return obj is TestShortId other && Equals(other); + } + + /// + public override int GetHashCode() + { + return _value; + } + + /// Determines whether two instances of are equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are equal; otherwise, false. + public static bool operator ==(TestShortId left, TestShortId right) + { + return left.Equals(right); + } + + /// Determines whether two instances of are not equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are not equal; otherwise, false. + public static bool operator !=(TestShortId left, TestShortId right) + { + return !left.Equals(right); + } + + /// + public int CompareTo(TestShortId other) + { + return _value.CompareTo(other._value); + } + + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) return 1; + return obj is TestShortId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(TestShortId)}"); + } + + /// Determines whether the first instance of is less than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than the second instance; otherwise, false. + public static bool operator <(TestShortId left, TestShortId right) + { + return left.CompareTo(right) < 0; + } + + /// Determines whether the first instance of is greater than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than the second instance; otherwise, false. + public static bool operator >(TestShortId left, TestShortId right) + { + return left.CompareTo(right) > 0; + } + + /// Determines whether the first instance of is less than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than or equal to the second instance; otherwise, false. + public static bool operator <=(TestShortId left, TestShortId right) + { + return left.CompareTo(right) <= 0; + } + + /// Determines whether the first instance of is greater than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than or equal to the second instance; otherwise, false. + public static bool operator >=(TestShortId left, TestShortId right) + { + return left.CompareTo(right) >= 0; + } + + /// Gets the underlying value of the . + /// The underlying value of the . + public short ToInt16() + { + return _value; + } + + /// + public override string ToString() + { + return _value.ToString(CultureInfo.InvariantCulture); + } + + private static Result Validate(short value) + { + if (value < 0) + return Result.Fail("The value must be equal to or greater than zero."); + + return Result.Ok(); + } +} + +public class TestShortIdTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(short) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is short identifierValue) + return TestShortId.Create(identifierValue); + + return base.ConvertFrom(context, culture, value); + } +} + +public class TestShortIdJsonConverter : JsonConverter +{ + public override void Write(Utf8JsonWriter writer, TestShortId identifier, JsonSerializerOptions options) + { + var value = ((IValueObject)identifier).Value; + writer.WriteNumberValue(value); + } + + public override TestShortId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetInt16(); + return TestShortId.Create(value); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateShortIdentifier_WithoutNamespace.verified.txt b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateShortIdentifier_WithoutNamespace.verified.txt new file mode 100644 index 0000000..21dacba --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.GenerateShortIdentifier_WithoutNamespace.verified.txt @@ -0,0 +1,231 @@ +//----------------------------------------------------------------------------- +// +// This code was generated by GeneratedIdentifierSourceGenerator which +// can be found in the LightResults.Extensions.GeneratedIdentifier namespace. +// +// Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated. +// +//----------------------------------------------------------------------------- + +#nullable enable + +using System.ComponentModel; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; +using LightResults; +using GeneratedIdentifier.Common.ValueObjects; + +[TypeConverter(typeof(TestShortIdTypeConverter))] +[JsonConverter(typeof(TestShortIdJsonConverter))] +readonly partial struct TestShortId : + ICreatableValueObject, + IParsableValueObject, + IValueObject, + IComparable, + IComparable +{ + /// Gets whether this identifier is the default value. + public bool IsDefault => _value == default; + + short IValueObject.Value => _value; + + private readonly short _value; + + private TestShortId(short value, bool skipValidation = false) + { + if (!skipValidation) + ValueObjectException.ThrowIfFailed(Validate(value)); + + _value = value; + } + + /// + public static TestShortId Create(short value) + { + var result = TryCreate(value); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryCreate(short value) + { + var validation = Validate(value); + if (validation.IsFailed(out var error)) + return Result.Fail(error); + + return Result.Ok(new TestShortId(value, true)); + } + + /// + public static TestShortId Parse(string s) + { + var result = TryParse(s); + if (result.IsSuccess(out var identifier, out var error)) + return identifier; + + throw new ValueObjectException(error.Message); + } + + /// + public static Result TryParse(string s) + { + if (short.TryParse(s, out var value)) + return TryCreate(value); + + return Result.Fail("The string is not a valid identifier."); + } + + /// + public static bool TryParse(string s, out TestShortId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public static bool TryParse(string s, IFormatProvider provider, out TestShortId identifier) + { + return TryParse(s).IsSuccess(out identifier); + } + + /// + public bool Equals(TestShortId other) + { + return _value == other._value; + } + + /// + public override bool Equals(object? obj) + { + return obj is TestShortId other && Equals(other); + } + + /// + public override int GetHashCode() + { + return _value; + } + + /// Determines whether two instances of are equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are equal; otherwise, false. + public static bool operator ==(TestShortId left, TestShortId right) + { + return left.Equals(right); + } + + /// Determines whether two instances of are not equal. + /// The first instance to compare. + /// The second instance to compare. + /// true if the instances are not equal; otherwise, false. + public static bool operator !=(TestShortId left, TestShortId right) + { + return !left.Equals(right); + } + + /// + public int CompareTo(TestShortId other) + { + return _value.CompareTo(other._value); + } + + /// + public int CompareTo(object? obj) + { + if (ReferenceEquals(null, obj)) return 1; + return obj is TestShortId other ? CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(TestShortId)}"); + } + + /// Determines whether the first instance of is less than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than the second instance; otherwise, false. + public static bool operator <(TestShortId left, TestShortId right) + { + return left.CompareTo(right) < 0; + } + + /// Determines whether the first instance of is greater than the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than the second instance; otherwise, false. + public static bool operator >(TestShortId left, TestShortId right) + { + return left.CompareTo(right) > 0; + } + + /// Determines whether the first instance of is less than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is less than or equal to the second instance; otherwise, false. + public static bool operator <=(TestShortId left, TestShortId right) + { + return left.CompareTo(right) <= 0; + } + + /// Determines whether the first instance of is greater than or equal to the second instance. + /// The first instance to compare. + /// The second instance to compare. + /// true if the first instance is greater than or equal to the second instance; otherwise, false. + public static bool operator >=(TestShortId left, TestShortId right) + { + return left.CompareTo(right) >= 0; + } + + /// Gets the underlying value of the . + /// The underlying value of the . + public short ToInt16() + { + return _value; + } + + /// + public override string ToString() + { + return _value.ToString(CultureInfo.InvariantCulture); + } + + private static Result Validate(short value) + { + if (value < 0) + return Result.Fail("The value must be equal to or greater than zero."); + + return Result.Ok(); + } +} + +public class TestShortIdTypeConverter : TypeConverter +{ + public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) + { + return sourceType == typeof(short) || base.CanConvertFrom(context, sourceType); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + if (value is short identifierValue) + return TestShortId.Create(identifierValue); + + return base.ConvertFrom(context, culture, value); + } +} + +public class TestShortIdJsonConverter : JsonConverter +{ + public override void Write(Utf8JsonWriter writer, TestShortId identifier, JsonSerializerOptions options) + { + var value = ((IValueObject)identifier).Value; + writer.WriteNumberValue(value); + } + + public override TestShortId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var value = reader.GetInt16(); + return TestShortId.Create(value); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.cs new file mode 100644 index 0000000..ba8db70 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/GeneratedIdentifierSourceGeneratorTests.cs @@ -0,0 +1,104 @@ +using System.Collections.Immutable; +using Basic.Reference.Assemblies; +using LightResults.Extensions.GeneratedIdentifier; +using LightResults.Extensions.GeneratedIdentifier.Tests; +using Microsoft.CodeAnalysis; +using SourceGeneratorTestHelpers; +using SourceGeneratorTestHelpers.XUnit; + +namespace GeneratedIdentifier.Tests; + +public sealed class GeneratedIdentifierSourceGeneratorTests +{ + static GeneratedIdentifierSourceGeneratorTests() + { + ModuleInitializer.Initialize(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GenerateGuidIdentifier(bool withNamespace) + { + var sources = GetSources(""" + /// Represents an identifier. + [GeneratedIdentifier] + public partial struct TestGuidId; + """, withNamespace + ); + + var result = RunGenerator(sources); + await result.VerifyAsync("TestGuidId.g.cs") + .UseMethodName($"{nameof(GenerateGuidIdentifier)}_With{(withNamespace ? "" : "out")}Namespace"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GenerateIntIdentifier(bool withNamespace) + { + var sources = GetSources(""" + /// Represents an identifier. + [GeneratedIdentifier] + public partial struct TestIntId; + """, withNamespace + ); + + var result = RunGenerator(sources); + await result.VerifyAsync("TestIntId.g.cs") + .UseMethodName($"{nameof(GenerateIntIdentifier)}_With{(withNamespace ? "" : "out")}Namespace"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GenerateShortIdentifier(bool withNamespace) + { + var sources = GetSources(""" + /// Represents an identifier. + [GeneratedIdentifier] + public partial struct TestShortId; + """, withNamespace + ); + + var result = RunGenerator(sources); + await result.VerifyAsync("TestShortId.g.cs") + .UseMethodName($"{nameof(GenerateShortIdentifier)}_With{(withNamespace ? "" : "out")}Namespace"); + } + + private static IEnumerable GetSources(string source, bool withNamespace = true) + { + const string usingStatements = """ + using System; + using LightResults.Extensions.GeneratedIdentifier; + """; + + if (withNamespace) + yield return $""" + {usingStatements} + + namespace MyProject.Identifiers; + + {source} + """; + else + yield return $""" + {usingStatements} + + {source} + """; + yield return """ + using System; + + namespace LightResults.Extensions.GeneratedIdentifier; + + [AttributeUsage(AttributeTargets.Struct)] + public sealed class GeneratedIdentifierAttribute : Attribute; + """; + } + + private static (ImmutableArray Diagnostics, GeneratorDriverRunResult Result) RunGenerator(IEnumerable sources) + { + return IncrementalGenerator.RunWithDiagnostics(sources, metadataReferences: ReferenceAssemblies.Net80); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/LightResults.Extensions.GeneratedIdentifier.Tests.csproj b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/LightResults.Extensions.GeneratedIdentifier.Tests.csproj new file mode 100644 index 0000000..402a32f --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/LightResults.Extensions.GeneratedIdentifier.Tests.csproj @@ -0,0 +1,66 @@ + + + + net8.0 + enable + enable + latest + LightResults.Extensions.GeneratedIdentifier.Tests + LightResults.Extensions.GeneratedIdentifier.Tests + latest-Default + false + true + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + IdentifierSourceGeneratorTests.cs + + + IdentifierSourceGeneratorTests.cs + + + IdentifierSourceGeneratorTests.cs + + + IdentifierSourceGeneratorTests.cs + + + IdentifierSourceGeneratorTests.cs + + + IdentifierSourceGeneratorTests.cs + + + GeneratedIdentifierSourceGeneratorTests.cs + + + GeneratedIdentifierSourceGeneratorTests.cs + + + + diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/ModuleInitializer.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/ModuleInitializer.cs new file mode 100644 index 0000000..6addfa9 --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/ModuleInitializer.cs @@ -0,0 +1,16 @@ +using DiffEngine; +using VerifyTests.DiffPlex; + +namespace LightResults.Extensions.GeneratedIdentifier.Tests; + +public static class ModuleInitializer +{ + public static void Initialize() + { + DiffRunner.Disabled = true; + VerifyDiffPlex.Initialize(OutputType.Compact); + VerifierSettings.InitializePlugins(); + VerifierSettings.ScrubLinesContaining("DiffEngineTray"); + VerifierSettings.IgnoreStackTrace(); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestGuidIdTest.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestGuidIdTest.cs new file mode 100644 index 0000000..a4a1c0b --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestGuidIdTest.cs @@ -0,0 +1,329 @@ +using FluentAssertions; +using GeneratedIdentifier.Common.ValueObjects; +using LightResults.Extensions.GeneratedIdentifier.Fixtures.Identifiers; + +// ReSharper disable SuspiciousTypeConversion.Global +// ReSharper disable EqualExpressionComparison +#pragma warning disable CS1718 // Comparison made to same variable + +namespace LightResults.Extensions.GeneratedIdentifier.Tests; + +public sealed class TestGuidIdTest +{ + private static readonly Guid Guid1 = Guid.Parse("bcbac1e2-de00-47e4-9891-58774a68668f"); + private static readonly Guid Guid2 = Guid.Parse("e8d81b8f-127f-4e7d-b7fb-c9ab6f34be72"); + + [Fact] + public void Create_ValidValue_ShouldSucceed() + { + // Arrange + var validValue = Guid1; + + // Act + var id = TestGuidId.Create(validValue); + + // Assert + id.Should().NotBeNull(); + id.ToGuid().Should().Be(validValue); + } + + [Fact] + public void TryCreate_ValidValue_ShouldSucceed() + { + // Arrange + var validValue = Guid1; + + // Act + var result = TestGuidId.TryCreate(validValue); + + // Assert + result.IsSuccess(out var id).Should().BeTrue(); + id.Should().NotBeNull(); + id.ToGuid().Should().Be(validValue); + } + + [Fact] + public void Parse_ValidString_ShouldSucceed() + { + // Arrange + const string validString = "5528cc73-cba9-4960-85c1-ed96dc4c7f95"; + + // Act + var result = TestGuidId.Parse(validString); + + // Assert + + result.ToGuid().Should().Be(Guid.Parse(validString)); + } + + [Theory] + [InlineData("invalid")] + [InlineData("g528cc73-cba9-4960-85c1-ed96dc4c7f95")] + public void Parse_InvalidString_ShouldThrowException(string invalidString) + { + // Act + var parse = () => TestGuidId.Parse(invalidString); + + // Assert + parse.Should().Throw(); + } + + [Fact] + public void TryParse_ValidString_ShouldSucceed() + { + // Arrange + const string validString = "5528cc73-cba9-4960-85c1-ed96dc4c7f95"; + + // Act + var result = TestGuidId.TryParse(validString); + + // Assert + result.IsSuccess(out var id).Should().BeTrue(); + id.Should().NotBeNull(); + id.ToGuid().Should().Be(Guid.Parse(validString)); + } + + [Theory] + [InlineData("invalid")] + [InlineData("-1")] + public void TryParse_InvalidString_ShouldFail(string invalidString) + { + // Act + var result = TestGuidId.TryParse(invalidString); + + // Assert + result.IsFailed().Should().BeTrue(); + result.Errors.Should().ContainSingle(); + } + + [Fact] + public void Equals_SameValues_ShouldBeEqual() + { + // Arrange + var id1 = TestGuidId.Create(Guid1); + var id2 = TestGuidId.Create(Guid1); + + // Assert + id1.Should().Be(id2); + (id1 == id2).Should().BeTrue(); + (id1 != id2).Should().BeFalse(); + } + + [Fact] + public void Equals_DifferentValues_ShouldNotBeEqual() + { + // Arrange + var id1 = TestGuidId.Create(Guid1); + var id2 = TestGuidId.Create(Guid2); + + // Assert + id1.Should().NotBe(id2); + (id1 == id2).Should().BeFalse(); + (id1 != id2).Should().BeTrue(); + } + + [Fact] + public void Equals_ObjectIsNull_ShouldReturnFalse() + { + // Arrange + var id = TestGuidId.Create(Guid1); + + // Act + var result = id.Equals(null); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void Equals_ObjectIsNotTestGuidId_ShouldReturnFalse() + { + // Arrange + var id = TestGuidId.Create(Guid1); + + // Act + var result = id.Equals("not an TestGuidId"); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void GetHashCode_ShouldReturnCorrectValue() + { + // Arrange + var underlyingValue = Guid1; + var id = TestGuidId.Create(underlyingValue); + + // Act + var hashCode1 = id.GetHashCode(); + var hashCode2 = underlyingValue.GetHashCode(); + + // Assert + hashCode1.Should().Be(hashCode2); + } + + [Fact] + public void Operators_Equality_ShouldReturnTrue() + { + // Arrange + var id1 = TestGuidId.Create(Guid1); + var id2 = TestGuidId.Create(Guid1); + + // Assert + (id1 == id2).Should().BeTrue(); + (id1 != id2).Should().BeFalse(); + } + + [Fact] + public void Operators_Inequality_ShouldReturnTrue() + { + // Arrange + var id1 = TestGuidId.Create(Guid1); + var id2 = TestGuidId.Create(Guid2); + + // Assert + (id1 != id2).Should().BeTrue(); + (id1 == id2).Should().BeFalse(); + } + + [Fact] + public void CompareTo_SameValue_ShouldReturnZero() + { + // Arrange + var id1 = TestGuidId.Create(Guid1); + var id2 = TestGuidId.Create(Guid1); + + // Act + var result = id1.CompareTo(id2); + + // Assert + result.Should().Be(0); + } + + [Fact] + public void CompareTo_LesserValue_ShouldReturnNegative() + { + // Arrange + var id1 = TestGuidId.Create(Guid1); + var id2 = TestGuidId.Create(Guid2); + + // Act + var result = id1.CompareTo(id2); + + // Assert + result.Should().BeNegative(); + } + + [Fact] + public void CompareTo_GreaterValue_ShouldReturnPositive() + { + // Arrange + var id1 = TestGuidId.Create(Guid2); + var id2 = TestGuidId.Create(Guid1); + + // Act + var result = id1.CompareTo(id2); + + // Assert + result.Should().BePositive(); + } + + [Fact] + public void Operators_LessThan_ShouldReturnTrue() + { + // Arrange + var id1 = TestGuidId.Create(Guid1); + var id2 = TestGuidId.Create(Guid2); + + // Assert + (id1 < id2).Should().BeTrue(); + } + + [Fact] + public void Operators_GreaterThan_ShouldReturnTrue() + { + // Arrange + var id1 = TestGuidId.Create(Guid2); + var id2 = TestGuidId.Create(Guid1); + + // Assert + (id1 > id2).Should().BeTrue(); + } + + [Fact] + public void Operators_LessThanOrEqual_ShouldReturnTrue() + { + // Arrange + var id1 = TestGuidId.Create(Guid1); + var id2 = TestGuidId.Create(Guid2); + + // Assert + (id1 <= id2).Should().BeTrue(); + (id1 <= id1).Should().BeTrue(); + } + + [Fact] + public void Operators_GreaterThanOrEqual_ShouldReturnTrue() + { + // Arrange + var id1 = TestGuidId.Create(Guid2); + var id2 = TestGuidId.Create(Guid1); + + // Assert + (id1 >= id2).Should().BeTrue(); + (id1 >= id1).Should().BeTrue(); + } + + [Fact] + public void CompareTo_ObjectIsNull_ShouldReturnPositive() + { + // Arrange + var id = TestGuidId.Create(Guid1); + + // Act + var result = id.CompareTo(null); + + // Assert + result.Should().BePositive(); + } + + [Fact] + public void CompareTo_ObjectIsNotTestGuidId_ShouldThrowException() + { + // Arrange + var id = TestGuidId.Create(Guid1); + + // Act + var compareTo = () => id.CompareTo("not an TestGuidId"); + + // Assert + compareTo.Should().Throw(); + } + + [Fact] + public void ToInt_ShouldReturnCorrectValue() + { + // Arrange + var id = TestGuidId.Create(Guid1); + + // Act + var integerValue = id.ToGuid(); + + // Assert + integerValue.Should().Be(Guid1); + } + + [Fact] + public void ToString_ShouldReturnStringValue() + { + // Arrange + var id = TestGuidId.Create(Guid1); + + // Act + var stringValue = id.ToString(); + + // Assert + stringValue.Should().Be(Guid1.ToString()); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestIntIdTest.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestIntIdTest.cs new file mode 100644 index 0000000..3f0a47e --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestIntIdTest.cs @@ -0,0 +1,353 @@ +using FluentAssertions; +using GeneratedIdentifier.Common.ValueObjects; +using LightResults.Extensions.GeneratedIdentifier.Fixtures.Identifiers; + +// ReSharper disable SuspiciousTypeConversion.Global +// ReSharper disable EqualExpressionComparison +#pragma warning disable CS1718 // Comparison made to same variable + +namespace LightResults.Extensions.GeneratedIdentifier.Tests; + +public sealed class TestIntIdTest +{ + [Fact] + public void Create_ValidValue_ShouldSucceed() + { + // Arrange + const int validValue = 42; + + // Act + var id = TestIntId.Create(validValue); + + // Assert + id.Should().NotBeNull(); + id.ToInt32().Should().Be(validValue); + } + + [Fact] + public void Create_InvalidValue_ShouldThrowException() + { + // Arrange + const int invalidValue = -1; + + // Act + var create = () => TestIntId.Create(invalidValue); + + // Assert + create.Should().Throw(); + } + + [Fact] + public void TryCreate_ValidValue_ShouldSucceed() + { + // Arrange + const int validValue = 42; + + // Act + var result = TestIntId.TryCreate(validValue); + + // Assert + result.IsSuccess(out var id).Should().BeTrue(); + id.Should().NotBeNull(); + id.ToInt32().Should().Be(validValue); + } + + [Fact] + public void TryCreate_InvalidValue_ShouldFail() + { + // Arrange + const int invalidValue = -1; + + // Act + var result = TestIntId.TryCreate(invalidValue); + + // Assert + result.IsFailed().Should().BeTrue(); + result.Errors.Should().ContainSingle(); + } + + [Fact] + public void Parse_ValidString_ShouldSucceed() + { + // Arrange + const string validString = "42"; + + // Act + var result = TestIntId.Parse(validString); + + // Assert + + result.ToInt32().Should().Be(int.Parse(validString)); + } + + [Theory] + [InlineData("invalid")] + [InlineData("-1")] + public void Parse_InvalidString_ShouldThrowException(string invalidString) + { + // Act + var parse = () => TestIntId.Parse(invalidString); + + // Assert + parse.Should().Throw(); + } + + [Fact] + public void TryParse_ValidString_ShouldSucceed() + { + // Arrange + const string validString = "42"; + + // Act + var result = TestIntId.TryParse(validString); + + // Assert + result.IsSuccess(out var id).Should().BeTrue(); + id.Should().NotBeNull(); + id.ToInt32().Should().Be(int.Parse(validString)); + } + + [Theory] + [InlineData("invalid")] + [InlineData("-1")] + public void TryParse_InvalidString_ShouldFail(string invalidString) + { + // Act + var result = TestIntId.TryParse(invalidString); + + // Assert + result.IsFailed().Should().BeTrue(); + result.Errors.Should().ContainSingle(); + } + + [Fact] + public void Equals_SameValues_ShouldBeEqual() + { + // Arrange + var id1 = TestIntId.Create(42); + var id2 = TestIntId.Create(42); + + // Assert + id1.Should().Be(id2); + (id1 == id2).Should().BeTrue(); + (id1 != id2).Should().BeFalse(); + } + + [Fact] + public void Equals_DifferentValues_ShouldNotBeEqual() + { + // Arrange + var id1 = TestIntId.Create(42); + var id2 = TestIntId.Create(99); + + // Assert + id1.Should().NotBe(id2); + (id1 == id2).Should().BeFalse(); + (id1 != id2).Should().BeTrue(); + } + + [Fact] + public void Equals_ObjectIsNull_ShouldReturnFalse() + { + // Arrange + var id = TestIntId.Create(42); + + // Act + var result = id.Equals(null); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void Equals_ObjectIsNotTestIntId_ShouldReturnFalse() + { + // Arrange + var id = TestIntId.Create(42); + + // Act + var result = id.Equals("not an TestIntId"); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void GetHashCode_ShouldReturnCorrectValue() + { + // Arrange + const int underlyingValue = 42; + var id = TestIntId.Create(underlyingValue); + + // Act + var hashCode1 = id.GetHashCode(); + var hashCode2 = underlyingValue.GetHashCode(); + + // Assert + hashCode1.Should().Be(hashCode2); + } + + [Fact] + public void Operators_Equality_ShouldReturnTrue() + { + // Arrange + var id1 = TestIntId.Create(42); + var id2 = TestIntId.Create(42); + + // Assert + (id1 == id2).Should().BeTrue(); + (id1 != id2).Should().BeFalse(); + } + + [Fact] + public void Operators_Inequality_ShouldReturnTrue() + { + // Arrange + var id1 = TestIntId.Create(42); + var id2 = TestIntId.Create(99); + + // Assert + (id1 != id2).Should().BeTrue(); + (id1 == id2).Should().BeFalse(); + } + + [Fact] + public void CompareTo_SameValue_ShouldReturnZero() + { + // Arrange + var id1 = TestIntId.Create(42); + var id2 = TestIntId.Create(42); + + // Act + var result = id1.CompareTo(id2); + + // Assert + result.Should().Be(0); + } + + [Fact] + public void CompareTo_LesserValue_ShouldReturnNegative() + { + // Arrange + var id1 = TestIntId.Create(42); + var id2 = TestIntId.Create(99); + + // Act + var result = id1.CompareTo(id2); + + // Assert + result.Should().BeNegative(); + } + + [Fact] + public void CompareTo_GreaterValue_ShouldReturnPositive() + { + // Arrange + var id1 = TestIntId.Create(99); + var id2 = TestIntId.Create(42); + + // Act + var result = id1.CompareTo(id2); + + // Assert + result.Should().BePositive(); + } + + [Fact] + public void Operators_LessThan_ShouldReturnTrue() + { + // Arrange + var id1 = TestIntId.Create(42); + var id2 = TestIntId.Create(99); + + // Assert + (id1 < id2).Should().BeTrue(); + } + + [Fact] + public void Operators_GreaterThan_ShouldReturnTrue() + { + // Arrange + var id1 = TestIntId.Create(99); + var id2 = TestIntId.Create(42); + + // Assert + (id1 > id2).Should().BeTrue(); + } + + [Fact] + public void Operators_LessThanOrEqual_ShouldReturnTrue() + { + // Arrange + var id1 = TestIntId.Create(42); + var id2 = TestIntId.Create(99); + + // Assert + (id1 <= id2).Should().BeTrue(); + (id1 <= id1).Should().BeTrue(); + } + + [Fact] + public void Operators_GreaterThanOrEqual_ShouldReturnTrue() + { + // Arrange + var id1 = TestIntId.Create(99); + var id2 = TestIntId.Create(42); + + // Assert + (id1 >= id2).Should().BeTrue(); + (id1 >= id1).Should().BeTrue(); + } + + [Fact] + public void CompareTo_ObjectIsNull_ShouldReturnPositive() + { + // Arrange + var id = TestIntId.Create(42); + + // Act + var result = id.CompareTo(null); + + // Assert + result.Should().BePositive(); + } + + [Fact] + public void CompareTo_ObjectIsNotTestIntId_ShouldThrowException() + { + // Arrange + var id = TestIntId.Create(42); + + // Act + var compareTo = () => id.CompareTo("not an TestIntId"); + + // Assert + compareTo.Should().Throw(); + } + + [Fact] + public void ToInt_ShouldReturnCorrectValue() + { + // Arrange + var id = TestIntId.Create(42); + + // Act + var integerValue = id.ToInt32(); + + // Assert + integerValue.Should().Be(42); + } + + [Fact] + public void ToString_ShouldReturnStringValue() + { + // Arrange + var id = TestIntId.Create(42); + + // Act + var stringValue = id.ToString(); + + // Assert + stringValue.Should().Be("42"); + } +} diff --git a/tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestShortIdTest.cs b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestShortIdTest.cs new file mode 100644 index 0000000..ecb96ae --- /dev/null +++ b/tests/LightResults.Extensions.GeneratedIdentifier.Tests/TestShortIdTest.cs @@ -0,0 +1,353 @@ +using FluentAssertions; +using GeneratedIdentifier.Common.ValueObjects; +using LightResults.Extensions.GeneratedIdentifier.Fixtures.Identifiers; + +// ReSharper disable SuspiciousTypeConversion.Global +// ReSharper disable EqualExpressionComparison +#pragma warning disable CS1718 // Comparison made to same variable + +namespace LightResults.Extensions.GeneratedIdentifier.Tests; + +public sealed class TestShortIdTest +{ + [Fact] + public void Create_ValidValue_ShouldSucceed() + { + // Arrange + const int validValue = 42; + + // Act + var id = TestShortId.Create(validValue); + + // Assert + id.Should().NotBeNull(); + id.ToInt16().Should().Be(validValue); + } + + [Fact] + public void Create_InvalidValue_ShouldThrowException() + { + // Arrange + const int invalidValue = -1; + + // Act + var create = () => TestShortId.Create(invalidValue); + + // Assert + create.Should().Throw(); + } + + [Fact] + public void TryCreate_ValidValue_ShouldSucceed() + { + // Arrange + const int validValue = 42; + + // Act + var result = TestShortId.TryCreate(validValue); + + // Assert + result.IsSuccess(out var id).Should().BeTrue(); + id.Should().NotBeNull(); + id.ToInt16().Should().Be(validValue); + } + + [Fact] + public void TryCreate_InvalidValue_ShouldFail() + { + // Arrange + const int invalidValue = -1; + + // Act + var result = TestShortId.TryCreate(invalidValue); + + // Assert + result.IsFailed().Should().BeTrue(); + result.Errors.Should().ContainSingle(); + } + + [Fact] + public void Parse_ValidString_ShouldSucceed() + { + // Arrange + const string validString = "42"; + + // Act + var result = TestShortId.Parse(validString); + + // Assert + + result.ToInt16().Should().Be(short.Parse(validString)); + } + + [Theory] + [InlineData("invalid")] + [InlineData("-1")] + public void Parse_InvalidString_ShouldThrowException(string invalidString) + { + // Act + var parse = () => TestShortId.Parse(invalidString); + + // Assert + parse.Should().Throw(); + } + + [Fact] + public void TryParse_ValidString_ShouldSucceed() + { + // Arrange + const string validString = "42"; + + // Act + var result = TestShortId.TryParse(validString); + + // Assert + result.IsSuccess(out var id).Should().BeTrue(); + id.Should().NotBeNull(); + id.ToInt16().Should().Be(short.Parse(validString)); + } + + [Theory] + [InlineData("invalid")] + [InlineData("-1")] + public void TryParse_InvalidString_ShouldFail(string invalidString) + { + // Act + var result = TestShortId.TryParse(invalidString); + + // Assert + result.IsFailed().Should().BeTrue(); + result.Errors.Should().ContainSingle(); + } + + [Fact] + public void Equals_SameValues_ShouldBeEqual() + { + // Arrange + var id1 = TestShortId.Create(42); + var id2 = TestShortId.Create(42); + + // Assert + id1.Should().Be(id2); + (id1 == id2).Should().BeTrue(); + (id1 != id2).Should().BeFalse(); + } + + [Fact] + public void Equals_DifferentValues_ShouldNotBeEqual() + { + // Arrange + var id1 = TestShortId.Create(42); + var id2 = TestShortId.Create(99); + + // Assert + id1.Should().NotBe(id2); + (id1 == id2).Should().BeFalse(); + (id1 != id2).Should().BeTrue(); + } + + [Fact] + public void Equals_ObjectIsNull_ShouldReturnFalse() + { + // Arrange + var id = TestShortId.Create(42); + + // Act + var result = id.Equals(null); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void Equals_ObjectIsNotTestShortId_ShouldReturnFalse() + { + // Arrange + var id = TestShortId.Create(42); + + // Act + var result = id.Equals("not an TestShortId"); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void GetHashCode_ShouldReturnCorrectValue() + { + // Arrange + const int underlyingValue = 42; + var id = TestShortId.Create(underlyingValue); + + // Act + var hashCode1 = id.GetHashCode(); + var hashCode2 = underlyingValue.GetHashCode(); + + // Assert + hashCode1.Should().Be(hashCode2); + } + + [Fact] + public void Operators_Equality_ShouldReturnTrue() + { + // Arrange + var id1 = TestShortId.Create(42); + var id2 = TestShortId.Create(42); + + // Assert + (id1 == id2).Should().BeTrue(); + (id1 != id2).Should().BeFalse(); + } + + [Fact] + public void Operators_Inequality_ShouldReturnTrue() + { + // Arrange + var id1 = TestShortId.Create(42); + var id2 = TestShortId.Create(99); + + // Assert + (id1 != id2).Should().BeTrue(); + (id1 == id2).Should().BeFalse(); + } + + [Fact] + public void CompareTo_SameValue_ShouldReturnZero() + { + // Arrange + var id1 = TestShortId.Create(42); + var id2 = TestShortId.Create(42); + + // Act + var result = id1.CompareTo(id2); + + // Assert + result.Should().Be(0); + } + + [Fact] + public void CompareTo_LesserValue_ShouldReturnNegative() + { + // Arrange + var id1 = TestShortId.Create(42); + var id2 = TestShortId.Create(99); + + // Act + var result = id1.CompareTo(id2); + + // Assert + result.Should().BeNegative(); + } + + [Fact] + public void CompareTo_GreaterValue_ShouldReturnPositive() + { + // Arrange + var id1 = TestShortId.Create(99); + var id2 = TestShortId.Create(42); + + // Act + var result = id1.CompareTo(id2); + + // Assert + result.Should().BePositive(); + } + + [Fact] + public void Operators_LessThan_ShouldReturnTrue() + { + // Arrange + var id1 = TestShortId.Create(42); + var id2 = TestShortId.Create(99); + + // Assert + (id1 < id2).Should().BeTrue(); + } + + [Fact] + public void Operators_GreaterThan_ShouldReturnTrue() + { + // Arrange + var id1 = TestShortId.Create(99); + var id2 = TestShortId.Create(42); + + // Assert + (id1 > id2).Should().BeTrue(); + } + + [Fact] + public void Operators_LessThanOrEqual_ShouldReturnTrue() + { + // Arrange + var id1 = TestShortId.Create(42); + var id2 = TestShortId.Create(99); + + // Assert + (id1 <= id2).Should().BeTrue(); + (id1 <= id1).Should().BeTrue(); + } + + [Fact] + public void Operators_GreaterThanOrEqual_ShouldReturnTrue() + { + // Arrange + var id1 = TestShortId.Create(99); + var id2 = TestShortId.Create(42); + + // Assert + (id1 >= id2).Should().BeTrue(); + (id1 >= id1).Should().BeTrue(); + } + + [Fact] + public void CompareTo_ObjectIsNull_ShouldReturnPositive() + { + // Arrange + var id = TestShortId.Create(42); + + // Act + var result = id.CompareTo(null); + + // Assert + result.Should().BePositive(); + } + + [Fact] + public void CompareTo_ObjectIsNotTestShortId_ShouldThrowException() + { + // Arrange + var id = TestShortId.Create(42); + + // Act + var compareTo = () => id.CompareTo("not an TestShortId"); + + // Assert + compareTo.Should().Throw(); + } + + [Fact] + public void ToInt_ShouldReturnCorrectValue() + { + // Arrange + var id = TestShortId.Create(42); + + // Act + var integerValue = id.ToInt16(); + + // Assert + integerValue.Should().Be(42); + } + + [Fact] + public void ToString_ShouldReturnStringValue() + { + // Arrange + var id = TestShortId.Create(42); + + // Act + var stringValue = id.ToString(); + + // Assert + stringValue.Should().Be("42"); + } +} diff --git a/tools/Benchmarks/Benchmarks.csproj b/tools/Benchmarks/Benchmarks.csproj index 5a511ea..04ed832 100644 --- a/tools/Benchmarks/Benchmarks.csproj +++ b/tools/Benchmarks/Benchmarks.csproj @@ -11,8 +11,8 @@ - - + +