From d799cb985c2a180107d54c16b846d98365e703f9 Mon Sep 17 00:00:00 2001 From: Stuart Turner Date: Sat, 1 Jun 2024 13:06:49 -0500 Subject: [PATCH 1/2] Add support for `Vogen` validations --- Directory.Packages.props | 3 +- .../ITypeSymbolExtensions.cs | 20 ++++++ ...ImmediateValidationsGenerator.Transform.cs | 27 +++++-- .../Models.cs | 1 + .../Templates/Validations.sbntxt | 12 ++++ ...mediate.Validations.FunctionalTests.csproj | 1 + .../IntegrationTests/VogenTests.cs | 71 +++++++++++++++++++ ...alidation#IV...ValidateClass.g.verified.cs | 62 ++++++++++++++++ .../GeneratorTests/CustomValidationTests.cs | 33 +++++++++ .../Immediate.Validations.Tests.csproj | 1 + tests/Immediate.Validations.Tests/Utility.cs | 1 + 11 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 tests/Immediate.Validations.FunctionalTests/IntegrationTests/VogenTests.cs create mode 100644 tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.VogenValidation#IV...ValidateClass.g.verified.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 4375691..b64e82e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,7 @@ true - + @@ -28,6 +28,7 @@ + diff --git a/src/Immediate.Validations.Generators/ITypeSymbolExtensions.cs b/src/Immediate.Validations.Generators/ITypeSymbolExtensions.cs index 5565f59..d6fed82 100644 --- a/src/Immediate.Validations.Generators/ITypeSymbolExtensions.cs +++ b/src/Immediate.Validations.Generators/ITypeSymbolExtensions.cs @@ -88,6 +88,26 @@ typeSymbol is }, }; + public static bool IsVogenAttribute(this INamedTypeSymbol? typeSymbol) => + typeSymbol is + { + Name: "ValueObjectAttribute", + ContainingNamespace: + { + Name: "Vogen", + ContainingNamespace.IsGlobalNamespace: true, + }, + } + or + { + MetadataName: "ValueObjectAttribute`1", + ContainingNamespace: + { + Name: "Vogen", + ContainingNamespace.IsGlobalNamespace: true, + }, + }; + public static bool IsValidatorAttribute(this INamedTypeSymbol? typeSymbol) => typeSymbol is { diff --git a/src/Immediate.Validations.Generators/ImmediateValidationsGenerator.Transform.cs b/src/Immediate.Validations.Generators/ImmediateValidationsGenerator.Transform.cs index 5c49455..c740995 100644 --- a/src/Immediate.Validations.Generators/ImmediateValidationsGenerator.Transform.cs +++ b/src/Immediate.Validations.Generators/ImmediateValidationsGenerator.Transform.cs @@ -169,8 +169,22 @@ CancellationToken token token.ThrowIfCancellationRequested(); - var isValidationProperty = propertyType.GetAttributes() - .Any(v => v.AttributeClass.IsValidateAttribute()); + var isValidationProperty = + propertyType + .GetAttributes() + .Any(v => v.AttributeClass.IsValidateAttribute()); + + var isVogenProperty = + propertyType + .GetAttributes() + .Any(v => v.AttributeClass.IsVogenAttribute()) + && propertyType + .GetMembers() + .Any(m => m is + { + Name: "Validate", + IsStatic: true, + }); token.ThrowIfCancellationRequested(); @@ -323,6 +337,7 @@ CancellationToken token if ( (isNullable || !isReferenceType) && !isValidationProperty + && !isVogenProperty && collectionPropertyDetails is null && validations is [] ) @@ -339,9 +354,11 @@ CancellationToken token IsNullable = isNullable, IsValidationProperty = isValidationProperty, - ValidationTypeFullName = isValidationProperty - ? baseType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) - : null, + IsVogenProperty = isVogenProperty, + ValidationTypeFullName = + (isValidationProperty || isVogenProperty) + ? baseType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + : null, CollectionPropertyDetails = collectionPropertyDetails, diff --git a/src/Immediate.Validations.Generators/Models.cs b/src/Immediate.Validations.Generators/Models.cs index f5f2acc..2c0129e 100644 --- a/src/Immediate.Validations.Generators/Models.cs +++ b/src/Immediate.Validations.Generators/Models.cs @@ -25,6 +25,7 @@ public sealed record ValidationTargetProperty public required bool IsReferenceType { get; init; } public required bool IsNullable { get; init; } public required bool IsValidationProperty { get; init; } + public required bool IsVogenProperty { get; init; } public required string? ValidationTypeFullName { get; init; } public required ValidationTargetProperty? CollectionPropertyDetails { get; init; } public required EquatableReadOnlyList Validations { get; init; } diff --git a/src/Immediate.Validations.Generators/Templates/Validations.sbntxt b/src/Immediate.Validations.Generators/Templates/Validations.sbntxt index 1d8dddd..aed189d 100644 --- a/src/Immediate.Validations.Generators/Templates/Validations.sbntxt +++ b/src/Immediate.Validations.Generators/Templates/Validations.sbntxt @@ -116,6 +116,18 @@ partial {{ class.type }} {{ class.name }} PropertyName = $"{{ get_prop_name(p.name, depth) }}.{error.PropertyName}", }); } + {{~ else if p.is_vogen_property ~}} + { + var validation = {{ p.validation_type_full_name }}.Validate(t.Value); + if (!string.IsNullOrWhiteSpace(validation.ErrorMessage)) + { + errors.Add(new() + { + PropertyName = $"{{ get_prop_name(p.name, depth) }}", + ErrorMessage = validation.ErrorMessage, + }); + } + } {{~ end ~}} {{~ if p.collection_property_details ~}} diff --git a/tests/Immediate.Validations.FunctionalTests/Immediate.Validations.FunctionalTests.csproj b/tests/Immediate.Validations.FunctionalTests/Immediate.Validations.FunctionalTests.csproj index 57e3ce0..f492cfb 100644 --- a/tests/Immediate.Validations.FunctionalTests/Immediate.Validations.FunctionalTests.csproj +++ b/tests/Immediate.Validations.FunctionalTests/Immediate.Validations.FunctionalTests.csproj @@ -16,6 +16,7 @@ + diff --git a/tests/Immediate.Validations.FunctionalTests/IntegrationTests/VogenTests.cs b/tests/Immediate.Validations.FunctionalTests/IntegrationTests/VogenTests.cs new file mode 100644 index 0000000..c7c9573 --- /dev/null +++ b/tests/Immediate.Validations.FunctionalTests/IntegrationTests/VogenTests.cs @@ -0,0 +1,71 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using Immediate.Validations.Shared; +using Vogen; +using Xunit; + +namespace Immediate.Validations.FunctionalTests.IntegrationTests; + +[ValueObject(deserializationStrictness: DeserializationStrictness.AllowAnything)] +[SuppressMessage( + "Design", + "CA1036:Override methods on comparable types", + Justification = "Intentionally not implemented in Vogen" +)] +public readonly partial struct UserId +{ + public static Validation Validate(int value) => + value > 0 ? Validation.Ok : Validation.Invalid("Must be greater than zero."); +} + +public sealed partial class VogenTests +{ + [Validate] + public sealed partial record VogenRecord : IValidationTarget + { + public required UserId UserId { get; init; } + } + + [Fact] + public void ValidUserIdWithNoErrors() + { + var record = JsonSerializer.Deserialize( + /*lang=json,strict*/ + """ + { + "UserId": 1 + } + """ + ); + + var errors = VogenRecord.Validate(record); + + Assert.Empty(errors); + } + + [Fact] + public void InvalidUserIdWithErrors() + { + var record = JsonSerializer.Deserialize( + /*lang=json,strict*/ + """ + { + "UserId": -1 + } + """ + ); + + var errors = VogenRecord.Validate(record); + + Assert.Equal( + [ + new() + { + PropertyName = "UserId", + ErrorMessage = "Must be greater than zero.", + } + ], + errors + ); + } +} diff --git a/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.VogenValidation#IV...ValidateClass.g.verified.cs b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.VogenValidation#IV...ValidateClass.g.verified.cs new file mode 100644 index 0000000..00ee023 --- /dev/null +++ b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.VogenValidation#IV...ValidateClass.g.verified.cs @@ -0,0 +1,62 @@ +//HintName: IV...ValidateClass.g.cs +using System.Collections.Generic; +using Immediate.Validations.Shared; + +#nullable enable +#pragma warning disable CS1591 + + +partial class ValidateClass +{ + static List IValidationTarget.Validate(ValidateClass? target) => + Validate(target); + + public static List Validate(ValidateClass? target) + { + if (target is not { } t) + { + return + [ + new() + { + PropertyName = ".self", + ErrorMessage = "`target` must not be `null`.", + }, + ]; + } + + var errors = new List(); + + + __ValidateUserId(errors, t, t.UserId); + + + return errors; + } + + + + private static void __ValidateUserId( + List errors, ValidateClass instance, global::UserId target + ) + { + + var t = target; + + { + var validation = global::UserId.Validate(t.Value); + if (!string.IsNullOrWhiteSpace(validation.ErrorMessage)) + { + errors.Add(new() + { + PropertyName = $"UserId", + ErrorMessage = validation.ErrorMessage, + }); + } + } + + + } + +} + diff --git a/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.cs b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.cs index 82f2ba1..df59a6e 100644 --- a/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.cs +++ b/tests/Immediate.Validations.Tests/GeneratorTests/CustomValidationTests.cs @@ -412,4 +412,37 @@ public partial class SubClass : BaseClass, IValidationTarget; _ = await Verify(result); } + + [Fact] + public async Task VogenValidation() + { + var driver = GeneratorTestHelper.GetDriver( + """ + #nullable enable + + using System.Collections.Generic; + using Immediate.Validations.Shared; + using Vogen; + + [ValueObject] + public readonly partial struct UserId + { + public static Validation Validate(int value) => + value > 0 ? Validation.Ok : Validation.Invalid("Must be greater than zero."); + } + + [Validate] + public partial class ValidateClass : IValidationTarget + { + public required UserId UserId { get; init; } + } + """); + + var result = driver.GetRunResult(); + + Assert.Empty(result.Diagnostics); + _ = Assert.Single(result.GeneratedTrees); + + _ = await Verify(result); + } } diff --git a/tests/Immediate.Validations.Tests/Immediate.Validations.Tests.csproj b/tests/Immediate.Validations.Tests/Immediate.Validations.Tests.csproj index 7ede801..b61721f 100644 --- a/tests/Immediate.Validations.Tests/Immediate.Validations.Tests.csproj +++ b/tests/Immediate.Validations.Tests/Immediate.Validations.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/tests/Immediate.Validations.Tests/Utility.cs b/tests/Immediate.Validations.Tests/Utility.cs index 0d519e2..a3974d3 100644 --- a/tests/Immediate.Validations.Tests/Utility.cs +++ b/tests/Immediate.Validations.Tests/Utility.cs @@ -8,5 +8,6 @@ public static MetadataReference[] GetMetadataReferences() => [ MetadataReference.CreateFromFile("./Immediate.Handlers.Shared.dll"), MetadataReference.CreateFromFile("./Immediate.Validations.Shared.dll"), + MetadataReference.CreateFromFile("./Vogen.SharedTypes.dll"), ]; } From 9f9695999292741988f34d732a7d2e5222746cd4 Mon Sep 17 00:00:00 2001 From: Stuart Turner Date: Sat, 1 Jun 2024 13:09:37 -0500 Subject: [PATCH 2/2] Update dependency --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b64e82e..bdb296b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,7 +8,7 @@ - +