diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4375691..bdb296b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,13 +2,13 @@
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"),
];
}