diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0530aef..732a2da 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -7,7 +7,7 @@
5.9.0
1.68.4
1.0.0-prerelease.44
- 2.0.0-prerelease.10
+ 2.0.0-prerelease.11
diff --git a/README.md b/README.md
index 6186414..61cec7d 100644
--- a/README.md
+++ b/README.md
@@ -75,7 +75,7 @@ modelBuilder
.HasColumnType("jsonb");
```
-See the `samples/` folder for end-to-end examples and `docs/` for guidance.
+See the `src/src/Sample` and `src/src/ZodSharpSample` projects for end-to-end examples and `docs/` for guidance.
## Validation with ZodSharp
@@ -104,8 +104,8 @@ public readonly partial record struct EmailAddress
var result = EmailAddressSchema.Validate(EmailAddress.Create("demo@example.com"));
```
-See [ZodSharp Validation](docs/ZodSharp-Validation.md), the `samples/ValueObjects.ZodSharpSample` project, and
-the `src/ZodSharp.AspNetCoreSample` project (ASP.NET Core Problem Details for strict deserialization failures).
+See [ZodSharp Validation](docs/ZodSharp-Validation.md), the `src/src/ZodSharpSample` project, and
+the `src/src/ZodSharp.AspNetCoreSample` project (ASP.NET Core Problem Details for strict deserialization failures).
## How it works
diff --git a/docs/Entity-Framework.md b/docs/Entity-Framework.md
index 8bf165f..968cbc3 100644
--- a/docs/Entity-Framework.md
+++ b/docs/Entity-Framework.md
@@ -71,4 +71,4 @@ using the generated `[JsonConverter]` (present by default) or the shared options
to support EF Core materialization.
- Value objects are immutable; EF tracks them by value like any struct/record.
-See `samples/` for a runnable DTO + JSON-column example.
\ No newline at end of file
+See `src/src/Sample` for a runnable DTO + JSON-column example.
\ No newline at end of file
diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md
index 0459c1a..ccd9e1c 100644
--- a/docs/Getting-Started.md
+++ b/docs/Getting-Started.md
@@ -176,13 +176,13 @@ constructed instance through `EmailAddressSchema` — `EmailAddress.Create("not-
In ASP.NET Core, `Purview.ZodSharp.AspNetCore` converts those `ZodException`s into standard Problem
Details responses — combine `ValueObjectDeserializationMode.Strict` with
`AddZodSharpProblemDetails()` + `UseExceptionHandler()` so invalid request bodies return
-`HttpValidationProblemDetails` automatically. See the `src/ZodSharp.AspNetCoreSample` project.
+`HttpValidationProblemDetails` automatically. See the `src/src/ZodSharp.AspNetCoreSample` project.
-See `ZodSharp-Validation.md` and the `samples/` folder.
+See `ZodSharp-Validation.md` and the `src/src/ZodSharpSample` project.
## Next steps
- `Entity-Framework.md` – mapping value objects to EF JSON columns.
- `Value-Object-Design.md` – where validation lives and the `Create`/`Hydrate` split.
- `ZodSharp-Validation.md` – validating value objects with Purview.ZodSharp.
-- The `samples/` folder for runnable examples.
\ No newline at end of file
+- The `src/src/Sample` and `src/src/ZodSharpSample` projects for runnable examples.
\ No newline at end of file
diff --git a/docs/ZodSharp-Validation.md b/docs/ZodSharp-Validation.md
index 49cd4d2..dd32255 100644
--- a/docs/ZodSharp-Validation.md
+++ b/docs/ZodSharp-Validation.md
@@ -4,7 +4,7 @@
[Zod](https://github.com/colinhacks/zod) schema validation library. It complements `Purview.ValueObjects`:
the value object owns the invariants, ZodSharp owns the rule definitions and validation results.
-Three patterns are covered here, demonstrated in the `samples/` folder:
+Three patterns are covered here, demonstrated in the `src/src/ZodSharpSample` project:
1. **Generator-integrated validation** — a value object annotated with both `[Scalar]`/`[ValueObject]`
and `[ZodSchema]` has its generated `Create` wired to the ZodSharp-generated schema.
@@ -110,7 +110,18 @@ public readonly partial record struct PhoneNumber
}
```
-A custom schema class name (from ZodSharp's `[ZodSchema(SchemaName = "...")]`) is honored.
+The `[ZodSchema]` attribute also exposes generator options that tune the emitted schema:
+
+- `RefinementMethodName` — names a synchronous instance refinement method (default `Validate`) that the
+ generator runs after the DataAnnotations rules.
+- `CustomValidationMethodName` — names a static async method that the generated validator's
+ `ValidateAsync` awaits after the synchronous rules pass (default `CustomValidationAsync`).
+- `GenerateParseMethod` / `GenerateValidateMethod` / `EnableComposition` — toggle the emitted `Parse`,
+ `Validate`, and composition (`ApplyAnd`/`ApplyOr`/`ApplyRefine`) members.
+
+> Note: `SchemaName` on `[ZodSchema]` is reserved by the attribute today but is not yet applied by the
+> ZodSharp generator — the generated schema class is always named `{TypeName}Schema`. Use the default
+> name when combining `[Scalar]`/`[ValueObject]` with `[ZodSchema]`.
## 3. Schema-first validation
@@ -156,7 +167,7 @@ Annotate a request/DTO class with `[ZodSchema]`, validate it, then map the valid
value objects:
```csharp
-[ZodSchema]
+[ZodSchema(RefinementMethodName = nameof(ValidateRegistration))]
public sealed class RegistrationDto
{
[Required, StringLength(100, MinimumLength = 2)]
@@ -168,8 +179,9 @@ public sealed class RegistrationDto
[Required, EmailAddress]
public string Email { get; init; } = string.Empty;
- // Custom sync refinement: the generator runs these errors after the DataAnnotations rules.
- public IEnumerable Validate()
+ // Custom sync refinement, discovered via the RefinementMethodName option. The generator runs
+ // these errors after the DataAnnotations rules.
+ public IEnumerable ValidateRegistration()
{
if (Name.StartsWith("x", StringComparison.OrdinalIgnoreCase))
yield return new ValidationError("name", "Name cannot start with 'x'.", [nameof(Name)]);
@@ -186,6 +198,34 @@ if (result.IsSuccess)
}
```
+### Async custom validation
+
+`CustomValidationMethodName` names a static async method with the signature
+`static ValueTask> Method(T value, CancellationToken cancellationToken)`. The
+generated `{Type}SchemaValidator` (which implements `IZodSchemaValidator`) awaits it in its
+`ValidateAsync` after the synchronous rules pass:
+
+```csharp
+[ZodSchema(CustomValidationMethodName = nameof(ValidatePromoCodeAsync))]
+public sealed class PromoCode
+{
+ [Required, RegularExpression(@"^[A-Z0-9]{4,10}$")]
+ public string Code { get; init; } = string.Empty;
+
+ internal static ValueTask> ValidatePromoCodeAsync(
+ PromoCode value, CancellationToken cancellationToken) =>
+ ValueTask.FromResult(
+ value.Code is "SAVE10" or "WELCOME20"
+ ? ValidationResult.Success(value)
+ : ValidationResult.Failure(
+ new ValidationError("code", "Unknown promotional code.", [nameof(Code)]))
+ );
+}
+
+PromoCodeSchemaValidator validator = new();
+var result = await validator.ValidateAsync(new PromoCode { Code = "HOMERUN42" });
+```
+
## 5. Dependency injection and the schema factory
`ZodSchemaFactory` resolves validators by validated type. Register the generated adapter or wrap a
@@ -254,45 +294,44 @@ builder.Services.ConfigureHttpJsonOptions(options =>
A `POST` body with an invalid email now returns `400 application/problem+json` with the structured issues
in the `issues` extension.
-Map error codes to HTTP statuses and formatted messages with `ErrorType` + `ErrorTypeRegistry`:
+Map error codes to HTTP statuses and formatted messages with `ErrorType` + `ErrorTypeRegistry`. Mark a
+static partial class with `[ErrorType]` on a `static readonly ErrorType` field and the bundled
+`ErrorTypeGenerator` emits `Create{Field}(...)` (builds a `ValidationError`) and `Throw{Field}(...)`
+(a `void` + `[DoesNotReturn]` method that throws the `ZodException`):
```csharp
-public static class ConcurrentErrorType
+[ErrorType]
+public static readonly ErrorType SaveFailed = new(
+ Code: "aggregate_save_failed",
+ Description: "The order could not be saved because it was modified concurrently.",
+ HttpStatus: StatusCodes.Status409Conflict,
+ MessageFormat: "Order '{OrderId}' (of type {AggregateType}) failed to save")
{
- public static readonly ErrorType SaveFailed = new(
- Code: "aggregate_save_failed",
- Description: "The order could not be saved because it was modified concurrently.",
- HttpStatus: StatusCodes.Status409Conflict,
- MessageFormat: "Order '{OrderId}' (of type {AggregateType}) failed to save")
- {
- Parameters = ["OrderId", "AggregateType"]
- };
-}
+ Parameters = ["OrderId", "AggregateType"]
+};
-ErrorTypeRegistry.Default.Register(ConcurrentErrorType.SaveFailed);
+ErrorTypeRegistry.Default.Register(ErrorTypes.SaveFailed);
```
-Throwing a `ZodException` with that code and parameters yields a `409 Conflict` whose message is
-formatted from the error's parameters:
+The generated `ThrowSaveFailed(orderId, aggregateType)` throws a `ZodException` carrying the
+`aggregate_save_failed` code, yielding a `409 Conflict` whose message is formatted from the error's
+parameters. Because it is `void` + `[DoesNotReturn]`, use it as a terminal call — for example a `void`
+minimal-API handler that always throws (the endpoint returns the mapped `409` via the exception
+handler):
```csharp
-throw new ZodException([
- ValidationError.Create(
- "aggregate_save_failed",
- "The order could not be saved.",
- path: [],
- parameters: new Dictionary
- {
- ["OrderId"] = orderId,
- ["AggregateType"] = "Order",
- }),
-]);
+app.MapPost("/orders/{orderId}/confirm", ConfirmOrder);
+
+static void ConfirmOrder(string orderId) => ErrorTypes.ThrowSaveFailed(orderId, "Order");
```
+(If you prefer not to use the generator, construct the `ZodException` manually with
+`ValidationError.Create(code, message, path: [], parameters: ...)` — it maps the same way.)
+
The bundled `ZODSASP001` analyzer flags `MessageFormat` placeholders missing from `Parameters` at
compile time. See the
[ASP.NET Core integration](https://purview.dev/docs/zodsharp/aspnetcore-integration/) guide and the
-`src/ZodSharp.AspNetCoreSample` project.
+`src/src/ZodSharp.AspNetCoreSample` project.
## JSON Schema export
@@ -310,6 +349,6 @@ that references the package, so `[ZodSchema]` is available there.
## See also
-- The runnable `samples/ValueObjects.ZodSharpSample` project.
+- The runnable `src/src/ZodSharpSample` project.
- [Getting Started](Getting-Started.md)
- [Value Object Design](Value-Object-Design.md)
\ No newline at end of file
diff --git a/src/src/Sample/README.md b/src/src/Sample/README.md
index 7c79ef1..17afdf2 100644
--- a/src/src/Sample/README.md
+++ b/src/src/Sample/README.md
@@ -6,7 +6,7 @@ Entity Framework JSON-column shape.
## Run
```text
-dotnet run --project samples/ValueObjects.Sample
+dotnet run --project src/src/Sample
```
## What it shows
diff --git a/src/src/ValueObjects/Sdk/README.md b/src/src/ValueObjects/Sdk/README.md
index 9df5b83..238a15c 100644
--- a/src/src/ValueObjects/Sdk/README.md
+++ b/src/src/ValueObjects/Sdk/README.md
@@ -50,4 +50,4 @@ modelBuilder
.HasColumnType("jsonb");
```
-See the `samples/` folder for end-to-end examples.
\ No newline at end of file
+See the `src/src/Sample` and `src/src/ZodSharpSample` projects for end-to-end examples.
\ No newline at end of file
diff --git a/src/src/ZodSharp.AspNetCoreSample/ErrorTypes.cs b/src/src/ZodSharp.AspNetCoreSample/ErrorTypes.cs
index 3629aa5..ca3dea9 100644
--- a/src/src/ZodSharp.AspNetCoreSample/ErrorTypes.cs
+++ b/src/src/ZodSharp.AspNetCoreSample/ErrorTypes.cs
@@ -7,11 +7,12 @@ namespace Purview.ValueObjects.ZodSharp.AspNetCoreSample;
/// HTTP status codes and formatted messages. The bundled ZODSASP001 analyzer verifies that
/// every placeholder is declared in .
///
-static class ConcurrentErrorType
+static partial class ErrorTypes
{
///
/// Maps the aggregate_save_failed code to a 409 Conflict response.
///
+ [ErrorType]
public static readonly ErrorType SaveFailed = new(
Code: "aggregate_save_failed",
Description: "The order could not be saved because it was modified concurrently.",
diff --git a/src/src/ZodSharp.AspNetCoreSample/Program.cs b/src/src/ZodSharp.AspNetCoreSample/Program.cs
index 14c05c5..edfd88c 100644
--- a/src/src/ZodSharp.AspNetCoreSample/Program.cs
+++ b/src/src/ZodSharp.AspNetCoreSample/Program.cs
@@ -19,7 +19,7 @@
builder.Services.AddProblemDetails();
// Map error codes to HTTP statuses and formatted messages.
-ErrorTypeRegistry.Default.Register(ConcurrentErrorType.SaveFailed);
+ErrorTypeRegistry.Default.Register(ErrorTypes.SaveFailed);
var app = builder.Build();
@@ -36,18 +36,11 @@
// Throws a ZodException carrying a registered error code. The handler resolves the ErrorType from
// the registry and returns a 409 Conflict response whose message is formatted from the error's
-// parameters.
+// parameters. ThrowSaveFailed is generated as void + [DoesNotReturn], so the endpoint is a void
+// handler that always throws.
app.MapPost("/orders/{orderId}/confirm", ConfirmOrder);
-static IResult ConfirmOrder(string orderId) =>
- throw new ZodException([
- ValidationError.Create(
- "aggregate_save_failed",
- "The order could not be saved.",
- path: [],
- parameters: new Dictionary { ["OrderId"] = orderId, ["AggregateType"] = "Order" }
- ),
- ]);
+static void ConfirmOrder(string orderId) => ErrorTypes.ThrowSaveFailed(orderId, "Order");
// Demonstrates on-demand mapping: a ZodException caught in the handler is converted explicitly
// with ErrorType resolution, without relying on the exception-handling middleware.
diff --git a/src/src/ZodSharp.AspNetCoreSample/README.md b/src/src/ZodSharp.AspNetCoreSample/README.md
index ad55fbe..3fb0cee 100644
--- a/src/src/ZodSharp.AspNetCoreSample/README.md
+++ b/src/src/ZodSharp.AspNetCoreSample/README.md
@@ -18,9 +18,10 @@ dotnet run --project src/src/ZodSharp.AspNetCoreSample
- **Automatic exception handling** — `AddZodSharpProblemDetails()` registers `ZodExceptionHandler` and
`app.UseExceptionHandler()` catches the thrown `ZodException`, converting it to a
`HttpValidationProblemDetails` response with the structured issues in the `issues` extension.
-- **Mapping error types to status codes** — `ConcurrentErrorType.SaveFailed` is registered in
- `ErrorTypeRegistry.Default`; a `ZodException` carrying the `aggregate_save_failed` code is returned as
- a `409 Conflict` with a message formatted from the error's parameters.
+- **Mapping error types to status codes** — `ErrorTypes.SaveFailed` (a `[ErrorType]`-generated partial)
+ is registered in `ErrorTypeRegistry.Default`; `ErrorTypes.ThrowSaveFailed(...)` throws a `ZodException`
+ carrying the `aggregate_save_failed` code, which is returned as a `409 Conflict` with a message
+ formatted from the error's parameters.
- **On-demand mapping** — `ZodException.ToHttpValidationProblemDetails(...)` converts an exception
explicitly, without the exception-handling middleware.
- **Compile-time placeholder checking** — the `ZODSASP001` analyzer (bundled with the package) verifies
diff --git a/src/src/ZodSharpSample/Program.cs b/src/src/ZodSharpSample/Program.cs
index 9f3d186..13c6282 100644
--- a/src/src/ZodSharpSample/Program.cs
+++ b/src/src/ZodSharpSample/Program.cs
@@ -16,6 +16,10 @@
Console.WriteLine("== Generated DTO validation ==");
DtoValidation();
+Console.WriteLine();
+Console.WriteLine("== Async custom validation ==");
+await AsyncCustomValidation();
+
Console.WriteLine();
Console.WriteLine("== DI / factory ==");
FactoryValidation();
@@ -141,6 +145,23 @@ static void DtoValidation()
Console.WriteLine($"Mapped -> {email.Value}, {money.Amount} {money.Currency.Value}");
}
+static async Task AsyncCustomValidation()
+{
+ // [ZodSchema(CustomValidationMethodName = ...)] names a static async validation method that the
+ // generated validator adapter awaits in its ValidateAsync after the synchronous rules pass.
+ PromoCodeSchemaValidator validator = new();
+
+ PromoCode known = new() { Code = "SAVE10" };
+ var knownResult = await validator.ValidateAsync(known, CancellationToken.None);
+ Console.WriteLine($"PromoCodeSchemaValidator.ValidateAsync('SAVE10') -> {knownResult.IsSuccess}");
+
+ PromoCode unknown = new() { Code = "HOMERUN42" };
+ var unknownResult = await validator.ValidateAsync(unknown, CancellationToken.None);
+ Console.WriteLine(
+ $"PromoCodeSchemaValidator.ValidateAsync('HOMERUN42') -> {unknownResult.IsSuccess}, {FormatErrors(unknownResult.Errors)}"
+ );
+}
+
static void FactoryValidation()
{
ZodSchemaFactory factory = new();
diff --git a/src/src/ZodSharpSample/PromoCode.cs b/src/src/ZodSharpSample/PromoCode.cs
new file mode 100644
index 0000000..1bbe50d
--- /dev/null
+++ b/src/src/ZodSharpSample/PromoCode.cs
@@ -0,0 +1,36 @@
+using System.ComponentModel.DataAnnotations;
+using ZodSharp;
+using ZodSharp.Core;
+
+namespace Purview.ValueObjects.ZodSharpSample;
+
+///
+/// A DTO validated by the source-generated PromoCodeSchema. The
+/// CustomValidationMethodName option names a static async validation method that the generator
+/// wires into the generated ValidateAsync, which awaits it after the synchronous DataAnnotations
+/// rules pass.
+///
+[ZodSchema(CustomValidationMethodName = nameof(ValidatePromoCodeAsync))]
+sealed class PromoCode
+{
+ [Required]
+ [RegularExpression(@"^[A-Z0-9]{4,10}$")]
+ public string Code { get; init; } = string.Empty;
+
+ internal static ValueTask> ValidatePromoCodeAsync(
+ PromoCode value,
+ CancellationToken cancellationToken
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var isKnownPromo = value.Code is "SAVE10" or "WELCOME20";
+ return ValueTask.FromResult(
+ isKnownPromo
+ ? ValidationResult.Success(value)
+ : ValidationResult.Failure(
+ new ValidationError("code", "Unknown promotional code.", [nameof(Code)])
+ )
+ );
+ }
+}
diff --git a/src/src/ZodSharpSample/README.md b/src/src/ZodSharpSample/README.md
index b256894..167bed5 100644
--- a/src/src/ZodSharpSample/README.md
+++ b/src/src/ZodSharpSample/README.md
@@ -22,7 +22,10 @@ dotnet run --project src/src/ZodSharpSample
`Z.Enum()`, `Z.Number().Positive()`) validate the raw underlying value, then the
result is mapped onto the value object via its strict `Create` factory.
- **DTO validation** — a `[ZodSchema]` `RegistrationDto` validated by the generated schema, including
- a custom `Validate()` refinement method, then mapped to value objects.
+ a custom refinement method wired via the `RefinementMethodName` option, then mapped to value objects.
+- **Async custom validation** — a `[ZodSchema(CustomValidationMethodName = ...)]` `PromoCode` whose
+ generated `PromoCodeSchemaValidator.ValidateAsync` awaits a hand-written async rule after the
+ synchronous DataAnnotations rules pass.
- **DI / factory** — `ZodSchemaFactory` resolving both the generated `EmailAddressSchemaValidator`
and a hand-built `ZodSchemaValidator`.
diff --git a/src/src/ZodSharpSample/RegistrationDto.cs b/src/src/ZodSharpSample/RegistrationDto.cs
index a6533dc..8922e68 100644
--- a/src/src/ZodSharpSample/RegistrationDto.cs
+++ b/src/src/ZodSharpSample/RegistrationDto.cs
@@ -8,7 +8,7 @@ namespace Purview.ValueObjects.ZodSharpSample;
/// A plain DTO validated by the source-generated RegistrationDtoSchema /
/// RegistrationDtoSchemaValidator. Values are mapped to value objects after validation.
///
-[ZodSchema]
+[ZodSchema(RefinementMethodName = nameof(ValidateRegistration))]
sealed class RegistrationDto
{
[Required]
@@ -23,10 +23,10 @@ sealed class RegistrationDto
public string Email { get; init; } = string.Empty;
///
- /// A custom sync refinement method. The generator discovers this Validate method and runs
- /// the returned errors after the DataAnnotations rules.
+ /// A custom sync refinement method, wired up via RefinementMethodName. The generator
+ /// discovers this instance method and runs the returned errors after the DataAnnotations rules.
///
- public IEnumerable Validate()
+ public IEnumerable ValidateRegistration()
{
if (Name.StartsWith('x'))
yield return new ValidationError("name", "Name cannot start with 'x'.", [nameof(Name)]);
diff --git a/src/tests/SourceGenerator.UnitTests/Analyzers/ValueObjectDiagnosticAnalyzerTests.cs b/src/tests/SourceGenerator.UnitTests/Analyzers/ValueObjectDiagnosticAnalyzerTests.cs
index eed1e38..ea0585b 100644
--- a/src/tests/SourceGenerator.UnitTests/Analyzers/ValueObjectDiagnosticAnalyzerTests.cs
+++ b/src/tests/SourceGenerator.UnitTests/Analyzers/ValueObjectDiagnosticAnalyzerTests.cs
@@ -181,7 +181,9 @@ CancellationToken cancellationToken
{
return base.OnBeforeRun(
sources,
- options.WithAdditionalNamespaces(TypeLibrary.SerializationNamespace),
+ // Fully-qualified because the referenced ZodSharp generator assembly exposes a
+ // global-namespace TypeLibrary that would otherwise shadow the source generator's.
+ options.WithAdditionalNamespaces(Common.TypeLibrary.SerializationNamespace),
cancellationToken
);
}
diff --git a/src/tests/SourceGenerator.UnitTests/Common/ZodSchemaValidationGeneratorTestOptions.cs b/src/tests/SourceGenerator.UnitTests/Common/ZodSchemaValidationGeneratorTestOptions.cs
new file mode 100644
index 0000000..1a75325
--- /dev/null
+++ b/src/tests/SourceGenerator.UnitTests/Common/ZodSchemaValidationGeneratorTestOptions.cs
@@ -0,0 +1,20 @@
+using ZodSharp.SourceGenerators;
+
+namespace Purview.ValueObjects.SourceGenerator.Common;
+
+///
+/// Options for generator tests that exercise the ZodSharp integration end-to-end: the real
+/// Purview.ZodSharp source generator runs alongside the value-object generator, so [ZodSchema]
+/// and the {Type}Schema classes come from the ZodSharp generator rather than being mocked
+/// in the test source.
+///
+public sealed record ZodSchemaValidationGeneratorTestOptions : ValueObjectsGeneratorTestOptions
+{
+ public ZodSchemaValidationGeneratorTestOptions()
+ {
+ AdditionalGeneratorTypes = [.. AdditionalGeneratorTypes, typeof(ZodSchemaGenerator)];
+ ExcludeGeneratedSourceHintNames = [.. ExcludeGeneratedSourceHintNames, "ZodSchemaAttribute.g.cs"];
+ }
+
+ public static new ZodSchemaValidationGeneratorTestOptions Default => new();
+}
diff --git a/src/tests/SourceGenerator.UnitTests/Generators/ZodSchemaValidationGeneratorTests.cs b/src/tests/SourceGenerator.UnitTests/Generators/ZodSchemaValidationGeneratorTests.cs
index 5e9a409..a6428f4 100644
--- a/src/tests/SourceGenerator.UnitTests/Generators/ZodSchemaValidationGeneratorTests.cs
+++ b/src/tests/SourceGenerator.UnitTests/Generators/ZodSchemaValidationGeneratorTests.cs
@@ -1,9 +1,10 @@
namespace Purview.ValueObjects.SourceGenerator.Generators;
///
-/// Tests the value-object generator's ZodSharp integration: when a value object is also annotated
-/// with [ZodSchema], the generated Create validates the constructed instance through
-/// the source-generated schema class.
+/// Tests the value-object generator's ZodSharp integration against the real Purview.ZodSharp source
+/// generator: when a value object is also annotated with [ZodSchema] (the attribute emitted by
+/// the ZodSharp generator), the generated Create validates the constructed instance through
+/// the schema class the ZodSharp generator produces.
///
public sealed class ZodSchemaValidationGeneratorTests : ValueObjectSourceGeneratorTestBase
{
@@ -11,38 +12,18 @@ public sealed class ZodSchemaValidationGeneratorTests : ValueObjectSourceGenerat
public async Task Scalar_GivenZodSchema_GeneratedCreateValidatesViaSchema(CancellationToken cancellationToken)
{
const string source = """
+ using System.ComponentModel.DataAnnotations;
using ZodSharp;
- using ZodSharp.Core;
-
- namespace ZodSharp
- {
- [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)]
- public sealed class ZodSchemaAttribute : System.Attribute
- {
- public string? SchemaName { get; init; }
- }
- }
namespace Testing
{
- [ZodSchema]
- public static class EmailAddressSchema
- {
- public static ValidationResult Validate(EmailAddress value) =>
- value.Value.Contains('@', System.StringComparison.Ordinal)
- ? ValidationResult.Success(value)
- : ValidationResult.Failure(
- new ValidationError("invalid", "Invalid email.", [nameof(value)])
- );
- }
-
- [Purview.ValueObjects.Serialization.Scalar]
+ [Scalar]
[ZodSchema]
public readonly partial record struct EmailAddress
{
+ [EmailAddress]
+ [StringLength(254, MinimumLength = 3)]
public string Value { get; }
-
- private EmailAddress(string value) => Value = value;
}
public static class Harness
@@ -66,7 +47,11 @@ public static bool CreateRejectsInvalid()
}
""";
- var result = await GenerateAsync(source, ValueObjectsGeneratorTestOptions.Default.Compile(), cancellationToken);
+ var result = await GenerateAsync(
+ source,
+ ZodSchemaValidationGeneratorTestOptions.Default.Compile(),
+ cancellationToken
+ );
var assembly = await Assert.That(result.CompilationResult.Assembly).IsNotNull();
var harness = assembly!.GetType("Testing.Harness")!;
@@ -83,34 +68,15 @@ public async Task Scalar_GivenZodSchema_InAdditionToHooks_RunsOnValidateToo(Canc
{
const string source = """
using ZodSharp;
- using ZodSharp.Core;
-
- namespace ZodSharp
- {
- [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)]
- public sealed class ZodSchemaAttribute : System.Attribute
- {
- public string? SchemaName { get; init; }
- }
- }
namespace Testing
{
- [ZodSchema]
- public static class EmailAddressSchema
- {
- public static ValidationResult Validate(EmailAddress value) =>
- ValidationResult.Success(value);
- }
-
- [Purview.ValueObjects.Serialization.Scalar]
+ [Scalar]
[ZodSchema]
public readonly partial record struct EmailAddress
{
public string Value { get; }
- private EmailAddress(string value) => Value = value;
-
static partial void OnValidate(string value)
{
if (value != "allowed")
@@ -139,7 +105,11 @@ public static bool OnValidateRejects()
}
""";
- var result = await GenerateAsync(source, ValueObjectsGeneratorTestOptions.Default.Compile(), cancellationToken);
+ var result = await GenerateAsync(
+ source,
+ ZodSchemaValidationGeneratorTestOptions.Default.Compile(),
+ cancellationToken
+ );
var assembly = await Assert.That(result.CompilationResult.Assembly).IsNotNull();
var harness = assembly!.GetType("Testing.Harness")!;
@@ -156,34 +126,15 @@ public async Task Scalar_GivenZodSchemaInsteadOfHooks_DoesNotRunOnValidate(Cance
{
const string source = """
using ZodSharp;
- using ZodSharp.Core;
-
- namespace ZodSharp
- {
- [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)]
- public sealed class ZodSchemaAttribute : System.Attribute
- {
- public string? SchemaName { get; init; }
- }
- }
namespace Testing
{
- [ZodSchema]
- public static class EmailAddressSchema
- {
- public static ValidationResult Validate(EmailAddress value) =>
- ValidationResult.Success(value);
- }
-
- [Purview.ValueObjects.Serialization.Scalar(ZodSchemaMode = Purview.ValueObjects.Serialization.ZodSchemaMode.InsteadOfHooks)]
+ [Scalar(ZodSchemaMode = Purview.ValueObjects.Serialization.ZodSchemaMode.InsteadOfHooks)]
[ZodSchema]
public readonly partial record struct EmailAddress
{
public string Value { get; }
- private EmailAddress(string value) => Value = value;
-
static partial void OnValidate(string value)
{
throw new System.ArgumentException("OnValidate must not run.", nameof(value));
@@ -198,7 +149,11 @@ public static bool CreateSkipsOnValidate() =>
}
""";
- var result = await GenerateAsync(source, ValueObjectsGeneratorTestOptions.Default.Compile(), cancellationToken);
+ var result = await GenerateAsync(
+ source,
+ ZodSchemaValidationGeneratorTestOptions.Default.Compile(),
+ cancellationToken
+ );
var assembly = await Assert.That(result.CompilationResult.Assembly).IsNotNull();
var harness = assembly!.GetType("Testing.Harness")!;
@@ -207,70 +162,4 @@ public static bool CreateSkipsOnValidate() =>
await Assert.That(skipped).IsTrue();
}
-
- [Test]
- public async Task Scalar_GivenZodSchemaWithCustomSchemaName_UsesThatSchemaClass(CancellationToken cancellationToken)
- {
- const string source = """
- using ZodSharp;
- using ZodSharp.Core;
-
- namespace ZodSharp
- {
- [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Struct)]
- public sealed class ZodSchemaAttribute : System.Attribute
- {
- public string? SchemaName { get; init; }
- }
- }
-
- namespace Testing
- {
- [ZodSchema(SchemaName = "EmailRules")]
- public static class EmailRules
- {
- public static ValidationResult Validate(EmailAddress value) =>
- value.Value.Contains('@', System.StringComparison.Ordinal)
- ? ValidationResult.Success(value)
- : ValidationResult.Failure(
- new ValidationError("invalid", "Invalid email.", [nameof(value)])
- );
- }
-
- [Purview.ValueObjects.Serialization.Scalar]
- [ZodSchema(SchemaName = "EmailRules")]
- public readonly partial record struct EmailAddress
- {
- public string Value { get; }
-
- private EmailAddress(string value) => Value = value;
- }
-
- public static class Harness
- {
- public static bool CreateRejectsInvalid()
- {
- try
- {
- EmailAddress.Create("not-an-email");
- return false;
- }
- catch (global::ZodSharp.Core.ZodException)
- {
- return true;
- }
- }
- }
- }
- """;
-
- var result = await GenerateAsync(source, ValueObjectsGeneratorTestOptions.Default.Compile(), cancellationToken);
-
- var assembly = await Assert.That(result.CompilationResult.Assembly).IsNotNull();
- var harness = assembly!.GetType("Testing.Harness")!;
-
- var rejects = (bool)harness.GetMethod("CreateRejectsInvalid")!.Invoke(null, null)!;
-
- await Assert.That(rejects).IsTrue();
- }
}
diff --git a/src/tests/SourceGenerator.UnitTests/SourceGenerator.UnitTests.csproj b/src/tests/SourceGenerator.UnitTests/SourceGenerator.UnitTests.csproj
index b8c7bf5..02631e4 100644
--- a/src/tests/SourceGenerator.UnitTests/SourceGenerator.UnitTests.csproj
+++ b/src/tests/SourceGenerator.UnitTests/SourceGenerator.UnitTests.csproj
@@ -6,6 +6,15 @@
+
+
+
+ $(NuGetPackageRoot)purview.zodsharp\$(PurviewZodSharpVersion)\analyzers\dotnet\cs\Purview.ZodSharp.SourceGenerators.dll
+
+
+