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 + + +