diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index f5e1608d..4409e70a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -25,7 +25,7 @@ jobs: secrets: inherit # The shared pipeline runs the C# suite only, so the TypeScript/Zod side of the cross-platform - # parity guarantee (docs/wiki/Guarantees-and-Limitations.md) would otherwise never run in CI. + # parity guarantee (docs/wiki/quality/Guarantees-and-Limitations.md) would otherwise never run in CI. # The vitest suite validates the JSON the C# cross-platform tests write, so the C# tests must run # first, in this same workspace — hence running them here rather than relying on the `build` job. cross-platform: @@ -288,7 +288,7 @@ jobs: # packages only - a Native AOT console, a Blazor WebAssembly app (trimmed and RunAOTCompilation) and a Native AOT # minimal API - and leak-scans every bin and publish folder for compiler-host assemblies. It then proves every # PackValidation ForbiddenContent rule rejects a package, against the packages the consumers used. - # `just smoke-packed-consumers` and `just verify-pack-rules` run the same locally. See docs/wiki/Release-Flow.md. + # `just smoke-packed-consumers` and `just verify-pack-rules` run the same locally. See docs/wiki/development/Release-Flow.md. packed-consumers: name: Packed consumers (Native AOT, Blazor WebAssembly, minimal API) runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index b9bff242..afc77128 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,6 +119,17 @@ Each package ships its own `README.md`, placed in the project's `Sdk/` folder (f Keep `purview-build.json`'s `PackValidation` requirements in sync with any packaging change. +## Documentation (`docs/wiki`) + +`docs/wiki` is the source of the [purview.dev](https://purview.dev) documentation for this project: the site's `github-path` sync reads the folder for both the `live` (latest stable tag) and `prerelease` (`main`) channels. + +- **One folder per topic area.** A page is published at `/docs/zodsharp///`, so the folder is part of its URL, and a link inside a page resolves relative to that page (`../concepts/Core-Concepts.md`). The tree, the seven sections and the conventions are in `docs/wiki/development/Contributing.md`. +- **Three pages stay at the wiki root.** `Getting-Started.md` is the site's landing page (`docs.rootPage`), and `Fluent-Schema-API.md` and `AspNetCore-Integration.md` are deep-linked by the site catalogue (`docsPage`), whose slug must be a single path segment and must exist in both documentation channels. +- **Adding a page is three edits** - the page in its folder, its `_Sidebar.md` entry inside the right section (path relative to the wiki root), and its `Home.md` row. +- **Renaming or moving a page changes a published URL.** Add the old slug to `docs.redirects` in the site repository's `src/src/data/projects/zodsharp.yml`; that is a cross-repository change. + +`src/tests/Repository.UnitTests/DocumentationWikiTests.cs` enforces that every page is listed exactly once in the sidebar and is reachable from `Home.md`, that both sets of links resolve to existing files, that every sidebar section has a matching `## ` heading in `Home.md`, that every folder's pages are all in the sidebar, and that every page opens with a single `# ` title. + ## Cross-platform fixtures Schemas and fixtures shared between TypeScript/Zod and C#/Purview.ZodSharp live under `src/ts/`. The fixture generator (`bun run generate-fixtures`) writes JSON fixtures and a `manifest.json` that the C# tests consume. Keep the TS `UserSchema` and the C# `CrossPlatformUserSchema` in sync. @@ -159,7 +170,7 @@ Commits must follow [Conventional Commits](https://www.conventionalcommits.org/) - Packed packages match `purview-build.json` `PackValidation` (including per-package `README.md` and `purview-logo-light.png`, and no `ForbiddenContent`: compiler-host assemblies outside `analyzers/`, generator or code-fix assemblies under `lib/`). After changing `PackValidation`, `just verify-pack-rules` proves every forbidden-content rule still rejects a package. - No build or publish output carries a compiler-host assembly (`Purview.SourceGeneratorFramework*`, `Microsoft.CodeAnalysis*`, `Purview.ZodSharp.SourceGenerators*`, `Purview.ZodSharp.CodeFixes*`): every publish recipe and CI publish job runs `scripts/assert-no-compiler-host-assemblies.ps1`, and `ReleaseGateTests` fails a workflow job that publishes without it. - Changes that affect what ships (packaging, `buildTransitive/`, package dependencies, the generator, trimming or AOT) also pass the gates that consume the real `.nupkg`: `just smoke-packed-generator` and `just smoke-packed-consumers` (Native AOT console, Blazor WebAssembly trimmed and AOT, Native AOT minimal API), plus `just wasm-size`, `just verify-trim ` and `just publish-aot-sample`. -- Before a release, run the pre-release checklist in `docs/wiki/Release-Flow.md` ("Release gates"): `just scrub`, `just pipeline-local-release ...`, then every gate above. +- Before a release, run the pre-release checklist in `docs/wiki/development/Release-Flow.md` ("Release gates"): `just scrub`, `just pipeline-local-release ...`, then every gate above. ## Installed skills (`.agents/skills/`) diff --git a/README.md b/README.md index b24ca06d..70a2d796 100644 --- a/README.md +++ b/README.md @@ -86,13 +86,13 @@ canonical paths (`Items[0].Name`, `Map[en]`, `Map["0"]`), typed bounds and local | Package | What it adds | | --- | --- | -| `Purview.ZodSharp.Extensions.Validation` | The Microsoft.Extensions.Validation bridge (`AddZodSharpValidation()`); WebAssembly-safe. See [Microsoft.Extensions.Validation](docs/wiki/Microsoft-Extensions-Validation.md) | -| `Purview.ZodSharp.AspNetCore.OpenApi` | OpenAPI transformers that describe ZodSharp's constraints and validation problem responses (`options.AddZodSharp()`). See [OpenAPI Integration](docs/wiki/OpenAPI-Integration.md) | -| `Purview.ZodSharp.Blazor` | Blazor Server and WebAssembly form validation. See [Blazor Integration](docs/wiki/Blazor-Integration.md) | +| `Purview.ZodSharp.Extensions.Validation` | The Microsoft.Extensions.Validation bridge (`AddZodSharpValidation()`); WebAssembly-safe. See [Microsoft.Extensions.Validation](docs/wiki/hosting/Microsoft-Extensions-Validation.md) | +| `Purview.ZodSharp.AspNetCore.OpenApi` | OpenAPI transformers that describe ZodSharp's constraints and validation problem responses (`options.AddZodSharp()`). See [OpenAPI Integration](docs/wiki/hosting/OpenAPI-Integration.md) | +| `Purview.ZodSharp.Blazor` | Blazor Server and WebAssembly form validation. See [Blazor Integration](docs/wiki/hosting/Blazor-Integration.md) | `Purview.ZodSharp.AspNetCore` uses the bridge on `net11.0` to produce full-fidelity ProblemDetails from the built-in pipeline ([ASP.NET Core Integration](docs/wiki/AspNetCore-Integration.md)), and messages are localized through -`IZodMessageFormatter` ([Localization](docs/wiki/Localization.md)). +`IZodMessageFormatter` ([Localization](docs/wiki/hosting/Localization.md)). ## Usage Examples @@ -275,7 +275,7 @@ public readonly struct MinLengthRule : IValidationRule } ``` -Every rule exposes its reported code and message template as public `ErrorCode`/`MessageFormat` constants so tests can assert against the rule instead of duplicating literals (`ZODSGEN042` enforces this convention — see the [Validation Rules Reference](docs/wiki/Validation-Rules-Reference.md) for the catalogue of built-in rules, [Custom Rules](docs/wiki/Custom-Rules.md) for the rule contract and [Value Objects Integration](docs/wiki/Value-Objects-Integration.md) for scalar value objects). +Every rule exposes its reported code and message template as public `ErrorCode`/`MessageFormat` constants so tests can assert against the rule instead of duplicating literals (`ZODSGEN042` enforces this convention — see the [Validation Rules Reference](docs/wiki/schemas/Validation-Rules-Reference.md) for the catalogue of built-in rules, [Custom Rules](docs/wiki/schemas/Custom-Rules.md) for the rule contract and [Value Objects Integration](docs/wiki/schemas/Value-Objects-Integration.md) for scalar value objects). #### 3. Compiled Validators @@ -575,7 +575,7 @@ var either = UserSchema.ApplyOr(user, u => u.Age < 18, "Must be an adult or a mi - Zero-reflection, zero-allocation validators - Value-first composition methods (`.ApplyAnd()`, `.ApplyOr()`, `.ApplyRefine()`) plus instance schema-composing composition (`.Refine()`, `.SuperRefine()`, `.Pipe()`, `.Catch()`, `.Prefault()`, `.Default()`) - Supports classes, structs, and records -- Validates `Purview.ValueObjects` `[Scalar]` types as a unit, adapting a rule written against the underlying value automatically ([Value Objects Integration](docs/wiki/Value-Objects-Integration.md)) +- Validates `Purview.ValueObjects` `[Scalar]` types as a unit, adapting a rule written against the underlying value automatically ([Value Objects Integration](docs/wiki/schemas/Value-Objects-Integration.md)) #### Supported DataAnnotations size validators diff --git a/docs/wiki/AspNetCore-Integration.md b/docs/wiki/AspNetCore-Integration.md index d821b067..9d40663c 100644 --- a/docs/wiki/AspNetCore-Integration.md +++ b/docs/wiki/AspNetCore-Integration.md @@ -196,7 +196,7 @@ The contract: - **Keys.** `errors` keys and `issues[].path` are `ValidationError.FullPath.ToDottedString()`: members joined with `.`, indexes as `[0]`, and keys as `[en]` or, when ambiguous, quoted (`Map["0"]`, `Map["a.b"]`). The root is `""`. - See [Core Concepts](Core-Concepts.md#paths). By default member names are the C# names, matching + See [Core Concepts](concepts/Core-Concepts.md#paths). By default member names are the C# names, matching Microsoft.Extensions.Validation and MVC model state. `PathNaming = ZodPathNaming.Json` applies the minimal-API `JsonOptions` naming policy. - **`code` and `category`.** `code` is ZodSharp's specific identity (`invalid_range`). `category` is the Zod issue @@ -210,11 +210,11 @@ The contract: issue's `path` already names the member. `displayName` is kept. Set `IncludeDiagnosticParameters = true` to send them. - **`traceId`.** It is present on every path: `Activity.Current?.Id`, else `HttpContext.TraceIdentifier`. - **No input values.** Messages and parameters never contain the input unless `ZodErrorOptions.IncludeReceivedValues` - is enabled. See [Core Concepts](Core-Concepts.md#pii). + is enabled. See [Core Concepts](concepts/Core-Concepts.md#pii). - **Messages.** They come from the registered `IZodMessageFormatter` in `CultureInfo.CurrentUICulture`. See - [Localization](Localization.md). + [Localization](hosting/Localization.md). - **OpenAPI.** The body is described by the `ZodValidationProblemDetails` and `ZodValidationIssue` component schemas - (see [OpenAPI Integration](OpenAPI-Integration.md)). + (see [OpenAPI Integration](hosting/OpenAPI-Integration.md)). ## Minimal API validation @@ -317,9 +317,9 @@ Notes: - **Localization**: register `AddLocalization()` and an `IZodMessageFormatter`, for example `new StringLocalizerZodMessageFormatter(factory, sharedResourceType: typeof(ValidationMessages))`. Messages follow `Accept-Language` (with `UseRequestLocalization`); codes and paths are unchanged. See - [Localization](Localization.md). + [Localization](hosting/Localization.md). - **The bridge**: resolver placement, `ZodReport` and non-HTTP validation are covered in - [Microsoft.Extensions.Validation](Microsoft-Extensions-Validation.md). + [Microsoft.Extensions.Validation](hosting/Microsoft-Extensions-Validation.md). - **Unions**: a union parameter is claimed by the runtime parameter resolver, which the application's `AddValidation()` call adds. The active case is reported under `Value` (`Value.Email`). - **Double validation**: `WithZodSharpValidation()` on an endpoint the pipeline validates skips the parameters @@ -327,7 +327,7 @@ Notes: it validates as usual. - **OpenAPI**: the `Purview.ZodSharp.AspNetCore.OpenApi` operation transformer adds the validation problem response for endpoints with a ZodSharp-known parameter. No endpoint metadata is added by the pipeline path. See - [OpenAPI Integration](OpenAPI-Integration.md). + [OpenAPI Integration](hosting/OpenAPI-Integration.md). | | `WithZodSharpValidation()` filter | Built-in pipeline | |---|---|---| @@ -645,7 +645,7 @@ builder.Services.AddZodSharp(options => assembly sources, which auto-discovers source-generated `[assembly: ZodSchemaGenerated(typeof(...))]` registrations. -Assembly scanning is not trim-safe. In a trimmed or Native AOT host, use the generated `AddZodSharp{Assembly}Schemas()` (see [Source Generator](Source-Generator.md)). +Assembly scanning is not trim-safe. In a trimmed or Native AOT host, use the generated `AddZodSharp{Assembly}Schemas()` (see [Source Generator](generator/Source-Generator.md)). `ZodSchemaFactoryOptions`: @@ -683,8 +683,8 @@ builder.Services.AddZodSharpLoadedAssemblies(); | `AddZodSharpFactory(configure)` | core `Purview.ZodSharp` | singleton `IZodSchemaFactory` | Any .NET host where you want manual control — register validators yourself in the `configure` callback. | | `AddZodSharp{Assembly}Schemas()` | generated into each assembly with `[ZodSchema]` types | a trim-safe `ZodSchemaRegistration` naming every generated validator | Trimmed or Native AOT hosts, and anywhere assembly scanning is unwanted. | | `AddZodSharpProblemDetails(...)` | `Purview.ZodSharp.AspNetCore` | `ZodExceptionHandler`, the ProblemDetails writer and services, and the .NET 11 pipeline enrichment — does **not** register the factory | Mapping thrown `ZodException`s to `ProblemDetails`; pair it with `AddZodSharp` when you also need DI validator resolution. | -| `AddZodSharpValidation(...)` | `Purview.ZodSharp.Extensions.Validation` (`net11.0`) | the Microsoft.Extensions.Validation resolver | Validating with the .NET 11 pipeline; see [Microsoft.Extensions.Validation](Microsoft-Extensions-Validation.md). | +| `AddZodSharpValidation(...)` | `Purview.ZodSharp.Extensions.Validation` (`net11.0`) | the Microsoft.Extensions.Validation resolver | Validating with the .NET 11 pipeline; see [Microsoft.Extensions.Validation](hosting/Microsoft-Extensions-Validation.md). | | `AddZodSchemaOptionsValidator(...)` | core `Purview.ZodSharp` | singleton `IValidateOptions` | Validating options objects; requires a factory registered first via `AddZodSharp` or `AddZodSharpFactory`. Validates every options name; on .NET 11 `ValidateOnStart()` runs its asynchronous rules too. | | `OptionsBuilder.BindAndValidateWithZod(section)` | `Purview.ZodSharp.AspNetCore` | binds a configuration section and adds the ZodSharp options validator | Options whose failures should name the configuration key (`Smtp:Servers:0:Host`). Uses the reflection-based binder; under Native AOT call `BindConfiguration` and `AddZodSchemaValidator` yourself. | -See [Dependency Injection](Dependency-Injection.md) for the underlying factory and options-validation wiring. +See [Dependency Injection](hosting/Dependency-Injection.md) for the underlying factory and options-validation wiring. diff --git a/docs/wiki/Contributing.md b/docs/wiki/Contributing.md deleted file mode 100644 index 82d7284c..00000000 --- a/docs/wiki/Contributing.md +++ /dev/null @@ -1,68 +0,0 @@ -# Contributing - -Contributions are welcome. Please open an issue or pull request against [purview-dev/zodsharp](https://github.com/purview-dev/zodsharp). - -## Repository layout - -``` -src/ - src/ - ZodSharp/ Core validation library + JSON Schema export (Z.ToJsonSchema) - SourceGenerators/ Compile-time [ZodSchema] generator (netstandard2.0, Roslyn) - SystemTextJson/ System.Text.Json integration + JSON Schema import - NewtonsoftJson/ Newtonsoft.Json integration + JSON Schema import - AspNetCore/ ASP.NET Core ProblemDetails integration - Examples.CLI/ Usage examples - Benchmarks/ BenchmarkDotNet performance suite - tests/ - *.UnitTests/ TUnit test projects -src/ts/ TypeScript (Zod) schema + fixture generation -tests/ts/ Vitest cross-platform tests -docs/wiki/ This documentation suite -``` - -## Commands - -```bash -just build # dotnet build src/ZodSharp.slnx -c Debug -just test # dotnet test src/ZodSharp.slnx -c Debug --treenode-filter "/*/*/*/*" -just lint-check # dotnet csharpier check . -just lint-fix # dotnet csharpier format . -just pack # dotnet pack src/ZodSharp.slnx -c Debug -o artifacts -just perf-tests # dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release -``` - -TypeScript tooling uses Bun: - -```bash -bun install -bun run test # vitest run (cross-platform TS tests) -bun run generate-fixtures # regenerates src/ts/fixtures/*.json from Zod -``` - -## Testing bar - -- Framework: **TUnit**. -- `[Test]` methods take a `CancellationToken cancellationToken` parameter where relevant. -- Use `// Arrange`, `// Act`, `// Assert` comments. -- Use meaningful, descriptive names (`Action_GivenCondition_ExpectedResult`). -- Treat work as incomplete until the relevant tests pass. - -For source generator tests, use the `Purview.SourceGeneratorFramework.Testing.TUnit` base classes (`TUnitSourceGeneratorTestBase`, `TUnitDiagnosticAnalyzerTestBase`) and assert with `CodeQuery`; test incrementally, not just generated text. - -## Documentation - -This wiki lives in `docs/wiki/`. The site build (purview-dev Astro/Starlight) pulls these files via `github-path` sync and rewrites relative `.md` links into site routes. - -Conventions: - -- `_Sidebar.md` declares page order (parsed by the sync, never rendered). -- Every page starts with a single `# ` heading (the page title) followed by a one-paragraph description. -- Link to other pages with relative `.md` links: `[Getting Started](Getting-Started.md)`. -- GitHub alert blockquotes (`> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!CAUTION]`, `> [!IMPORTANT]`) are converted to Starlight asides. -- Relative repository-path links (e.g. `src/src/ZodSharp/Z.cs`) are rewritten to GitHub blob URLs automatically. -- Keep the `CrossPlatformUserSchema` (C#) and `UserSchema` (TypeScript) in sync when either changes. - -## Formatting - -Formatting is enforced with CSharpier. `.editorconfig` at the repo root defines style (tabs for code, 2-space for XML/JSON/YAML/markdown). \ No newline at end of file diff --git a/docs/wiki/Fluent-Schema-API.md b/docs/wiki/Fluent-Schema-API.md index 02cb6a7d..94749b05 100644 --- a/docs/wiki/Fluent-Schema-API.md +++ b/docs/wiki/Fluent-Schema-API.md @@ -37,26 +37,26 @@ Each schema type has its own page: -- [String Validation](String-Validation.md) — `ZodString`. -- [Number Validation](Number-Validation.md) — `ZodNumber`. -- [Object Validation](Object-Validation.md) — `ZodObject`, `ZodObjectBuilder`. -- [Arrays and Other Schemas](Arrays-and-Other-Schemas.md) — `ZodArray`, `ZodBoolean`, `ZodNull`, `ZodEnum`, `ZodNativeEnum`, `ZodLiteral`, `ZodRecord`, `ZodTuple`, `ZodLazy`. -- [Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md) — `ZodUnion`, `ZodTypedUnion`, `ZodDiscriminatedUnion`. -- [Composition and Transforms](Composition-and-Transforms.md) — `ZodTransform`, `ZodRefinement`, `ZodSuperRefinement`, `ZodPipe`, `ZodCatch`, `ZodDefault`, `ZodPrefault`, `ZodIntersection`. -- [Compiled Validators and Caching](Compiled-Validators-and-Caching.md) — `CompiledValidator`, `SchemaCache`. +- [String Validation](schemas/String-Validation.md) — `ZodString`. +- [Number Validation](schemas/Number-Validation.md) — `ZodNumber`. +- [Object Validation](schemas/Object-Validation.md) — `ZodObject`, `ZodObjectBuilder`. +- [Arrays and Other Schemas](schemas/Arrays-and-Other-Schemas.md) — `ZodArray`, `ZodBoolean`, `ZodNull`, `ZodEnum`, `ZodNativeEnum`, `ZodLiteral`, `ZodRecord`, `ZodTuple`, `ZodLazy`. +- [Unions and Discriminated Unions](schemas/Unions-and-Discriminated-Unions.md) — `ZodUnion`, `ZodTypedUnion`, `ZodDiscriminatedUnion`. +- [Composition and Transforms](concepts/Composition-and-Transforms.md) — `ZodTransform`, `ZodRefinement`, `ZodSuperRefinement`, `ZodPipe`, `ZodCatch`, `ZodDefault`, `ZodPrefault`, `ZodIntersection`. +- [Compiled Validators and Caching](schemas/Compiled-Validators-and-Caching.md) — `CompiledValidator`, `SchemaCache`. ## The interfaces - `IZodSchema` — `Validate` / `ValidateAsync`; `IZodSchema` is the convenience form where input equals output. -- `IZodSchemaValidator` (marker) and `IZodSchemaValidator` — the DI-facing adapter surface (see [Dependency Injection](Dependency-Injection.md)). +- `IZodSchemaValidator` (marker) and `IZodSchemaValidator` — the DI-facing adapter surface (see [Dependency Injection](hosting/Dependency-Injection.md)). - `IValidationRule` — the rule contract implemented by every struct rule. `ZodType.AddRule(rule)` and `Rule(rule)` are public, so custom rules can be attached to any schema, and a fluent extension method can give them a first-class method like the built-in rules. Every rule exposes its reported code and message template as public `const string ErrorCode` / `MessageFormat` constants (enforced by `ZODSGEN042`). `ZodString` also exposes `IsValidSpan`/`ValidateSpan` for span-based string validation, and string rules that implement `IStringValidationRule` participate in the span path. - `IZodRule` — implemented by rules that own their error identity (`Code`/`Origin`). When a mapped rule implements it, the generator prefers the rule's values over the attribute's, so one attribute can produce a per-member error code. - `IStringValidationRule` — the span-based counterpart of `IValidationRule`. -The shipped `ZodSharp.Rules` namespace provides the built-in rules (for example `EmailRule`, `MinLengthRule`, `NonSentinelRule`); each is a `readonly record struct` usable standalone or through the fluent API. Every rule has a fluent method on its schema — see [Validation Rules Reference](Validation-Rules-Reference.md) for the catalogue. +The shipped `ZodSharp.Rules` namespace provides the built-in rules (for example `EmailRule`, `MinLengthRule`, `NonSentinelRule`); each is a `readonly record struct` usable standalone or through the fluent API. Every rule has a fluent method on its schema — see [Validation Rules Reference](schemas/Validation-Rules-Reference.md) for the catalogue. -See [Custom Rules](Custom-Rules.md) for defining, attaching, and mapping rules (including generic rules, the `ErrorCode`/`MessageFormat` constants convention, and [fluent extension methods](Custom-Rules.md#extending-the-fluent-interface)). +See [Custom Rules](schemas/Custom-Rules.md) for defining, attaching, and mapping rules (including generic rules, the `ErrorCode`/`MessageFormat` constants convention, and [fluent extension methods](schemas/Custom-Rules.md#extending-the-fluent-interface)). ## Convenience composition on any schema -Because composition is implemented on the base `ZodType`, every schema can chain `.Describe(...)`, `.Transform(...)`, `.Refine(...)`, `.SuperRefine(...)`, `.Pipe(...)`, `.Catch(...)`, `.Prefault(...)`, `.Default(...)`, `.And(...)`, and `.Or(...)`. See [Composition and Transforms](Composition-and-Transforms.md). \ No newline at end of file +Because composition is implemented on the base `ZodType`, every schema can chain `.Describe(...)`, `.Transform(...)`, `.Refine(...)`, `.SuperRefine(...)`, `.Pipe(...)`, `.Catch(...)`, `.Prefault(...)`, `.Default(...)`, `.And(...)`, and `.Or(...)`. See [Composition and Transforms](concepts/Composition-and-Transforms.md). \ No newline at end of file diff --git a/docs/wiki/Getting-Started.md b/docs/wiki/Getting-Started.md index 50a66e54..c16c8753 100644 --- a/docs/wiki/Getting-Started.md +++ b/docs/wiki/Getting-Started.md @@ -104,7 +104,7 @@ var result = UserSchema.Validate(user); var validated = UserSchema.Parse(user); // throws on failure ``` -See the [Source Generator](Source-Generator.md) and [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) pages for the full feature set. +See the [Source Generator](generator/Source-Generator.md) and [Source Generator DataAnnotations](generator/Source-Generator-DataAnnotations.md) pages for the full feature set. ## JSON integration @@ -123,13 +123,16 @@ var jsonSchema = Z.ToJsonSchema(userSchema, new ToJsonSchemaOptions { Title = "U var imported = Z.FromJsonSchema(jsonSchemaString); ``` -See [System.Text.Json Integration](SystemTextJson-Integration.md), [Newtonsoft.Json Integration](NewtonsoftJson-Integration.md), [JSON Schema Export](JsonSchema-Export.md), and [JSON Schema Import](JsonSchema-Import.md). +See [System.Text.Json Integration](serialization/SystemTextJson-Integration.md), [Newtonsoft.Json Integration](serialization/NewtonsoftJson-Integration.md), [JSON Schema Export](serialization/JsonSchema-Export.md), and [JSON Schema Import](serialization/JsonSchema-Import.md). -## Next pages +## Where to go next -- [Core Concepts](Core-Concepts.md) -- [Fluent Schema API](Fluent-Schema-API.md) -- [Source Generator](Source-Generator.md) -- [Dependency Injection](Dependency-Injection.md) -- [Cross-Platform Interop](Cross-Platform-Interop.md) -- [Performance](Performance.md) \ No newline at end of file +This page is the entry point of the wiki, so the rest of it is grouped by what you are trying to do. Each group below is a section of the sidebar, in reading order; [Home](Home.md) lists every page in all seven sections. + +- **Learn the model** — [Core Concepts](concepts/Core-Concepts.md) explains the pipeline, the result and the error model; [Fluent Schema API](Fluent-Schema-API.md) is the surface every schema type shares. +- **Pick a schema or a rule** — [Validation Rules Reference](schemas/Validation-Rules-Reference.md) lists every built-in rule, and each schema type has its own page: [String Validation](schemas/String-Validation.md), [Number Validation](schemas/Number-Validation.md), [Object Validation](schemas/Object-Validation.md), [Arrays and Other Schemas](schemas/Arrays-and-Other-Schemas.md), [Unions and Discriminated Unions](schemas/Unions-and-Discriminated-Unions.md), [Composition and Transforms](concepts/Composition-and-Transforms.md). Write your own in [Custom Rules](schemas/Custom-Rules.md). +- **Generate validators** — [Source Generator](generator/Source-Generator.md), then [Source Generator DataAnnotations](generator/Source-Generator-DataAnnotations.md), with [Source Generator Diagnostics](generator/Source-Generator-Diagnostics.md) for the codes the build reports. +- **Move data across a boundary** — [JSON Schema Export](serialization/JsonSchema-Export.md) and [JSON Schema Import](serialization/JsonSchema-Import.md) for interoperability, [System.Text.Json Integration](serialization/SystemTextJson-Integration.md) and [Newtonsoft.Json Integration](serialization/NewtonsoftJson-Integration.md) for deserialize-and-validate. +- **Host it** — [ASP.NET Core Integration](AspNetCore-Integration.md), [Blazor Integration](hosting/Blazor-Integration.md), [HttpClient Integration](hosting/Http-Integration.md), [Dependency Injection](hosting/Dependency-Injection.md), and [Localization](hosting/Localization.md) for messages. On .NET 11, [Microsoft.Extensions.Validation](hosting/Microsoft-Extensions-Validation.md) and [OpenAPI Integration](hosting/OpenAPI-Integration.md) cover the platform pipeline. +- **Ship it** — [Guarantees and Limitations](quality/Guarantees-and-Limitations.md) states what is promised, [Performance](quality/Performance.md) what it costs, and [Trimming and Native AOT](quality/Trimming-and-AOT.md) what the publish gates check. [Cross-Platform Interop](quality/Cross-Platform-Interop.md) covers the Zod parity fixtures. +- **Work on it** — [Contributing](development/Contributing.md) and [Release Flow](development/Release-Flow.md). \ No newline at end of file diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 9339fad5..c09e65ad 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -2,71 +2,116 @@ Purview.ZodSharp is a high-performance schema validation library for C#, ported from TypeScript [Zod](https://github.com/colinhacks/zod). It uses struct-based rules and `Span` to minimise allocations, ships a fluent API that mirrors Zod, exports and imports JSON Schema, and includes a compile-time source generator for maximum performance. -This wiki is the project documentation hub for the core API, source generator, JSON integration packages, and the cross-platform TypeScript tooling. Packages are published under the `Purview.*` package IDs. - -## Start here - -- [Getting Started](Getting-Started.md) -- [Core Concepts](Core-Concepts.md) -- [Fluent Schema API](Fluent-Schema-API.md) -- [Source Generator](Source-Generator.md) -- [Guarantees and Limitations](Guarantees-and-Limitations.md) -- [Performance](Performance.md) -- [Contributing](Contributing.md) - -## Core validation - -- [String Validation](String-Validation.md) -- [Number Validation](Number-Validation.md) -- [Object Validation](Object-Validation.md) -- [Arrays and Other Schemas](Arrays-and-Other-Schemas.md) -- [Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md) -- [Composition and Transforms](Composition-and-Transforms.md) -- [Custom Rules](Custom-Rules.md) -- [Validation Rules Reference](Validation-Rules-Reference.md) -- [Value Objects Integration](Value-Objects-Integration.md) -- [Compiled Validators and Caching](Compiled-Validators-and-Caching.md) -- [Dependency Injection](Dependency-Injection.md) - -## JSON integration - -- [JSON Schema Export](JsonSchema-Export.md) -- [JSON Schema Import](JsonSchema-Import.md) -- [System.Text.Json Integration](SystemTextJson-Integration.md) -- [Newtonsoft.Json Integration](NewtonsoftJson-Integration.md) -- [ASP.NET Core Integration](AspNetCore-Integration.md) - -## .NET 11 validation - -- [Microsoft.Extensions.Validation](Microsoft-Extensions-Validation.md): the bridge that makes minimal APIs and Blazor validate with ZodSharp -- [ASP.NET Core Integration](AspNetCore-Integration.md): the ProblemDetails contract and both minimal-API modes -- [OpenAPI Integration](OpenAPI-Integration.md): describing ZodSharp constraints and the error contract in OpenAPI 3.1 and 3.2 -- [Blazor Integration](Blazor-Integration.md): forms on Blazor Server, WebAssembly and Auto -- [Localization](Localization.md): message formatting, `IStringLocalizer` lookup, culture and PII +This wiki is the documentation hub for the core API, the source generator, the JSON and hosting integrations, and the cross-platform TypeScript tooling. Packages are published under the `Purview.*` package IDs. + +## How this wiki is organised + +The wiki is one folder per topic area, and seven sections, in reading order. The sidebar carries the same sections in the same order, and `_Sidebar.md` is what the published documentation site reads for page order - a page that is not listed there has no place in the sidebar, so new pages are added to it as part of adding the page. A page's folder is part of its published URL (`schemas/Custom-Rules.md` is `/docs/zodsharp/schemas/custom-rules/`), so a page moves folder only when it changes topic. + +| Section | Where it lives | +| --- | --- | +| Get started | the wiki root and `concepts/` | +| Validation reference | `schemas/`, plus `concepts/Composition-and-Transforms.md` | +| Source generator | `generator/` | +| JSON and serialization | `serialization/` | +| ASP.NET Core, Blazor and .NET 11 | `hosting/`, plus the root-level `AspNetCore-Integration.md` | +| Guarantees, performance and deployment | `quality/` | +| Contributing and releases | `development/` | + +Three pages stay at the wiki root: `Getting-Started.md`, which the site publishes as the documentation landing page, and `Fluent-Schema-API.md` and `AspNetCore-Integration.md`, which the site's project catalogue deep-links by slug. A deep link is a single path segment, so those two keep root-level URLs; see [Contributing](development/Contributing.md). + +## Get started + +Read these first; they are the vocabulary the reference pages assume. `Getting-Started.md` and `Fluent-Schema-API.md` sit at the wiki root, `Core-Concepts.md` in `concepts/`. + +| Page | What it answers | +| --- | --- | +| [Getting Started](Getting-Started.md) | Which packages to install, and a first schema, object, generated validator and JSON round-trip. | +| [Core Concepts](concepts/Core-Concepts.md) | The two-phase pipeline, `ValidationResult`, the error model (codes, categories, paths, bounds, parameters), and coercion. | +| [Fluent Schema API](Fluent-Schema-API.md) | The `Z` factory, and the methods every schema type exposes. | + +## Validation reference + +`schemas/` holds one page per schema family and the rule catalogue; `concepts/Composition-and-Transforms.md` covers composition. Start with the page for the type you are validating; the rules themselves are all listed in the reference. + +| Page | What it answers | +| --- | --- | +| [String Validation](schemas/String-Validation.md) | `ZodString`: length bounds, formats, regex, trimming and casing. | +| [Number Validation](schemas/Number-Validation.md) | `ZodNumber` and `ZodBigInt`: bounds, integer/safe/finite checks, multiples and sign. | +| [Object Validation](schemas/Object-Validation.md) | `ZodObject` and `ZodObjectBuilder`: fields, optional/strict/catchall, dictionary input. | +| [Arrays and Other Schemas](schemas/Arrays-and-Other-Schemas.md) | Arrays, tuples, records, maps, literals, enums and dates. | +| [Unions and Discriminated Unions](schemas/Unions-and-Discriminated-Unions.md) | `Z.Union`, discriminated unions, `Z.NativeUnion` and exhaustive matching. | +| [Composition and Transforms](concepts/Composition-and-Transforms.md) | `And`/`Or`/`Refine`/`Transform`/`Coerce`, and the value-first `Apply*` methods. | +| [Validation Rules Reference](schemas/Validation-Rules-Reference.md) | Every built-in rule, with its error code, message template and parameters. | +| [Custom Rules](schemas/Custom-Rules.md) | Writing a rule, exposing it as an attribute, extending the fluent interface, scalar value objects. | +| [Value Objects Integration](schemas/Value-Objects-Integration.md) | Validating a `Purview.ValueObjects` `[Scalar]` as a unit, with an empty failure path. | +| [Compiled Validators and Caching](schemas/Compiled-Validators-and-Caching.md) | Cached delegates for hot paths. | ## Source generator -- [Source Generator](Source-Generator.md) -- [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) -- [Source Generator Diagnostics](Source-Generator-Diagnostics.md) +`generator/` - `[ZodSchema]` turns a class, struct or record into a static, zero-allocation validator at compile time. Read the overview first; the other two pages are references you will come back to. + +| Page | What it answers | +| --- | --- | +| [Source Generator](generator/Source-Generator.md) | What the generator emits (`Validate`/`Parse`, `Paths`, `JsonNames`, trim-safe registration, the async graph), its options and hooks. | +| [Source Generator DataAnnotations](generator/Source-Generator-DataAnnotations.md) | Which `System.ComponentModel.DataAnnotations` attributes map to which rules. | +| [Source Generator Diagnostics](generator/Source-Generator-Diagnostics.md) | Every `ZODSGEN` diagnostic: severity, cause and fix. | + +## JSON and serialization + +`serialization/` - JSON Schema is the interchange format with TypeScript/Zod; the two library packages add deserialize-and-validate on top of it. Export lives in the core package, import in whichever JSON package you reference. + +| Page | What it answers | +| --- | --- | +| [JSON Schema Export](serialization/JsonSchema-Export.md) | `Z.ToJsonSchema`, `ToJsonSchemaOptions`, dictionaries, generated schemas and the temporal wire forms. | +| [JSON Schema Import](serialization/JsonSchema-Import.md) | `Z.FromJsonSchema`, and which package provides it. | +| [System.Text.Json Integration](serialization/SystemTextJson-Integration.md) | Deserialize-and-validate, validating converters, generated serialization contexts. | +| [Newtonsoft.Json Integration](serialization/NewtonsoftJson-Integration.md) | The same surface for Newtonsoft.Json. | + +## ASP.NET Core, Blazor and .NET 11 + +`hosting/`, plus the root-level `AspNetCore-Integration.md` - everything that plugs ZodSharp into a host. On `net11.0` the platform's own validation runs through ZodSharp, so minimal APIs and Blazor report the same error contract as direct calls. + +| Page | What it answers | +| --- | --- | +| [ASP.NET Core Integration](AspNetCore-Integration.md) | ProblemDetails, minimal-API validation in both modes, `IZodSchemaFactory` registration. | +| [Microsoft.Extensions.Validation](hosting/Microsoft-Extensions-Validation.md) | The .NET 11 bridge that makes minimal APIs and Blazor validate with ZodSharp. | +| [OpenAPI Integration](hosting/OpenAPI-Integration.md) | Transformers that describe ZodSharp's constraints and the error contract in OpenAPI 3.1 and 3.2. | +| [Blazor Integration](hosting/Blazor-Integration.md) | `ZodValidator`, message components, server errors onto fields, static-SSR client rules. | +| [HttpClient Integration](hosting/Http-Integration.md) | Validated request and response bodies, and reading validation problems as ZodSharp errors. | +| [Dependency Injection](hosting/Dependency-Injection.md) | Resolving validators through `IZodSchemaFactory`, and `IValidateOptions`. | +| [Localization](hosting/Localization.md) | Message formatting, `IStringLocalizer` lookup, culture and PII. | + +## Guarantees, performance and deployment + +`quality/` - the limits of the performance claims, and what the publish gates prove before a package ships. + +| Page | What it answers | +| --- | --- | +| [Guarantees and Limitations](quality/Guarantees-and-Limitations.md) | What is guaranteed (allocation-free valid paths, trim safety) and what is not. | +| [Performance](quality/Performance.md) | The BenchmarkDotNet suite and the committed measurements. | +| [Trimming and Native AOT](quality/Trimming-and-AOT.md) | `IsTrimmable`/`IsAotCompatible`, and the rooted and consumer publish checks. | +| [Cross-Platform Interop](quality/Cross-Platform-Interop.md) | The shared TypeScript/Zod fixture pipeline that pins C# behaviour to Zod. | + +## Contributing and releases -## Cross-platform and workflow +`development/` - repository conventions (including how this wiki is maintained) and the release flow. -- [Cross-Platform Interop](Cross-Platform-Interop.md) -- [Performance](Performance.md) -- [Release Flow](Release-Flow.md) -- [Contributing](Contributing.md) +| Page | What it answers | +| --- | --- | +| [Contributing](development/Contributing.md) | Repository layout, commands, the testing bar, and how this wiki is maintained. | +| [Release Flow](development/Release-Flow.md) | The pipeline, its gates and the pre-release checklist. | ## Feature highlights - **Zero-allocation validation** — validation rules are `readonly record struct`s and hot paths use `Span`; every valid input path validates without allocating. - **Fluent API** — `Z.String().Min(3).Max(50).Email()`, composable objects, arrays, unions, tuples, records, discriminators, and more. - **Native C# 15 unions (.NET 11+)** — `Z.NativeUnion` returns an allocation-free native union for reference-type cases, with exhaustive pattern matching; `Z.Union` stays the zero-allocation choice for value-type cases. The analyzer reports `ZODSGEN041` and offers a code fix. -- **Structured issues** — failures carry a machine-readable `Code`, a Zod-compatible `Category`, a typed `FullPath` (rendered `Items[0].Name`), `Origin`, typed `Lower`/`Upper` bounds, `Parameters` and the `MessageFormat` template, in addition to a human message. See [Core Concepts](Core-Concepts.md#error-fields) and [Localization](Localization.md). +- **Structured issues** — failures carry a machine-readable `Code`, a Zod-compatible `Category`, a typed `FullPath` (rendered `Items[0].Name`), `Origin`, typed `Lower`/`Upper` bounds, `Parameters` and the `MessageFormat` template, in addition to a human message. See [Core Concepts](concepts/Core-Concepts.md#error-fields) and [Localization](hosting/Localization.md). - **Test-friendly rule identity** — every rule exposes its code and message template as public `ErrorCode`/`MessageFormat` constants (the analyzer reports `ZODSGEN042` when one is missing), so tests assert against the rule instead of duplicating literals. `NonSentinelRule` rejects the ORM sentinel values (`Guid.Empty`, `DateTime.MinValue`/`MaxValue`, and more). - **JSON Schema interoperability** — export via `Z.ToJsonSchema` (core package) and import via `Z.FromJsonSchema` (in either JSON integration package), enabling cross-language reuse with TypeScript/Zod. - **Compile-time source generation** — the `[ZodSchema]` attribute turns a class, struct, or record into a zero-allocation static validator, honouring DataAnnotations attributes such as `[Required]`, `[Length]`, `[Range]`, and `[EmailAddress]`. -- **Value object scalars** — a `Purview.ValueObjects` `[Scalar]` can be validated as a unit with `[ZodSchema]`; a rule written against the scalar's underlying value is adapted automatically. See [Value Objects Integration](Value-Objects-Integration.md). +- **Value object scalars** — a `Purview.ValueObjects` `[Scalar]` can be validated as a unit with `[ZodSchema]`; a rule written against the scalar's underlying value is adapted automatically. See [Value Objects Integration](schemas/Value-Objects-Integration.md). - **Integration packages** — `Purview.ZodSharp.SystemTextJson`, `Purview.ZodSharp.NewtonsoftJson`, `Purview.ZodSharp.Http` (`HttpClient`, WebAssembly-safe), and `Purview.ZodSharp.AspNetCore` (ProblemDetails); on .NET 11, `Purview.ZodSharp.Extensions.Validation` (minimal APIs and Blazor through Microsoft.Extensions.Validation), `Purview.ZodSharp.AspNetCore.OpenApi` (OpenAPI transformers) and `Purview.ZodSharp.Blazor`. - **Cross-platform tests** — a shared TypeScript/Zod fixture set is generated into the repo and asserted against from both the C# test suite and a vitest suite. -- **Multi-target** — packages target `net10.0` and `net11.0`; the source generator targets `netstandard2.0` so it runs in any compiler host. \ No newline at end of file +- **Multi-target** — packages target `net10.0` and `net11.0`; the source generator targets `netstandard2.0` so it runs in any compiler host. diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index 0e8492de..ba4b88de 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -1,34 +1,47 @@ + + - [Home](Home.md) -- [Getting Started](Getting-Started.md) -- [Core Concepts](Core-Concepts.md) -- [Fluent Schema API](Fluent-Schema-API.md) -- [String Validation](String-Validation.md) -- [Number Validation](Number-Validation.md) -- [Object Validation](Object-Validation.md) -- [Arrays and Other Schemas](Arrays-and-Other-Schemas.md) -- [Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md) -- [Composition and Transforms](Composition-and-Transforms.md) -- [Custom Rules](Custom-Rules.md) -- [Validation Rules Reference](Validation-Rules-Reference.md) -- [Value Objects Integration](Value-Objects-Integration.md) -- [Compiled Validators and Caching](Compiled-Validators-and-Caching.md) -- [JSON Schema Export](JsonSchema-Export.md) -- [JSON Schema Import](JsonSchema-Import.md) -- [System.Text.Json Integration](SystemTextJson-Integration.md) -- [Newtonsoft.Json Integration](NewtonsoftJson-Integration.md) -- [HttpClient Integration](Http-Integration.md) -- [ASP.NET Core Integration](AspNetCore-Integration.md) -- [Microsoft.Extensions.Validation](Microsoft-Extensions-Validation.md) -- [OpenAPI Integration](OpenAPI-Integration.md) -- [Blazor Integration](Blazor-Integration.md) -- [Localization](Localization.md) -- [Dependency Injection](Dependency-Injection.md) -- [Source Generator](Source-Generator.md) -- [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) -- [Source Generator Diagnostics](Source-Generator-Diagnostics.md) -- [Cross-Platform Interop](Cross-Platform-Interop.md) -- [Trimming and Native AOT](Trimming-and-AOT.md) -- [Performance](Performance.md) -- [Guarantees and Limitations](Guarantees-and-Limitations.md) -- [Release Flow](Release-Flow.md) -- [Contributing](Contributing.md) \ No newline at end of file +- **Get started** + - [Getting Started](Getting-Started.md) + - [Core Concepts](concepts/Core-Concepts.md) + - [Fluent Schema API](Fluent-Schema-API.md) +- **Validation reference** + - [String Validation](schemas/String-Validation.md) + - [Number Validation](schemas/Number-Validation.md) + - [Object Validation](schemas/Object-Validation.md) + - [Arrays and Other Schemas](schemas/Arrays-and-Other-Schemas.md) + - [Unions and Discriminated Unions](schemas/Unions-and-Discriminated-Unions.md) + - [Composition and Transforms](concepts/Composition-and-Transforms.md) + - [Validation Rules Reference](schemas/Validation-Rules-Reference.md) + - [Custom Rules](schemas/Custom-Rules.md) + - [Value Objects Integration](schemas/Value-Objects-Integration.md) + - [Compiled Validators and Caching](schemas/Compiled-Validators-and-Caching.md) +- **Source generator** + - [Source Generator](generator/Source-Generator.md) + - [Source Generator DataAnnotations](generator/Source-Generator-DataAnnotations.md) + - [Source Generator Diagnostics](generator/Source-Generator-Diagnostics.md) +- **JSON and serialization** + - [JSON Schema Export](serialization/JsonSchema-Export.md) + - [JSON Schema Import](serialization/JsonSchema-Import.md) + - [System.Text.Json Integration](serialization/SystemTextJson-Integration.md) + - [Newtonsoft.Json Integration](serialization/NewtonsoftJson-Integration.md) +- **ASP.NET Core, Blazor and .NET 11** + - [ASP.NET Core Integration](AspNetCore-Integration.md) + - [Microsoft.Extensions.Validation](hosting/Microsoft-Extensions-Validation.md) + - [OpenAPI Integration](hosting/OpenAPI-Integration.md) + - [Blazor Integration](hosting/Blazor-Integration.md) + - [HttpClient Integration](hosting/Http-Integration.md) + - [Dependency Injection](hosting/Dependency-Injection.md) + - [Localization](hosting/Localization.md) +- **Guarantees, performance and deployment** + - [Guarantees and Limitations](quality/Guarantees-and-Limitations.md) + - [Performance](quality/Performance.md) + - [Trimming and Native AOT](quality/Trimming-and-AOT.md) + - [Cross-Platform Interop](quality/Cross-Platform-Interop.md) +- **Contributing and releases** + - [Contributing](development/Contributing.md) + - [Release Flow](development/Release-Flow.md) diff --git a/docs/wiki/Composition-and-Transforms.md b/docs/wiki/concepts/Composition-and-Transforms.md similarity index 98% rename from docs/wiki/Composition-and-Transforms.md rename to docs/wiki/concepts/Composition-and-Transforms.md index 61c2aecf..1518447f 100644 --- a/docs/wiki/Composition-and-Transforms.md +++ b/docs/wiki/concepts/Composition-and-Transforms.md @@ -102,7 +102,7 @@ var schema = Z.Number().Prefault(1); ## And / Or -- `.And(other)` → `ZodIntersection` — both must succeed; an issue raised identically by both sides is reported once (see [Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md)). +- `.And(other)` → `ZodIntersection` — both must succeed; an issue raised identically by both sides is reported once (see [Unions and Discriminated Unions](../schemas/Unions-and-Discriminated-Unions.md)). - `.Or(other)` → `ZodTypedUnion` — either may succeed. ## Example: layered validation diff --git a/docs/wiki/Core-Concepts.md b/docs/wiki/concepts/Core-Concepts.md similarity index 97% rename from docs/wiki/Core-Concepts.md rename to docs/wiki/concepts/Core-Concepts.md index 20bd1dce..2bc149df 100644 --- a/docs/wiki/Core-Concepts.md +++ b/docs/wiki/concepts/Core-Concepts.md @@ -5,7 +5,7 @@ Every schema derives from `ZodType` (namespace `ZodSharp.Core`). Validation is a two-phase pipeline: 1. **ParseInternal** — each schema overrides this hook to perform its type check and traversal (rejecting `null` where not allowed, coercing types, walking objects/arrays/tuples/unions, producing structured failures). -2. **Rules** — on success, the accumulated `IValidationRule` structs are evaluated. A failing rule emits a `ValidationError` carrying the rule's code (`too_small`, `too_big`, `not_multiple_of`, `not_finite`, `invalid_string`, `invalid_type`, `invalid_value`, …; `validation_failed` for custom rules without a code) and its structured identity: the Zod issue code as `Category`, an `Origin`, typed `Lower`/`Upper` bounds and parameters (see [Error fields](#error-fields)). Each rule exposes its reported code and message as public `const string ErrorCode` / `MessageFormat` constants, so tests can assert against the rule instead of duplicating literals (a rule that omits them is reported as `ZODSGEN042` — see [Custom Rules](Custom-Rules.md)). +2. **Rules** — on success, the accumulated `IValidationRule` structs are evaluated. A failing rule emits a `ValidationError` carrying the rule's code (`too_small`, `too_big`, `not_multiple_of`, `not_finite`, `invalid_string`, `invalid_type`, `invalid_value`, …; `validation_failed` for custom rules without a code) and its structured identity: the Zod issue code as `Category`, an `Origin`, typed `Lower`/`Upper` bounds and parameters (see [Error fields](#error-fields)). Each rule exposes its reported code and message as public `const string ErrorCode` / `MessageFormat` constants, so tests can assert against the rule instead of duplicating literals (a rule that omits them is reported as `ZODSGEN042` — see [Custom Rules](../schemas/Custom-Rules.md)). ```csharp ValidationResult result = schema.Validate(value); @@ -45,7 +45,7 @@ compares every field, including the path segments and parameters by value, so tw | `Code` | ZodSharp's specific identity: a rule's `ErrorCode` (`too_small`, `invalid_string`, `invalid_length`, …) or a schema code from `ZodIssueCodes` (`invalid_type`, `missing_field`, `invalid_union`, `unrecognized_keys`, `refinement_failed`, `transform_error`, …). The same failure reports the same code whichever producer reports it (runtime schema, generated validator, JSON Schema import). | | `Category` | The Zod v4 issue code (`ZodIssueCategories`): `invalid_type`, `too_small`, `too_big`, `invalid_format`, `not_multiple_of`, `unrecognized_keys`, `invalid_union`, `invalid_key`, `invalid_element`, `invalid_value`, `custom`. Every built-in error sets it, so a client can treat ZodSharp and Zod issues alike. | | `Message` | The rendered message, formatted with the invariant culture. Built-in messages never quote string input (see [PII](#pii)). | -| `MessageFormat` | The template that produced `Message`, with named placeholders (`"{displayName} is required"`), or `null` when the message was supplied literally. Hosts re-format it in the request culture (see [Localization](Localization.md)). | +| `MessageFormat` | The template that produced `Message`, with named placeholders (`"{displayName} is required"`), or `null` when the message was supplied literally. Hosts re-format it in the request culture (see [Localization](../hosting/Localization.md)). | | `FullPath` | The authoritative, typed location (`ZodPath`); see [Paths](#paths). | | `Path` | The legacy `ImmutableArray` projection of `FullPath`: members and keys as-is, indexes as digits (`["Items", "0", "Name"]`). Derived from `FullPath`, so the two never disagree. | | `Origin` | The kind of value that failed: `string`, `number`, `bigint`, `date`, `array`, `object`, `enum`, … | @@ -73,7 +73,7 @@ The shared identities live in `ZodSharp.Core`, so every producer reports a failu A failed union reports one `invalid_union` error whose per-option issues are available through `error.GetUnionErrors()` (an `ImmutableArray>`, stored under the `unionErrors` -parameter); see [Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md). +parameter); see [Unions and Discriminated Unions](../schemas/Unions-and-Discriminated-Unions.md). Create custom issues with `ValidationError.Create(code, message, path, parameters, origin, minimum, maximum, inclusive, category)`; `path` may be a `ZodPath`, an `ImmutableArray` or (through the constructor) a `string[]`. Fields not in @@ -166,7 +166,7 @@ re-formats `MessageFormat` when every placeholder resolves and otherwise returns literally, `{{`/`}}` escape braces, and named placeholders resolve from the parameters and then the built-in fields (`displayName`, `member`, `path`, `code`, `category`, `origin`, `minimum`, `maximum`, `inclusive`; `displayName` is the last member of the path, or `Value` at the root). A sequence parameter (an options or keys list) renders as its elements joined by `, `. See -[Localization](Localization.md). +[Localization](../hosting/Localization.md). ## PII @@ -215,7 +215,7 @@ catch (ZodException ex) - Schemas are classes deriving from `ZodType`; the fluent methods return `this` (or a wrapping schema) so chains read naturally. - Rules are `readonly record struct` implementations of `IValidationRule` (`bool IsValid(in T value)`, `string GetErrorMessage(in T value)`) — zero allocation. -- Custom rules can be attached with the public `AddRule`/`Rule` methods and surfaced as DataAnnotations-style attributes; see [Custom Rules](Custom-Rules.md). +- Custom rules can be attached with the public `AddRule`/`Rule` methods and surfaced as DataAnnotations-style attributes; see [Custom Rules](../schemas/Custom-Rules.md). - `ValidateSpan(ReadOnlySpan value)` is available on `ZodString` for span-based validation; use `IsValidSpan(value, out errors)` for an allocation-free check. - A schema's `Description` is set with `.Describe("...")`. @@ -254,4 +254,4 @@ Object fields and union options wrap typed schemas so boxed values from a `Dicti ## Dependency injection -`IZodSchemaFactory` is the registry that resolves validators by type. See [Dependency Injection](Dependency-Injection.md). \ No newline at end of file +`IZodSchemaFactory` is the registry that resolves validators by type. See [Dependency Injection](../hosting/Dependency-Injection.md). \ No newline at end of file diff --git a/docs/wiki/development/Contributing.md b/docs/wiki/development/Contributing.md new file mode 100644 index 00000000..74220fb6 --- /dev/null +++ b/docs/wiki/development/Contributing.md @@ -0,0 +1,113 @@ +# Contributing + +Contributions are welcome. Please open an issue or pull request against [purview-dev/zodsharp](https://github.com/purview-dev/zodsharp). + +## Repository layout + +``` +src/ + src/ + ZodSharp/ Core validation library + JSON Schema export (Z.ToJsonSchema) + SourceGenerators/ Compile-time [ZodSchema] generator (netstandard2.0, Roslyn) + SystemTextJson/ System.Text.Json integration + JSON Schema import + NewtonsoftJson/ Newtonsoft.Json integration + JSON Schema import + AspNetCore/ ASP.NET Core ProblemDetails integration + Examples.CLI/ Usage examples + Benchmarks/ BenchmarkDotNet performance suite + tests/ + *.UnitTests/ TUnit test projects +src/ts/ TypeScript (Zod) schema + fixture generation +tests/ts/ Vitest cross-platform tests +docs/wiki/ This documentation suite +``` + +## Commands + +```bash +just build # dotnet build src/ZodSharp.slnx -c Debug +just test # dotnet test src/ZodSharp.slnx -c Debug --treenode-filter "/*/*/*/*" +just lint-check # dotnet csharpier check . +just lint-fix # dotnet csharpier format . +just pack # dotnet pack src/ZodSharp.slnx -c Debug -o artifacts +just perf-tests # dotnet run --project src/src/Benchmarks/Benchmarks.csproj -c Release +``` + +TypeScript tooling uses Bun: + +```bash +bun install +bun run test # vitest run (cross-platform TS tests) +bun run generate-fixtures # regenerates src/ts/fixtures/*.json from Zod +``` + +## Testing bar + +- Framework: **TUnit**. +- `[Test]` methods take a `CancellationToken cancellationToken` parameter where relevant. +- Use `// Arrange`, `// Act`, `// Assert` comments. +- Use meaningful, descriptive names (`Action_GivenCondition_ExpectedResult`). +- Treat work as incomplete until the relevant tests pass. + +For source generator tests, use the `Purview.SourceGeneratorFramework.Testing.TUnit` base classes (`TUnitSourceGeneratorTestBase`, `TUnitDiagnosticAnalyzerTestBase`) and assert with `CodeQuery`; test incrementally, not just generated text. + +## Documentation + +This wiki lives in `docs/wiki/`, which is the documentation source for [purview.dev](https://purview.dev): the site build (purview-dev Astro/Starlight) pulls these files via `github-path` sync, publishes each file at `/docs/zodsharp///`, and rewrites relative `.md` links into site routes. `_Sidebar.md` and `Home.md` are not published; the site's landing page is `Getting-Started.md` (`docs.rootPage` in the site's project catalogue). + +### Layout + +One folder per topic area, so a page can be found by what it is about rather than by remembering its file name: + +``` +docs/wiki/ + Getting-Started.md The landing page (docs.rootPage) + Fluent-Schema-API.md Deep-linked by the site catalogue (docsPage) + AspNetCore-Integration.md Deep-linked by the site catalogue (docsPage) + Home.md The index (not published) + _Sidebar.md Page order (not published) + concepts/ Core Concepts; Composition and Transforms + schemas/ One page per schema family, plus the rule catalogue + generator/ Source Generator, DataAnnotations, Diagnostics + serialization/ JSON Schema export and import, System.Text.Json, Newtonsoft.Json + hosting/ Microsoft.Extensions.Validation, OpenAPI, Blazor, HttpClient, DI, localization + quality/ Guarantees, performance, trimming/AOT, cross-platform interop + development/ This page and the release flow +``` + +The pages are also grouped into seven sections, in reading order. The same seven sections, in the same order, are the groups in `_Sidebar.md`, the `## ` headings in `Home.md`, and the seven `Where to go next` bullets at the end of `Getting-Started.md` (the last of those name each section by the goal it serves rather than by its title). + +| Section | Folder | +| --- | --- | +| Get started | the wiki root and `concepts/` | +| Validation reference | `schemas/`, plus `concepts/Composition-and-Transforms.md` | +| Source generator | `generator/` | +| JSON and serialization | `serialization/` | +| ASP.NET Core, Blazor and .NET 11 | `hosting/`, plus the root-level `AspNetCore-Integration.md` | +| Guarantees, performance and deployment | `quality/` | +| Contributing and releases | `development/` | + +### Conventions + +- **The folder is part of the URL.** The sync derives a page's slug from its path relative to `docs/wiki/`, so `schemas/Custom-Rules.md` is published at `/docs/zodsharp/schemas/custom-rules/`. Moving a page between folders therefore changes its URL. +- **File and folder names are URL-safe already.** Letters, digits and single dashes only (`SystemTextJson-Integration.md`, not `System Text JSON.md`): each name becomes an address segment verbatim, lowercased, so nothing has to be percent-encoded and the address mirrors the tree. The sync refuses to publish a path whose folder or file name reduces to nothing. +- **Three pages stay at the wiki root.** `Getting-Started.md` is the landing page, and `Fluent-Schema-API.md` / `AspNetCore-Integration.md` are the two pages the site's project catalogue deep-links through `docsPage`. That slug must be a single path segment (the catalogue schema rejects `/`) and must exist in both documentation channels, so those two keep root-level URLs and move into a folder only when the deep link moves with them. +- **`_Sidebar.md` is the page-order authority.** The sync reads its links in document order and orders the published sidebar accordingly, so every page appears there exactly once, in reading order, inside its section, with its path relative to the wiki root. Its bold labels are the section names; they are never rendered by the site, but they are what a reader of the file sees. +- **`Home.md` is the index.** It lists every page at least once, inside the section it belongs to, with a one-line "what it answers" - it is what a reader sees when browsing the folder on GitHub. +- **Every page starts with a single `# ` heading** (the page title, and the sidebar label the site uses) followed by a one-paragraph description. +- **Link to a sibling by file name, and to anything else by its path from the page**: `[Custom Rules](Custom-Rules.md)` within `schemas/`, `[Core Concepts](../concepts/Core-Concepts.md)` across folders, optionally with a fragment (`Core-Concepts.md#error-fields`). The sync resolves each link relative to the page it appears on. +- **Renaming or moving a page changes its published URL.** Add the old slug to `docs.redirects` in the site's `zodsharp.yml` so bookmarks and inbound links keep working; the generated redirect never shadows a real page, so it activates when the move is released. +- GitHub alert blockquotes (`> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!CAUTION]`, `> [!IMPORTANT]`) are converted to Starlight asides. +- Relative repository-path links (e.g. `src/src/ZodSharp/Z.cs`) are rewritten to GitHub blob URLs automatically. +- Keep the `CrossPlatformUserSchema` (C#) and `UserSchema` (TypeScript) in sync when either changes. + +### Adding a page + +1. Add the file to the folder that covers the topic (`docs/wiki/schemas/My-Page.md`), starting with a single `# ` heading and a one-paragraph description. A page that does not fit an existing folder is a new topic area, and a new folder needs a new section - decide that deliberately rather than by adding to an unrelated one. +2. Add it to `_Sidebar.md` exactly once, inside that section's list and in the position a reader should meet it, with its path relative to the wiki root. +3. Add it to `Home.md` exactly once, in that section's table, with a one-line "what it answers". + +`DocumentationWikiTests` (`src/tests/Repository.UnitTests`) enforces all three: every page is listed exactly once in the sidebar and is reachable from `Home.md`, every sidebar and page link resolves to an existing file, every sidebar section has a matching `## ` heading in `Home.md`, every folder holds at least one page listed in the sidebar, and every page opens with a single `# ` title. `just test` runs it. + +## Formatting + +Formatting is enforced with CSharpier. `.editorconfig` at the repo root defines style (tabs for code, 2-space for XML/JSON/YAML/markdown). diff --git a/docs/wiki/Release-Flow.md b/docs/wiki/development/Release-Flow.md similarity index 97% rename from docs/wiki/Release-Flow.md rename to docs/wiki/development/Release-Flow.md index 5c5a4c2f..38ef76d4 100644 --- a/docs/wiki/Release-Flow.md +++ b/docs/wiki/development/Release-Flow.md @@ -33,7 +33,7 @@ rather than passing. | Packed generator | The IL-merged generator in the `.nupkg` loads and generates in a net10.0 consumer (it has regressed twice while the in-repo tests, which use the unmerged generator, stayed green). | `just smoke-packed-generator` | `pr.yml` `packed-generator` | | Packed consumers | Every shipping package, packed to an isolated feed and cache, works for consumers that reference packages only: a Native AOT console (generated validation, member resolution, count attributes, JSON Schema export, the validation bridge, ProblemDetails, OpenAPI options), a Blazor WebAssembly app with `` and a scalar value object carrying a type-level rule (trimmed, and `RunAOTCompilation` when `wasm-tools` is installed; `_framework` holds the ZodSharp assemblies, `_content` the static web assets) and a Native AOT minimal API (an invalid POST returns 400 `application/problem+json` with `issues`; the OpenAPI document carries `ZodValidationProblemDetails`), and a multi-project API - a `[ZodSchema]`/`[ErrorType]` class library referenced by a minimal API that references `Microsoft.AspNetCore.OpenApi` with XML documentation on (the OpenAPI document is served with both projects' schemas and XML-comment descriptions, and no documentation ID is duplicated). Every bin and publish folder is leak-scanned. | `just smoke-packed-consumers` | `pr.yml` `packed-consumers` (linux-x64, `-WasmAot Require`) | | Compiler-host leak scan | No generator, code-fix, Purview.SourceGeneratorFramework or Roslyn assembly reaches build or publish output, by file name or in a `deps.json`, `runtimeconfig.json` or boot manifest. Purview.SourceGeneratorFramework.dll once leaked this way and broke Blazor WebAssembly AOT while every gate passed. | `scripts/assert-no-compiler-host-assemblies.ps1 -Path ` (`just scan-compiler-host-leaks `); every publish recipe below runs it | a step in every job that publishes; `ReleaseGateTests` fails a job that publishes without it | -| Trim and Native AOT harness | The whole trim-safe surface is warning-free under ILC with every ZodSharp assembly rooted (IL3054 alone tolerated, see [Trimming and Native AOT](Trimming-and-AOT.md#the-rooted-publish-and-il3054-generic-recursion)), and again as a plain consumer; each native binary checks its behaviour. | `just verify-trim ` | `pr.yml` `verify-trim` | +| Trim and Native AOT harness | The whole trim-safe surface is warning-free under ILC with every ZodSharp assembly rooted (IL3054 alone tolerated, see [Trimming and Native AOT](../quality/Trimming-and-AOT.md#the-rooted-publish-and-il3054-generic-recursion)), and again as a plain consumer; each native binary checks its behaviour. | `just verify-trim ` | `pr.yml` `verify-trim` | | Native AOT sample | Examples.MinimalApi publishes with `PublishAot=true` and no IL warning, and the native binary returns the ProblemDetails contract. | `just publish-aot-sample` | `pr.yml` `aot-sample` | | WebAssembly size budget | Examples.Blazor.Client publishes trimmed with no IL warning, and each ZodSharp assembly stays within 10% of `.config/wasm-size-baseline.json`. | `just wasm-size` | `pr.yml` `wasm-trimmed` | | WebAssembly AOT and browser | Examples.Blazor.Client compiles with `RunAOTCompilation=true`; the published Blazor sample passes its browser checks. | `just wasm-size -p:RunAOTCompilation=true` | `wasm-nightly.yml` `wasm-aot`, `blazor-e2e` | diff --git a/docs/wiki/Source-Generator-DataAnnotations.md b/docs/wiki/generator/Source-Generator-DataAnnotations.md similarity index 97% rename from docs/wiki/Source-Generator-DataAnnotations.md rename to docs/wiki/generator/Source-Generator-DataAnnotations.md index ae87c1a8..94ccfbd0 100644 --- a/docs/wiki/Source-Generator-DataAnnotations.md +++ b/docs/wiki/generator/Source-Generator-DataAnnotations.md @@ -192,7 +192,7 @@ A `[ZodSchema]` type does not need `[ValidatableType]`: the ZodSharp bridge (`Purview.ZodSharp.Extensions.Validation`) resolves its generated validator first, and the attribute only makes the Microsoft.Extensions.Validation generator emit a second, DataAnnotations-only validator for the same type. The generator reports `ZODSGEN047` (info), with a code fix that removes the attribute. See -[Microsoft.Extensions.Validation](Microsoft-Extensions-Validation.md). +[Microsoft.Extensions.Validation](../hosting/Microsoft-Extensions-Validation.md). ## Type applicability diagnostics @@ -207,8 +207,8 @@ See [Source Generator Diagnostics](Source-Generator-Diagnostics.md) for the full ## Custom attributes -The same pipeline honours custom rules exposed as validation attributes. Mark the attribute with `[ZodRule(typeof(MyRule))]` (or mark the rule itself with `[ZodRule]` to have the attribute generated), and properties annotated with it are validated through the rule. See [Custom Rules](Custom-Rules.md). +The same pipeline honours custom rules exposed as validation attributes. Mark the attribute with `[ZodRule(typeof(MyRule))]` (or mark the rule itself with `[ZodRule]` to have the attribute generated), and properties annotated with it are validated through the rule. See [Custom Rules](../schemas/Custom-Rules.md). -Every built-in rule also ships a generated attribute in the `ZodSharp.Rules` namespace, so it can be used directly alongside the attributes above: `[Email]`, `[E164]`, `[Ulid]`, `[Uuid]`, `[Jwt]`, `[IpAddress]`, `[Hex]`, `[Regex]`, `[StartsWith]`, `[EndsWith]`, `[Includes]`, `[MultipleOf]`, `[Finite]`, `[SafeInteger]`, `[Int]`, `[Uri]`, `[Base64Url]`, `[Nanoid]`, `[Cuid2]`, `[DateString]`, `[DatetimeString]`, `[TimeString]`, `[Emoji]`, `[Xid]`, `[Ksuid]`, `[Duration]`, `[Guid]`, `[Cidr]`, `[NonSentinel]`, `[NullOrNonWhiteSpace]`, `[MinValue]`, `[MaxValue]`, `[GreaterThan]`, `[LessThan]`, `[GreaterThanOrEqual]`, `[LessThanOrEqual]`, `[Even]`, `[Odd]`, `[Enum]`, `[MinCount]`, `[MaxCount]`, `[CountRange]`, `[RequiredZod]`, `[RangeZod]`, `[LengthZod]`, `[StringLengthZod]`, `[CompareZod]`, `[AllowedValuesZod]`, and `[DeniedValuesZod]`. A value the rule declares without a default is a required constructor argument — `[Regex("…")]`, `[UUID(UuidVersion.V4)]`, `[Uri(UriKind.Absolute)]`, `[MinValue(3)]` — while defaulted values and `Message`/`Code` stay named properties. A rule with overloaded constructors mirrors each overload, so `[Uuid]` uses the versionless UUID rule and `[UUID(UuidVersion.V4)]` the versioned one. `[UUID]` also applies to a `Guid` member or a `[Scalar]` type (a `Guid` is a UUID, so the rule validates its bytes), which is how a time-bound identifier that must be an RFC version is constrained. The rules whose name collides with a DataAnnotations attribute use a `Zod` suffix (`[MinLengthZod]`, `[MaxLengthZod]`, `[UrlZod]`, `[PhoneZod]`, `[CreditCardZod]`, `[Base64StringZod]`, `[RequiredZod]`, `[RangeZod]`, `[LengthZod]`, `[StringLengthZod]`, `[CompareZod]`, `[AllowedValuesZod]`, `[DeniedValuesZod]`). See [Built-in attributes](Custom-Rules.md#built-in-attributes-shipped-with-purviewzodsharp). +Every built-in rule also ships a generated attribute in the `ZodSharp.Rules` namespace, so it can be used directly alongside the attributes above: `[Email]`, `[E164]`, `[Ulid]`, `[Uuid]`, `[Jwt]`, `[IpAddress]`, `[Hex]`, `[Regex]`, `[StartsWith]`, `[EndsWith]`, `[Includes]`, `[MultipleOf]`, `[Finite]`, `[SafeInteger]`, `[Int]`, `[Uri]`, `[Base64Url]`, `[Nanoid]`, `[Cuid2]`, `[DateString]`, `[DatetimeString]`, `[TimeString]`, `[Emoji]`, `[Xid]`, `[Ksuid]`, `[Duration]`, `[Guid]`, `[Cidr]`, `[NonSentinel]`, `[NullOrNonWhiteSpace]`, `[MinValue]`, `[MaxValue]`, `[GreaterThan]`, `[LessThan]`, `[GreaterThanOrEqual]`, `[LessThanOrEqual]`, `[Even]`, `[Odd]`, `[Enum]`, `[MinCount]`, `[MaxCount]`, `[CountRange]`, `[RequiredZod]`, `[RangeZod]`, `[LengthZod]`, `[StringLengthZod]`, `[CompareZod]`, `[AllowedValuesZod]`, and `[DeniedValuesZod]`. A value the rule declares without a default is a required constructor argument — `[Regex("…")]`, `[UUID(UuidVersion.V4)]`, `[Uri(UriKind.Absolute)]`, `[MinValue(3)]` — while defaulted values and `Message`/`Code` stay named properties. A rule with overloaded constructors mirrors each overload, so `[Uuid]` uses the versionless UUID rule and `[UUID(UuidVersion.V4)]` the versioned one. `[UUID]` also applies to a `Guid` member or a `[Scalar]` type (a `Guid` is a UUID, so the rule validates its bytes), which is how a time-bound identifier that must be an RFC version is constrained. The rules whose name collides with a DataAnnotations attribute use a `Zod` suffix (`[MinLengthZod]`, `[MaxLengthZod]`, `[UrlZod]`, `[PhoneZod]`, `[CreditCardZod]`, `[Base64StringZod]`, `[RequiredZod]`, `[RangeZod]`, `[LengthZod]`, `[StringLengthZod]`, `[CompareZod]`, `[AllowedValuesZod]`, `[DeniedValuesZod]`). See [Built-in attributes](../schemas/Custom-Rules.md#built-in-attributes-shipped-with-purviewzodsharp). -`[MinCount(n)]`, `[MaxCount(n)]` and `[CountRange(min, max)]` are the trim-safe spelling of `[MinLength]`, `[MaxLength]` and `[Length]` on a collection or dictionary member: the BCL attributes' constructors are `[RequiresUnreferencedCode]`, so they raise IL2026 in a trimmed or AOT application. The generator validates both spellings through the same code, with the same error identity, and `ZODSGEN053` suggests the swap in a trimmed project. See [Trimming and Native AOT](Trimming-and-AOT.md). \ No newline at end of file +`[MinCount(n)]`, `[MaxCount(n)]` and `[CountRange(min, max)]` are the trim-safe spelling of `[MinLength]`, `[MaxLength]` and `[Length]` on a collection or dictionary member: the BCL attributes' constructors are `[RequiresUnreferencedCode]`, so they raise IL2026 in a trimmed or AOT application. The generator validates both spellings through the same code, with the same error identity, and `ZODSGEN053` suggests the swap in a trimmed project. See [Trimming and Native AOT](../quality/Trimming-and-AOT.md). \ No newline at end of file diff --git a/docs/wiki/Source-Generator-Diagnostics.md b/docs/wiki/generator/Source-Generator-Diagnostics.md similarity index 98% rename from docs/wiki/Source-Generator-Diagnostics.md rename to docs/wiki/generator/Source-Generator-Diagnostics.md index ab9f60c5..7eb55cb1 100644 --- a/docs/wiki/Source-Generator-Diagnostics.md +++ b/docs/wiki/generator/Source-Generator-Diagnostics.md @@ -51,12 +51,12 @@ The `[ZodSchema]` generator ships an analyzer (category `ZodSharp.SourceGenerato | ZODSGEN050 | A generated `IValidateOptions` validator would conflict with a hand-authored `[OptionsValidator]` class (SYSLIB1205), so the ZodSharp validator is not generated. Keep one generator: remove `[OptionsValidator]`, or set `[ZodSchema(SuppressIValidateOptions = true)]` | | ZODSGEN051 | (warning) A `[ZodSchema]` type is declared in a Razor component (`.razor`). Razor compiles components with its own source generator, which the ZodSharp generator cannot see, so no schema is generated; move the type to a `.cs` file | | ZODSGEN052 | (warning) An attribute's `ErrorMessage` uses a positional placeholder the attribute does not supply, or an unbalanced brace (`"The ErrorMessage of [{0}] on '{1}' contains '{2}', which the attribute does not supply; it is shown literally"`). Messages are formatted with a never-throw engine, so the text is shown literally at runtime; the diagnostic catches the typo at build time | -| ZODSGEN053 | (warning) `[MinLength]`, `[MaxLength]` or `[Length]` is applied to a collection member of a `[ZodSchema]` type in a project with `PublishTrimmed`, `PublishAot`, `IsTrimmable` or `IsAotCompatible` (`"[{0}] on the collection member '{1}' is not trim-safe (its constructor is [RequiresUnreferencedCode]); use [{2}] from ZodSharp.Rules, which the generator validates identically"`). The BCL attributes raise IL2026 on every usage; `[MinCount]`, `[MaxCount]` and `[CountRange]` report the same errors. A code fix swaps the attribute. See [Trimming and Native AOT](Trimming-and-AOT.md#collection-counts-mincount-maxcount-and-countrange) | +| ZODSGEN053 | (warning) `[MinLength]`, `[MaxLength]` or `[Length]` is applied to a collection member of a `[ZodSchema]` type in a project with `PublishTrimmed`, `PublishAot`, `IsTrimmable` or `IsAotCompatible` (`"[{0}] on the collection member '{1}' is not trim-safe (its constructor is [RequiresUnreferencedCode]); use [{2}] from ZodSharp.Rules, which the generator validates identically"`). The BCL attributes raise IL2026 on every usage; `[MinCount]`, `[MaxCount]` and `[CountRange]` report the same errors. A code fix swaps the attribute. See [Trimming and Native AOT](../quality/Trimming-and-AOT.md#collection-counts-mincount-maxcount-and-countrange) | IDs `ZODSGEN002` and `ZODSGEN022`–`ZODSGEN026` are intentionally unused; rule identifiers are never renumbered or re-used. The `[ErrorType]` generator, shipped in the same analyzer assembly, reports `ZODSASP001`–`ZODSASP003`, -`ZODSASP100` and `ZODSASP101`; they are described in [ASP.NET Core Integration](AspNetCore-Integration.md). +`ZODSASP100` and `ZODSASP101`; they are described in [ASP.NET Core Integration](../AspNetCore-Integration.md). ## Suppressing diff --git a/docs/wiki/Source-Generator.md b/docs/wiki/generator/Source-Generator.md similarity index 96% rename from docs/wiki/Source-Generator.md rename to docs/wiki/generator/Source-Generator.md index 9ffb12de..acf8db27 100644 --- a/docs/wiki/Source-Generator.md +++ b/docs/wiki/generator/Source-Generator.md @@ -39,7 +39,7 @@ For a `[ZodSchema]` target type `{TypeName}`, the generator emits: `BuildJsonSchema` returns a `ZodSharp.JsonSchema.JsonSchemaDefinition` built from the same metadata as `Validate`, so a generated `[ZodSchema]` schema can be exported through `Z.ToJsonSchema` without rebuilding its -rules by hand. See [JSON Schema Export](JsonSchema-Export.md#export-a-generated-zodschema). +rules by hand. See [JSON Schema Export](../serialization/JsonSchema-Export.md#export-a-generated-zodschema). ```csharp // Value-first composition methods (EnableComposition, default true): @@ -93,7 +93,7 @@ foreach (var error in result.Errors) Generated validators locate collection and dictionary errors with typed index and key segments, so `ToDottedString()` renders `Lines[0].Sku` and `ByCode[abc].Sku` (an all-digit or punctuated key is quoted: `ByCode["0"]`, `ByCode["a.b"]`). Keys are rendered with the invariant culture. See -[Core Concepts](Core-Concepts.md) for the path format. +[Core Concepts](../concepts/Core-Concepts.md) for the path format. ### Validating a single member @@ -353,7 +353,7 @@ A type that also has a hand-authored `[OptionsValidator]` class (Microsoft.Exten would get two validators for the same options (SYSLIB1205), so the ZodSharp validator is not generated and `ZODSGEN050` is reported. Keep one: remove `[OptionsValidator]`, or set `[ZodSchema(SuppressIValidateOptions = true)]`. For options validated through the factory instead, see -[Dependency Injection](Dependency-Injection.md#validating-options-objects). +[Dependency Injection](../hosting/Dependency-Injection.md#validating-options-objects). ## Trim-safe registration @@ -378,7 +378,7 @@ builder.Services.AddZodSharpContosoOrdersSchemas(); extension adds a `ZodSchemaRegistration` and ensures `AddZodSharpFactory()` has registered the factory, which applies every registration when it is first resolved — so calls from several model assemblies compose. Prefer it to `RegisterFromAssembly`, which discovers validators through metadata and is not trim-safe. See -[Dependency Injection](Dependency-Injection.md#registering-source-generated-validators). +[Dependency Injection](../hosting/Dependency-Injection.md#registering-source-generated-validators). ## Unions and closed hierarchies (C# 15) @@ -472,7 +472,7 @@ public class RepositoryInventoryResponse > [!NOTE] > `[CollectionBehavior]` and `[ZodSchema(AllowNulls = ...)]` only take effect when the named argument is present; an attribute that leaves it out defers to the next level. `[CollectionBehavior]` with options that cannot affect the property is reported as `ZODSGEN046`. -A collection whose elements may be null exports its `items` schema with a `null` branch, mirroring how a nullable property is exported. A dictionary exports `{ "type": "object", "additionalProperties": … }` — see [JSON Schema Export](JsonSchema-Export.md#dictionaries). +A collection whose elements may be null exports its `items` schema with a `null` branch, mirroring how a nullable property is exported. A dictionary exports `{ "type": "object", "additionalProperties": … }` — see [JSON Schema Export](../serialization/JsonSchema-Export.md#dictionaries). ### Struct collections and default immutable arrays @@ -542,11 +542,11 @@ property, then `PreserveOffset`. The attribute shapes the schema only; pair `Utc package's `UtcDateTimeOffsetConverter` (and `PreserveOffset` under a global UTC converter with `PreserveOffsetDateTimeOffsetConverter`). Both modes export an exact sentinel pattern; `Utc` additionally narrows the accepted wire form to a zero offset. See -[JSON Schema Export](JsonSchema-Export.md#temporal-sentinels-and-the-datetimeoffset-wire-form). +[JSON Schema Export](../serialization/JsonSchema-Export.md#temporal-sentinels-and-the-datetimeoffset-wire-form). ## Automatic enum validation -Every non-flags enum property is validated automatically: the generated validator rejects a value that is not a defined member of the enum type. The check is emitted as an `EnumRule` (see [Validation Rules Reference](Validation-Rules-Reference.md#enum-rules)) and reports `invalid_enum_value`. +Every non-flags enum property is validated automatically: the generated validator rejects a value that is not a defined member of the enum type. The check is emitted as an `EnumRule` (see [Validation Rules Reference](../schemas/Validation-Rules-Reference.md#enum-rules)) and reports `invalid_enum_value`. A member that is defined but never a valid value can be excluded globally by marking it `[ZodIgnore]`, and excluded for a single property with `[DeniedValues]`: @@ -593,7 +593,7 @@ A nullable enum property is validated only when it is not `null`. - Nullable properties are null-guarded before value-set/type validation; a nullable target rejects `null` with `invalid_type`. - A target implementing `IValidatableObject` is validated by it after the members and type rules pass, as DataAnnotations and Microsoft.Extensions.Validation do. On .NET 11, `IAsyncValidatableObject` and `AsyncValidationAttribute` members run in `ValidateAsync`; a synchronous `Validate` of such a type throws `InvalidOperationException` (`ZODSGEN048` warns at build time). See [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md#ivalidatableobject-and-asynchronous-dataannotations). -- A `Purview.ValueObjects` scalar marked with `[Scalar]` can carry `[ZodSchema]` on the same type; the generated validator validates the scalar as a unit and reports an empty path. A rule written against the scalar's underlying value is adapted automatically, or resolved against the unwrapped value and null-guarded when the underlying value is a `Nullable` — see [Value Objects Integration](Value-Objects-Integration.md). -- A rule marked with the parameterless `[ZodRule]` generates a matching validation attribute. Every built-in rule ships its attribute inside `Purview.ZodSharp` (in the `ZodSharp.Rules` namespace — `[Email]`, `[E164]`, `[Regex]`, `[NonSentinel]`, `[MinValue]`, `[Even]`, …; names that collide with `System.ComponentModel.DataAnnotations` use a `Zod` suffix such as `[MinLengthZod]`), so they can annotate a member or a scalar value object directly — see [Built-in attributes](Custom-Rules.md#built-in-attributes-shipped-with-purviewzodsharp). +- A `Purview.ValueObjects` scalar marked with `[Scalar]` can carry `[ZodSchema]` on the same type; the generated validator validates the scalar as a unit and reports an empty path. A rule written against the scalar's underlying value is adapted automatically, or resolved against the unwrapped value and null-guarded when the underlying value is a `Nullable` — see [Value Objects Integration](../schemas/Value-Objects-Integration.md). +- A rule marked with the parameterless `[ZodRule]` generates a matching validation attribute. Every built-in rule ships its attribute inside `Purview.ZodSharp` (in the `ZodSharp.Rules` namespace — `[Email]`, `[E164]`, `[Regex]`, `[NonSentinel]`, `[MinValue]`, `[Even]`, …; names that collide with `System.ComponentModel.DataAnnotations` use a `Zod` suffix such as `[MinLengthZod]`), so they can annotate a member or a scalar value object directly — see [Built-in attributes](../schemas/Custom-Rules.md#built-in-attributes-shipped-with-purviewzodsharp). -See [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) for the attribute coverage and structured issue shape, [Custom Rules](Custom-Rules.md) for extending validation with your own rules and attributes, and [Source Generator Diagnostics](Source-Generator-Diagnostics.md) for the `ZODSGEN*` diagnostics. \ No newline at end of file +See [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) for the attribute coverage and structured issue shape, [Custom Rules](../schemas/Custom-Rules.md) for extending validation with your own rules and attributes, and [Source Generator Diagnostics](Source-Generator-Diagnostics.md) for the `ZODSGEN*` diagnostics. \ No newline at end of file diff --git a/docs/wiki/Blazor-Integration.md b/docs/wiki/hosting/Blazor-Integration.md similarity index 99% rename from docs/wiki/Blazor-Integration.md rename to docs/wiki/hosting/Blazor-Integration.md index 2f3cb518..660c11e8 100644 --- a/docs/wiki/Blazor-Integration.md +++ b/docs/wiki/hosting/Blazor-Integration.md @@ -201,7 +201,7 @@ if (await response.TryReadZodValidationProblemAsync() is { } problem) ``` - `TryReadZodValidationProblemAsync` reads an unsuccessful JSON response with `errors` or `issues` - the contract - [ASP.NET Core Integration](AspNetCore-Integration.md) writes - using source-generated JSON metadata. It and + [ASP.NET Core Integration](../AspNetCore-Integration.md) writes - using source-generated JSON metadata. It and `ZodValidationProblemDetails` (namespace `ZodSharp.Http`) come from `Purview.ZodSharp.Http`, which this package references; see [HttpClient Integration](Http-Integration.md) for validating request and response bodies too. - `AddZodProblemDetails` places issues by their typed `segments`, so `Lines[0].Quantity` lands on diff --git a/docs/wiki/Dependency-Injection.md b/docs/wiki/hosting/Dependency-Injection.md similarity index 98% rename from docs/wiki/Dependency-Injection.md rename to docs/wiki/hosting/Dependency-Injection.md index 9fd6a405..09eda988 100644 --- a/docs/wiki/Dependency-Injection.md +++ b/docs/wiki/hosting/Dependency-Injection.md @@ -81,7 +81,7 @@ builder.Services.AddZodSchemaRegistration(factory => factory.Register(new ZodSch The `Purview.ZodSharp.AspNetCore` package offers the richer `AddZodSharp` with exact assembly scans, assembly-graph scans, loaded-assembly scans, and additive assembly-contribution helpers — see -[ASP.NET Core Integration](AspNetCore-Integration.md). +[ASP.NET Core Integration](../AspNetCore-Integration.md). ### Choosing a registration method @@ -177,7 +177,7 @@ validation in its own service scope. Synchronous options access (`IOptions.Va runs the synchronous rules only — that is the platform's contract. On earlier frameworks only the synchronous path exists. -The source generator also auto-generates `IValidateOptions` validators for types whose names end in configurable suffixes — see [Source Generator](Source-Generator.md#ivalidateoptions-support). Their failures read `{path}: {message} ({code})`. +The source generator also auto-generates `IValidateOptions` validators for types whose names end in configurable suffixes — see [Source Generator](../generator/Source-Generator.md#ivalidateoptions-support). Their failures read `{path}: {message} ({code})`. ## Example: consuming a factory diff --git a/docs/wiki/Http-Integration.md b/docs/wiki/hosting/Http-Integration.md similarity index 96% rename from docs/wiki/Http-Integration.md rename to docs/wiki/hosting/Http-Integration.md index 8b879340..f4d3c843 100644 --- a/docs/wiki/Http-Integration.md +++ b/docs/wiki/hosting/Http-Integration.md @@ -4,7 +4,7 @@ ZodSharp API returns back into ZodSharp errors. It targets `net10.0` and `net11.0`, references only the base framework (no ASP.NET Core shared framework), and is browser-safe: it is the package a Blazor WebAssembly client, a Blazor Server component, a MAUI app or a service-to-service client uses to talk to an API built with -[ASP.NET Core Integration](AspNetCore-Integration.md). +[ASP.NET Core Integration](../AspNetCore-Integration.md). ```bash dotnet add package Purview.ZodSharp.Http @@ -12,7 +12,7 @@ dotnet add package Purview.ZodSharp.Http Every API takes a `JsonTypeInfo`, typically from a source-generated `JsonSerializerContext`. There are deliberately no `JsonSerializerOptions` overloads, so nothing resolves a contract reflectively; see -[Trimming and Native AOT](Trimming-and-AOT.md). +[Trimming and Native AOT](../quality/Trimming-and-AOT.md). ## Sending @@ -67,7 +67,7 @@ var result = await response.Content.ReadFromJsonAndValidateAsync( ``` `ReadFromJsonAndValidateAsync` deserializes and validates in one pass, exactly like `DeserializeAndValidateAsync` in -[System.Text.Json Integration](SystemTextJson-Integration.md): malformed JSON, a `null` document or an unconvertible +[System.Text.Json Integration](../serialization/SystemTextJson-Integration.md): malformed JSON, a `null` document or an unconvertible value is one `invalid_json` error with the serializer's path, never an exception. A value a strict value object converter rejects (`JsonException(inner: ZodException)`) is reported as that `ZodException`'s errors under the member that held it (`CustomerEmail.Value`), not as `invalid_json`. The body is read as UTF-8, the encoding RFC 8259 requires diff --git a/docs/wiki/Localization.md b/docs/wiki/hosting/Localization.md similarity index 95% rename from docs/wiki/Localization.md rename to docs/wiki/hosting/Localization.md index 7a4443eb..50daae82 100644 --- a/docs/wiki/Localization.md +++ b/docs/wiki/hosting/Localization.md @@ -28,7 +28,7 @@ templates, exactly as for a generated validator. A rule built with a custom mess positional placeholders renamed (`"At least {0}"` becomes `"At least {minimum}"`). Placeholders resolve from the error's `Parameters`, then from its built-in fields. See -[Core Concepts](Core-Concepts.md#error-fields) for the full list of error fields. +[Core Concepts](../concepts/Core-Concepts.md#error-fields) for the full list of error fields. ## `ZodMessageFormat` @@ -166,7 +166,7 @@ Generated validators put the member's display name in the `displayName` paramete - `ErrorMessageResourceType`/`ErrorMessageResourceName` on a DataAnnotations attribute are honoured for the message template. -See [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md). +See [Source Generator DataAnnotations](../generator/Source-Generator-DataAnnotations.md). ## Culture policy @@ -177,7 +177,7 @@ See [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md). | A message rendered by an `IZodMessageFormatter` at a host boundary | `CultureInfo.CurrentUICulture` | Dictionary keys in paths are rendered with the invariant culture (`ZodPathSegment.Key`), so a path never -depends on the thread's culture. See [Core Concepts](Core-Concepts.md#culture). +depends on the thread's culture. See [Core Concepts](../concepts/Core-Concepts.md#culture). ## PII @@ -192,10 +192,10 @@ token or another secret, so: It never serializes raw objects or nested errors. A custom message can still opt in to the value through positional placeholders. See -[Core Concepts](Core-Concepts.md#pii). +[Core Concepts](../concepts/Core-Concepts.md#pii). ## See also -- [ASP.NET Core Integration](AspNetCore-Integration.md) +- [ASP.NET Core Integration](../AspNetCore-Integration.md) - [Microsoft.Extensions.Validation](Microsoft-Extensions-Validation.md) -- [Custom Rules](Custom-Rules.md) +- [Custom Rules](../schemas/Custom-Rules.md) diff --git a/docs/wiki/Microsoft-Extensions-Validation.md b/docs/wiki/hosting/Microsoft-Extensions-Validation.md similarity index 95% rename from docs/wiki/Microsoft-Extensions-Validation.md rename to docs/wiki/hosting/Microsoft-Extensions-Validation.md index 48849cff..21ef8ef4 100644 --- a/docs/wiki/Microsoft-Extensions-Validation.md +++ b/docs/wiki/hosting/Microsoft-Extensions-Validation.md @@ -11,7 +11,7 @@ Microsoft.Extensions.Validation source generator. - References the `Microsoft.Extensions.Validation` package, never the ASP.NET Core shared framework, so it works in a Blazor WebAssembly client and in worker or console hosts. - `Purview.ZodSharp.AspNetCore` depends on it on `net11.0`. That package turns the pipeline's response into the full - ZodSharp ProblemDetails contract (see [ASP.NET Core Integration](AspNetCore-Integration.md)). + ZodSharp ProblemDetails contract (see [ASP.NET Core Integration](../AspNetCore-Integration.md)). ## Install @@ -36,7 +36,7 @@ builder.Services.AddValidation(); - Registers `IZodSchemaFactory` when none is registered yet, as `AddZodSharpFactory()` does. Any registration works: `AddZodSharpFactory`, `AddZodSharp` from the ASP.NET Core package, or the generated - `AddZodSharp{Assembly}Schemas()` (see [Source Generator](Source-Generator.md)). + `AddZodSharp{Assembly}Schemas()` (see [Source Generator](../generator/Source-Generator.md)). - Calls `AddValidation()`. - Registers `ZodValidatableInfoResolver` as a singleton. - Places the resolver in `ValidationOptions.Resolvers` from a post-configure step. The generated @@ -110,7 +110,7 @@ Each ZodSharp error becomes one `Microsoft.Extensions.Validation.ValidationError | `ValidationError` | Value | |---|---| -| `Path` | The canonical ZodSharp path prefixed with `ValidateContext.CurrentValidationPath`: `Customer.Address.Street`, `Lines[0].Sku`, `ByCode[abc].Sku`. See [Core Concepts](Core-Concepts.md#paths). | +| `Path` | The canonical ZodSharp path prefixed with `ValidateContext.CurrentValidationPath`: `Customer.Address.Street`, `Lines[0].Sku`, `ByCode[abc].Sku`. See [Core Concepts](../concepts/Core-Concepts.md#paths). | | `Name` | The failing member's name (`Street`); empty for a model-level error. | | `Container` | The object that owns the member (`order.Customer.Address`). Blazor uses it to place the message on the right field. Never a struct: an error inside a struct member (`Email.Value`) is attributed to the nearest reference-type owner and its member (`signUp`, `Email`), because a value type cannot be a `FieldIdentifier` model. | | `ErrorMessage` | The ZodSharp message, formatted by `MessageFormatter` or localized (see [Localization](Localization.md)). | @@ -192,7 +192,7 @@ The bridge behaves like the platform's own type info: Blazor use `EditContext.ValidateAsync()`. A type has asynchronous rules when its `RequiresAsyncValidation` is `true`. - **Async rules can use services.** Asynchronous validation passes `ValidateContext.ServiceProvider` through to - service-aware rules (see [Source Generator](Source-Generator.md)). + service-aware rules (see [Source Generator](../generator/Source-Generator.md)). - **Field-level validation does not recurse.** It runs only the changed member's own rules (`ValidateMember`). Nested schemas, type-level rules and refinements are not run, because Blazor attributes every error of a field run to the changed field. Members without rules are not offered for field-level validation @@ -211,7 +211,7 @@ A C# union that has no validator of its own is validated through its active case Microsoft.Extensions.Validation does. The case is reported under `Value` (`Value.Name`, `Value[0].Name`) and validated by whichever resolver knows the case type. For a union minimal-API parameter, the runtime parameter resolver that the application's own `AddValidation()` call adds claims the parameter. See -[Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md). +[Unions and Discriminated Unions](../schemas/Unions-and-Discriminated-Unions.md). ## Trim-safe sources @@ -245,13 +245,13 @@ registration (`AddZodSharp{Assembly}Schemas()`) is also trim-safe. Assembly scan - **Models declared in `.razor` files are not generated.** The Razor compiler generates C# from `@code` blocks after source generators run, so the ZodSharp generator never sees a `[ZodSchema]` type declared there. The generator reports `ZODSGEN051`; move the type to a `.cs` file (see - [Source Generator Diagnostics](Source-Generator-Diagnostics.md)). + [Source Generator Diagnostics](../generator/Source-Generator-Diagnostics.md)). - **Lookups are cached.** Register validators before the first validation (see above). ## See also -- [ASP.NET Core Integration](AspNetCore-Integration.md): the minimal-API pipeline and the ProblemDetails contract. +- [ASP.NET Core Integration](../AspNetCore-Integration.md): the minimal-API pipeline and the ProblemDetails contract. - [Blazor Integration](Blazor-Integration.md): forms, field placement and ``. - [OpenAPI Integration](OpenAPI-Integration.md) - [Localization](Localization.md) -- [Core Concepts](Core-Concepts.md#error-fields): the error fields and the canonical path format. +- [Core Concepts](../concepts/Core-Concepts.md#error-fields): the error fields and the canonical path format. diff --git a/docs/wiki/OpenAPI-Integration.md b/docs/wiki/hosting/OpenAPI-Integration.md similarity index 95% rename from docs/wiki/OpenAPI-Integration.md rename to docs/wiki/hosting/OpenAPI-Integration.md index 1e4ff634..68f7e078 100644 --- a/docs/wiki/OpenAPI-Integration.md +++ b/docs/wiki/hosting/OpenAPI-Integration.md @@ -62,7 +62,7 @@ The component ids and the media type are constants: `ZodOpenApiOptions.Validatio ## Source of truth -For each type, the transformer uses ZodSharp's own JSON Schema (see [JSON Schema Export](JsonSchema-Export.md)): +For each type, the transformer uses ZodSharp's own JSON Schema (see [JSON Schema Export](../serialization/JsonSchema-Export.md)): - the generated `BuildJsonSchema()` of a `[ZodSchema]` type (no reflection), or - a runtime schema registered in `IZodSchemaFactory`, converted with `ToJsonSchemaConverter`. @@ -130,7 +130,7 @@ rejects when absent are added to `required`: non-nullable references, `[Required | A runtime `ZodDiscriminatedUnion` | `oneOf` with a `discriminator` whose `propertyName` is the union's discriminator. | With `DiscriminatedUnionsAsOneOf = false`, all three keep `anyOf`. See -[Unions and Discriminated Unions](Unions-and-Discriminated-Unions.md). +[Unions and Discriminated Unions](../schemas/Unions-and-Discriminated-Unions.md). ## Validation problem responses @@ -146,7 +146,7 @@ container, or a C# union whose cases ZodSharp knows. It also lists `422` when an duplicated. Its description is kept, and its `application/problem+json` schema is replaced with the ZodSharp contract. -The two component schemas describe the body that [ASP.NET Core Integration](AspNetCore-Integration.md#the-error-contract) +The two component schemas describe the body that [ASP.NET Core Integration](../AspNetCore-Integration.md#the-error-contract) documents: - `ZodValidationProblemDetails` is RFC 9457 `HttpValidationProblemDetails` plus `issues` (an array of @@ -194,10 +194,10 @@ With `EmitVendorExtensions` on, rules that no standard keyword captures are reco | `x-zodsharp-unsupported` | the names of rules that have no JSON Schema representation (`CidrRule`, `JWTRule`, ...) | The same extensions are available outside OpenAPI from `JsonSchemaDefinition.GetVendorExtensions()`. They never -change the serialized JSON Schema (see [JSON Schema Export](JsonSchema-Export.md)). +change the serialized JSON Schema (see [JSON Schema Export](../serialization/JsonSchema-Export.md)). ## See also -- [ASP.NET Core Integration](AspNetCore-Integration.md) +- [ASP.NET Core Integration](../AspNetCore-Integration.md) - [Microsoft.Extensions.Validation](Microsoft-Extensions-Validation.md) -- [JSON Schema Export](JsonSchema-Export.md) +- [JSON Schema Export](../serialization/JsonSchema-Export.md) diff --git a/docs/wiki/Cross-Platform-Interop.md b/docs/wiki/quality/Cross-Platform-Interop.md similarity index 94% rename from docs/wiki/Cross-Platform-Interop.md rename to docs/wiki/quality/Cross-Platform-Interop.md index a20a927d..c5df10b8 100644 --- a/docs/wiki/Cross-Platform-Interop.md +++ b/docs/wiki/quality/Cross-Platform-Interop.md @@ -43,7 +43,7 @@ Validity is not the only thing that lines up. Every built-in ZodSharp error repo while `Code` keeps ZodSharp's more specific identity, and `FullPath.ToZodPath()` returns the path in Zod's issue shape (strings for members and keys, integers for indexes). Messages use Zod's type names (`"Expected number, received string"`), never CLR names. A client can therefore handle ZodSharp and Zod issues with the same code; -see [Core Concepts](Core-Concepts.md#error-fields). +see [Core Concepts](../concepts/Core-Concepts.md#error-fields). | Zod issue | ZodSharp error | |---|---| @@ -60,14 +60,14 @@ The same interop goal is available without fixtures via JSON Schema: - Export: `Z.ToJsonSchema` (core package) → JSON Schema, or `z.toJSONSchema` on the TypeScript side (Zod v4+). - Import: `Z.FromJsonSchema` (in the System.Text.Json or Newtonsoft.Json package — namespace `ZodSharp.JsonSchema.SystemTextJson` / `ZodSharp.JsonSchema.NewtonsoftJson`). -A generated collection maps to Zod's array element type: `T[]` / `List` ↔ `z.array(T)`, and a nullable element (`T?[]`, `List`, or a property that allows nulls via `[CollectionBehavior(AllowNulls = true)]`) ↔ `z.array(T.nullable())`. A collection whose elements may be null exports `items.anyOf: [ …, { "type": "null" } ]`, and both `z.fromJSONSchema` and `Z.FromJsonSchema` read that shape back. Null elements are rejected by default, so a generated `T[]` matches `z.array(T)` exactly — see [Collection null items](Source-Generator.md#collection-null-items). +A generated collection maps to Zod's array element type: `T[]` / `List` ↔ `z.array(T)`, and a nullable element (`T?[]`, `List`, or a property that allows nulls via `[CollectionBehavior(AllowNulls = true)]`) ↔ `z.array(T.nullable())`. A collection whose elements may be null exports `items.anyOf: [ …, { "type": "null" } ]`, and both `z.fromJSONSchema` and `Z.FromJsonSchema` read that shape back. Null elements are rejected by default, so a generated `T[]` matches `z.array(T)` exactly — see [Collection null items](../generator/Source-Generator.md#collection-null-items). A dictionary property (`IDictionary` / `IReadOnlyDictionary`) is a map: `Dictionary` ↔ `z.record(z.string(), V)`. The generated schema exports `{ "type": "object", "additionalProperties": … }` (with `minProperties`/`maxProperties` for size constraints), and its failures use the same key-prefixed paths as `z.record`. Both `z.fromJSONSchema` and `Z.FromJsonSchema` read that shape back as a record — see -[JSON Schema Export](JsonSchema-Export.md#dictionaries). +[JSON Schema Export](../serialization/JsonSchema-Export.md#dictionaries). The temporal parity suite (`tests/ts/temporal-parity.test.ts`) proves the same loop for `[NonSentinel]` on `DateOnly`, `TimeOnly`, `DateTime`, and `DateTimeOffset` under the default preserve-offset contract, including @@ -75,7 +75,7 @@ the offset-equivalent bounds. The C# producer (`JsonSchema_GivenGeneratedTemporalSchemas_WritesZodParityArtifacts`) writes to `src/tests/cross-platform/temporal-parity/`; the UTC contract has its own generator tests. -See [JSON Schema Export](JsonSchema-Export.md) and [JSON Schema Import](JsonSchema-Import.md). +See [JSON Schema Export](../serialization/JsonSchema-Export.md) and [JSON Schema Import](../serialization/JsonSchema-Import.md). ## Directory layout diff --git a/docs/wiki/Guarantees-and-Limitations.md b/docs/wiki/quality/Guarantees-and-Limitations.md similarity index 93% rename from docs/wiki/Guarantees-and-Limitations.md rename to docs/wiki/quality/Guarantees-and-Limitations.md index 50bc5f39..62bfb1e0 100644 --- a/docs/wiki/Guarantees-and-Limitations.md +++ b/docs/wiki/quality/Guarantees-and-Limitations.md @@ -6,12 +6,12 @@ - **No reflection on hot paths.** The runtime library uses expression trees only in the opt-in `CompiledValidator` and to compile a one-off discriminator accessor per (type, discriminator) pair for `ZodDiscriminatedUnion`. After that first use, validation runs direct property access; the source generator emits direct typed codegen. - **Deterministic, reviewable generated code.** The `[ZodSchema]` generator output is stable and de-duplicated; there are no scope leaks in emitted code. - **Cross-platform parity.** The C# implementation is exercised against TypeScript/Zod fixtures (see [Cross-Platform Interop](Cross-Platform-Interop.md)). Array item nullability lines up with Zod: a generated `T[]` rejects null elements like `z.array(T)`, and a nullable element exports the same `items.anyOf` null branch that `z.array(T.nullable())` does. Error `Category` values are Zod's issue codes and `FullPath.ToZodPath()` is Zod's issue path, so the parity extends from validity to the issue shape and on to ProblemDetails keys. -- **One path format.** Every error carries a typed `FullPath`, and every host renders it with the one renderer, `ZodPath.ToDottedString()`: members joined with `.`, indexes bracketed (`Items[0].Name`), keys bracketed and quoted when ambiguous (`Map[en]`, `Map["0"]`, `Map["a.b"]`), the root as `""`. Runtime schemas, generated validators, JSON integrations and JSON Schema import produce the same path for the same location, keys are rendered with the invariant culture, and member names default to C# names (matching Microsoft.Extensions.Validation). See [Core Concepts: Paths](Core-Concepts.md#paths). +- **One path format.** Every error carries a typed `FullPath`, and every host renders it with the one renderer, `ZodPath.ToDottedString()`: members joined with `.`, indexes bracketed (`Items[0].Name`), keys bracketed and quoted when ambiguous (`Map[en]`, `Map["0"]`, `Map["a.b"]`), the root as `""`. Runtime schemas, generated validators, JSON integrations and JSON Schema import produce the same path for the same location, keys are rendered with the invariant culture, and member names default to C# names (matching Microsoft.Extensions.Validation). See [Core Concepts: Paths](../concepts/Core-Concepts.md#paths). - **One error identity per failure.** The same failure reports the same `Code`, `Category` (the Zod v4 issue code), `Origin`, bounds and message template whichever producer reports it. Re-pathing (`WithPathPrefix`) keeps every field. -- **One ProblemDetails contract.** The endpoint filter, `ZodExceptionHandler`, `ZodTypedResults`, the `ToHttpValidationProblemDetails`/`ToValidationProblem` helpers (the overloads that take the request or options; the obsolete option-less 2.x overloads use built-in defaults) and (on .NET 11) the Microsoft.Extensions.Validation pipeline all write through `ZodProblemDetailsWriter`, so the same errors produce the same body - keys, `issues`, status - byte for byte apart from `traceId`. See [ASP.NET Core Integration](AspNetCore-Integration.md). -- **Culture-independent validation.** Codes, categories, origins, parameters, path keys, bounds and the messages produced at validation time are invariant. Only a host boundary applies a culture, through an `IZodMessageFormatter` in `CultureInfo.CurrentUICulture`. See [Core Concepts: Culture](Core-Concepts.md#culture) and [Localization](Localization.md). +- **One ProblemDetails contract.** The endpoint filter, `ZodExceptionHandler`, `ZodTypedResults`, the `ToHttpValidationProblemDetails`/`ToValidationProblem` helpers (the overloads that take the request or options; the obsolete option-less 2.x overloads use built-in defaults) and (on .NET 11) the Microsoft.Extensions.Validation pipeline all write through `ZodProblemDetailsWriter`, so the same errors produce the same body - keys, `issues`, status - byte for byte apart from `traceId`. See [ASP.NET Core Integration](../AspNetCore-Integration.md). +- **Culture-independent validation.** Codes, categories, origins, parameters, path keys, bounds and the messages produced at validation time are invariant. Only a host boundary applies a culture, through an `IZodMessageFormatter` in `CultureInfo.CurrentUICulture`. See [Core Concepts: Culture](../concepts/Core-Concepts.md#culture) and [Localization](../hosting/Localization.md). - **Messages never throw.** Messages are formatted by `ZodMessageFormat`, which writes unknown placeholders and stray braces literally, so a `{` in a custom message can no longer turn a validation failure into a `FormatException` (at build time in the generator, or at validation time). -- **No string input in messages by default.** Built-in messages never quote string input (numeric rules state the failing number, size rules the count), and the `received` parameter holds only the input's type name unless `ZodErrorOptions.IncludeReceivedValues` is enabled (development only). See [Core Concepts: PII](Core-Concepts.md#pii). +- **No string input in messages by default.** Built-in messages never quote string input (numeric rules state the failing number, size rules the count), and the `received` parameter holds only the input's type name unless `ZodErrorOptions.IncludeReceivedValues` is enabled (development only). See [Core Concepts: PII](../concepts/Core-Concepts.md#pii). - **No stack overflow on cyclic graphs.** Generated validators stop at `ZodValidationDepth.MaxDepth` (default 32, as Microsoft.Extensions.Validation) with an `InvalidOperationException`. - **Multi-targeting.** Packages target `net10.0` and `net11.0` (the .NET 11 integrations - `Purview.ZodSharp.Extensions.Validation`, `Purview.ZodSharp.AspNetCore.OpenApi` and `Purview.ZodSharp.Blazor` - target `net11.0` only); the source generator targets `netstandard2.0` so it runs in any compiler host. - **A fully built schema is safe to cache and share across threads.** Validation only reads the rule set and `Description`, so once construction is finished a schema can be reused concurrently. Building is *not* immutable — see the next section. @@ -36,7 +36,7 @@ The generator reports `Origin = "string"` for string size failures, `Origin = "r ### Rule messages are not templates -Rule failures report their rendered `Message`, but a rule's `MessageFormat` constant uses positional placeholders (`{0}`, `{1}`), so a runtime rule failure carries `MessageFormat = null` unless the rule reports a named template through `IZodRule.MessageFormat`. Schema-level errors (`invalid_type`, `missing_field`, `invalid_union`, array sizes, …) carry their named template. A host formatter that re-renders by template therefore falls back to `Message` for those rule failures; a localizer can still key on `Code` (see [Localization](Localization.md)). +Rule failures report their rendered `Message`, but a rule's `MessageFormat` constant uses positional placeholders (`{0}`, `{1}`), so a runtime rule failure carries `MessageFormat = null` unless the rule reports a named template through `IZodRule.MessageFormat`. Schema-level errors (`invalid_type`, `missing_field`, `invalid_union`, array sizes, …) carry their named template. A host formatter that re-renders by template therefore falls back to `Message` for those rule failures; a localizer can still key on `Code` (see [Localization](../hosting/Localization.md)). ### Typed union allocation @@ -70,7 +70,7 @@ offset-bearing `DateTime` into the receiver's time zone, so that form is not por `Utc`, or `DateTimeOffset` (whose sentinel pattern is exact) for a cross-platform contract. `DateTimeOffsetSerialization.Utc` is an optional narrower contract that requires a zero offset and a converter that rejects non-UTC input on read; see -[JSON Schema Export](JsonSchema-Export.md#temporal-sentinels-and-the-datetimeoffset-wire-form). +[JSON Schema Export](../serialization/JsonSchema-Export.md#temporal-sentinels-and-the-datetimeoffset-wire-form). `TimeOnly` is exported as a local-time `pattern` instead of `format: time`. JSON Schema's `format: time` is an RFC 3339 full-time and requires an offset, while System.Text.Json writes `TimeOnly` as a local @@ -86,9 +86,9 @@ RFC 3339 full-time and requires an offset, while System.Text.Json writes `TimeOn ## Custom rules -Custom rules and their DataAnnotations-style attributes are a first-class extension point. Rules can be attached to a property or to the schema type itself (validating the value object as a unit), and a generic rule can be closed with the target type so one rule serves every scalar of a given shape. A rule written against a `Purview.ValueObjects` scalar's underlying value is adapted automatically when it is applied to a `[Scalar]` type, so one rule also serves every scalar backed by the same primitive. See [Custom Rules](Custom-Rules.md) for the rule contract, the public `AddRule`/`Rule` API, and how to map a rule to a `ValidationAttribute` that the source generator honours, and [Value Objects Integration](Value-Objects-Integration.md) for the `[Scalar]` walkthrough and error code / message definitions. +Custom rules and their DataAnnotations-style attributes are a first-class extension point. Rules can be attached to a property or to the schema type itself (validating the value object as a unit), and a generic rule can be closed with the target type so one rule serves every scalar of a given shape. A rule written against a `Purview.ValueObjects` scalar's underlying value is adapted automatically when it is applied to a `[Scalar]` type, so one rule also serves every scalar backed by the same primitive. See [Custom Rules](../schemas/Custom-Rules.md) for the rule contract, the public `AddRule`/`Rule` API, and how to map a rule to a `ValidationAttribute` that the source generator honours, and [Value Objects Integration](../schemas/Value-Objects-Integration.md) for the `[Scalar]` walkthrough and error code / message definitions. ## Contract vs. underlying libraries - `System.ComponentModel.DataAnnotations` semantics are honoured where documented — e.g. `[Length]` treats `null` as valid unless `[Required]` is present; `[RegularExpression]` runs only on non-empty strings. -- Validation failure payloads map to `ProblemDetails`/`HttpValidationProblemDetails` in the ASP.NET Core package (see [ASP.NET Core Integration](AspNetCore-Integration.md)). \ No newline at end of file +- Validation failure payloads map to `ProblemDetails`/`HttpValidationProblemDetails` in the ASP.NET Core package (see [ASP.NET Core Integration](../AspNetCore-Integration.md)). \ No newline at end of file diff --git a/docs/wiki/Performance.md b/docs/wiki/quality/Performance.md similarity index 95% rename from docs/wiki/Performance.md rename to docs/wiki/quality/Performance.md index 0cdc1cc6..f128d6c5 100644 --- a/docs/wiki/Performance.md +++ b/docs/wiki/quality/Performance.md @@ -156,8 +156,8 @@ The char-scan is ~20% faster than the previous regex on the valid path, is versi 1. **Struct-based rules** — every rule is a `readonly record struct` implementing `IValidationRule`, so there is no per-validation object allocation. 2. **Span-aware helpers** — `Span`/`ReadOnlySpan` APIs and `ArrayPool`-backed helpers. The shipped string rules implement `IStringValidationRule` (except the URL and Base64 rules, which need a string), so `ZodString.ValidateSpan` validates the span directly and only materialises the value string for its result; `ZodString.IsValidSpan` avoids that allocation entirely. -3. **Compiled validators** — `CompiledValidator.Compile` removes interface dispatch (see [Compiled Validators and Caching](Compiled-Validators-and-Caching.md)). -4. **Source generation** — `[ZodSchema]` emits direct property access and typed equality checks with no reflection (see [Source Generator](Source-Generator.md)). -5. **Cacheable schema instances** — a fully built schema is safe to cache and share, so `SchemaCache` avoids repeated construction (see [Compiled Validators and Caching](Compiled-Validators-and-Caching.md)). Building is not immutable: the fluent rule methods mutate the receiver. See [Custom Rules](Custom-Rules.md) for extending rules. +3. **Compiled validators** — `CompiledValidator.Compile` removes interface dispatch (see [Compiled Validators and Caching](../schemas/Compiled-Validators-and-Caching.md)). +4. **Source generation** — `[ZodSchema]` emits direct property access and typed equality checks with no reflection (see [Source Generator](../generator/Source-Generator.md)). +5. **Cacheable schema instances** — a fully built schema is safe to cache and share, so `SchemaCache` avoids repeated construction (see [Compiled Validators and Caching](../schemas/Compiled-Validators-and-Caching.md)). Building is not immutable: the fluent rule methods mutate the receiver. See [Custom Rules](../schemas/Custom-Rules.md) for extending rules. The allocations on a successful validation are limited to the string transforms (`ToLower`/`ToUpper`/`Trim`), `ZodString.ValidateSpan` (its result carries a string), and the union non-first-option paths. \ No newline at end of file diff --git a/docs/wiki/Trimming-and-AOT.md b/docs/wiki/quality/Trimming-and-AOT.md similarity index 97% rename from docs/wiki/Trimming-and-AOT.md rename to docs/wiki/quality/Trimming-and-AOT.md index 75fcf03d..5c3a3bc1 100644 --- a/docs/wiki/Trimming-and-AOT.md +++ b/docs/wiki/quality/Trimming-and-AOT.md @@ -155,7 +155,7 @@ it. Revisit it in the next major version, where the type shape can change. ## Related -- [Source Generator](Source-Generator.md) — generated registration and validators. -- [Source Generator Diagnostics](Source-Generator-Diagnostics.md) — `ZODSGEN053`. -- [System.Text.Json Integration](SystemTextJson-Integration.md) — the `JsonTypeInfo` overloads. +- [Source Generator](../generator/Source-Generator.md) — generated registration and validators. +- [Source Generator Diagnostics](../generator/Source-Generator-Diagnostics.md) — `ZODSGEN053`. +- [System.Text.Json Integration](../serialization/SystemTextJson-Integration.md) — the `JsonTypeInfo` overloads. - [Guarantees and Limitations](Guarantees-and-Limitations.md). diff --git a/docs/wiki/Arrays-and-Other-Schemas.md b/docs/wiki/schemas/Arrays-and-Other-Schemas.md similarity index 96% rename from docs/wiki/Arrays-and-Other-Schemas.md rename to docs/wiki/schemas/Arrays-and-Other-Schemas.md index df05c452..7a17fab2 100644 --- a/docs/wiki/Arrays-and-Other-Schemas.md +++ b/docs/wiki/schemas/Arrays-and-Other-Schemas.md @@ -91,7 +91,7 @@ var schema = Z.Record(Z.Number()); A null value is the value schema's decision: `Z.Record(Z.String())` rejects it, while `Z.Record(Z.Optional(Z.String()))` accepts it. The schema exports and imports as `{ "type": "object", "additionalProperties": … }` (see -[JSON Schema Export](JsonSchema-Export.md#dictionaries)), and a generated `Dictionary` +[JSON Schema Export](../serialization/JsonSchema-Export.md#dictionaries)), and a generated `Dictionary` property is validated the same way. ## Tuple @@ -134,4 +134,4 @@ optional.Validate(null); // Success optional.Validate("value"); // Success ``` -For a generated collection, element nullability follows the element type: `T[]`/`List` rejects `null` elements, while `T?[]`/`List` accepts them — the same distinction as `z.array(T)` versus `z.array(T.nullable())`. A dictionary property (`IDictionary<,>`/`IReadOnlyDictionary<,>`) is a map instead: values are validated like elements, and it maps to `z.record(z.string(), V)`. See [Collection null items](Source-Generator.md#collection-null-items). \ No newline at end of file +For a generated collection, element nullability follows the element type: `T[]`/`List` rejects `null` elements, while `T?[]`/`List` accepts them — the same distinction as `z.array(T)` versus `z.array(T.nullable())`. A dictionary property (`IDictionary<,>`/`IReadOnlyDictionary<,>`) is a map instead: values are validated like elements, and it maps to `z.record(z.string(), V)`. See [Collection null items](../generator/Source-Generator.md#collection-null-items). \ No newline at end of file diff --git a/docs/wiki/Compiled-Validators-and-Caching.md b/docs/wiki/schemas/Compiled-Validators-and-Caching.md similarity index 95% rename from docs/wiki/Compiled-Validators-and-Caching.md rename to docs/wiki/schemas/Compiled-Validators-and-Caching.md index 9311605e..3b8e7dba 100644 --- a/docs/wiki/Compiled-Validators-and-Caching.md +++ b/docs/wiki/schemas/Compiled-Validators-and-Caching.md @@ -58,7 +58,7 @@ var schema = SchemaCache.GetOrCreate("user", () => A **fully built** schema is safe to cache and share across threads: validation only reads the rule set and the description. But building a schema is **not** immutable — the fluent rule methods mutate the receiver -in place (see [Guarantees and Limitations](Guarantees-and-Limitations.md#fluent-rule-methods-mutate-the-receiver)). +in place (see [Guarantees and Limitations](../quality/Guarantees-and-Limitations.md#fluent-rule-methods-mutate-the-receiver)). So finish configuring a schema *before* putting it in the cache, and never apply a fluent rule method to a schema you retrieved from it: that mutates the instance every other caller is sharing. @@ -77,4 +77,4 @@ when you genuinely need lookup by name. ## The source generator alternative For the highest performance, prefer the compile-time source generator: `[ZodSchema]` emits a static -validator with no runtime compilation or dispatch overhead. See [Source Generator](Source-Generator.md). +validator with no runtime compilation or dispatch overhead. See [Source Generator](../generator/Source-Generator.md). diff --git a/docs/wiki/Custom-Rules.md b/docs/wiki/schemas/Custom-Rules.md similarity index 97% rename from docs/wiki/Custom-Rules.md rename to docs/wiki/schemas/Custom-Rules.md index 43e68c49..f0c9a80f 100644 --- a/docs/wiki/Custom-Rules.md +++ b/docs/wiki/schemas/Custom-Rules.md @@ -39,11 +39,11 @@ public readonly record struct EmailRule : IValidationRule ``` - **`ErrorCode`** is the rule's canonical code — the value a test compares against when no per-usage override is supplied. The interface member `IValidationRule.Code` defaults to `"validation_failed"`; a rule that accepts a per-usage `code` override returns it (falling back to `ErrorCode`) from both `Code` and `IZodRule.Code`, so every route reports the same effective value. -- **`MessageFormat`** is a positional template. `{0}` (and `{1}`, …) are the offending value and any rule-specific arguments. Format it with `ZodSharp.Rules.RuleMessage.Format(format, …)`, which uses the invariant culture (messages are culture-independent; hosts localize at the boundary - see [Localization](Localization.md)), caches the parsed format and **never throws**: a custom message with a stray brace or an index you do not supply is written out literally instead of raising `FormatException` at validation time. -- **Keep the input out of the default message.** Errors leave the process (ProblemDetails, logs, UI), so a default `MessageFormat` should describe what was expected rather than quote the value (`"Invalid email format"`, not `"Invalid email format: {0}"`). The built-in rules follow this; see [Core Concepts: PII](Core-Concepts.md#pii). +- **`MessageFormat`** is a positional template. `{0}` (and `{1}`, …) are the offending value and any rule-specific arguments. Format it with `ZodSharp.Rules.RuleMessage.Format(format, …)`, which uses the invariant culture (messages are culture-independent; hosts localize at the boundary - see [Localization](../hosting/Localization.md)), caches the parsed format and **never throws**: a custom message with a stray brace or an index you do not supply is written out literally instead of raising `FormatException` at validation time. +- **Keep the input out of the default message.** Errors leave the process (ProblemDetails, logs, UI), so a default `MessageFormat` should describe what was expected rather than quote the value (`"Invalid email format"`, not `"Invalid email format: {0}"`). The built-in rules follow this; see [Core Concepts: PII](../concepts/Core-Concepts.md#pii). - The constants may be inherited from a base rule class, and **abstract bases are exempt**, so a shared base can host them for its concrete derivations. -The convention is enforced by an analyzer: a source-declared rule that does not expose a public `const string ErrorCode` **and** a public `const string MessageFormat` is reported as **ZODSGEN042**. See [Source Generator Diagnostics](Source-Generator-Diagnostics.md) for the full list. +The convention is enforced by an analyzer: a source-declared rule that does not expose a public `const string ErrorCode` **and** a public `const string MessageFormat` is reported as **ZODSGEN042**. See [Source Generator Diagnostics](../generator/Source-Generator-Diagnostics.md) for the full list. A `[Test]` can therefore assert without duplicating strings: @@ -199,7 +199,7 @@ var result = schema.Validate("John Doe"); // result.Errors[0].Path is empty ``` -Both methods mutate the receiver and return it for chaining; see [Guarantees and Limitations](Guarantees-and-Limitations.md#fluent-rule-methods-mutate-the-receiver). +Both methods mutate the receiver and return it for chaining; see [Guarantees and Limitations](../quality/Guarantees-and-Limitations.md#fluent-rule-methods-mutate-the-receiver). ## Extending the fluent interface @@ -644,13 +644,13 @@ See [Value Objects Integration](Value-Objects-Integration.md) for the full walkt | ZODSGEN042 | Warning | A validation rule does not expose public `const string ErrorCode` / `MessageFormat` constants, so its error identity cannot be asserted in tests without duplicating literals. | | ZODSGEN043 | Info | A built-in rule marked `[ZodRule]` does not generate a validation attribute (its name is taken by a `System.ComponentModel.DataAnnotations` attribute, or a constructor parameter cannot be represented as an attribute property). | -See [Source Generator Diagnostics](Source-Generator-Diagnostics.md) for the full list. +See [Source Generator Diagnostics](../generator/Source-Generator-Diagnostics.md) for the full list. ## Related - [Validation Rules Reference](Validation-Rules-Reference.md) — the catalogue of built-in rules. -- [Fluent Schema API](Fluent-Schema-API.md) — `AddRule`/`Rule` live on `ZodType`. +- [Fluent Schema API](../Fluent-Schema-API.md) — `AddRule`/`Rule` live on `ZodType`. - [Value Objects Integration](Value-Objects-Integration.md) — `[Scalar]` value objects and the `ScalarRuleAdapter`. -- [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) — built-in attribute coverage. -- [Guarantees and Limitations](Guarantees-and-Limitations.md) — allocation and mutation semantics. +- [Source Generator DataAnnotations](../generator/Source-Generator-DataAnnotations.md) — built-in attribute coverage. +- [Guarantees and Limitations](../quality/Guarantees-and-Limitations.md) — allocation and mutation semantics. diff --git a/docs/wiki/Number-Validation.md b/docs/wiki/schemas/Number-Validation.md similarity index 100% rename from docs/wiki/Number-Validation.md rename to docs/wiki/schemas/Number-Validation.md diff --git a/docs/wiki/Object-Validation.md b/docs/wiki/schemas/Object-Validation.md similarity index 95% rename from docs/wiki/Object-Validation.md rename to docs/wiki/schemas/Object-Validation.md index 5e57a2a6..c9b06c25 100644 --- a/docs/wiki/Object-Validation.md +++ b/docs/wiki/schemas/Object-Validation.md @@ -31,7 +31,7 @@ var result = userSchema.Validate(new Dictionary Object errors use member segments for field keys, so `FullPath.ToDottedString()` renders `address.street`, and a nested array element `items[0].name`. An object key that contains `.`, brackets, quotes or whitespace is quoted -(`["a.b"]`). See [Core Concepts: Paths](Core-Concepts.md#paths) and [Error fields](Core-Concepts.md#error-fields). +(`["a.b"]`). See [Core Concepts: Paths](../concepts/Core-Concepts.md#paths) and [Error fields](../concepts/Core-Concepts.md#error-fields). ## Unknown key policies @@ -85,4 +85,4 @@ These return a **new** `ZodObject` instance: ## Builder -`ZodObjectBuilder` validates its arguments: a null/whitespace field name or a null schema throws `ArgumentNullException`. Typed fields are wrapped so boxed values coerce correctly (see [Core Concepts](Core-Concepts.md)). \ No newline at end of file +`ZodObjectBuilder` validates its arguments: a null/whitespace field name or a null schema throws `ArgumentNullException`. Typed fields are wrapped so boxed values coerce correctly (see [Core Concepts](../concepts/Core-Concepts.md)). \ No newline at end of file diff --git a/docs/wiki/String-Validation.md b/docs/wiki/schemas/String-Validation.md similarity index 100% rename from docs/wiki/String-Validation.md rename to docs/wiki/schemas/String-Validation.md diff --git a/docs/wiki/Unions-and-Discriminated-Unions.md b/docs/wiki/schemas/Unions-and-Discriminated-Unions.md similarity index 92% rename from docs/wiki/Unions-and-Discriminated-Unions.md rename to docs/wiki/schemas/Unions-and-Discriminated-Unions.md index 1e6ca7ec..4eb5b02d 100644 --- a/docs/wiki/Unions-and-Discriminated-Unions.md +++ b/docs/wiki/schemas/Unions-and-Discriminated-Unions.md @@ -31,7 +31,7 @@ option in declaration order, with paths relative to the union. It is stored unde errors). ProblemDetails projects the union error at its own path, with `unionErrors` as nested issues. > [!NOTE] -> On the failure path the accumulated option errors are collected, which allocates. Matching a later option (e.g. number or boolean after a string option) also allocates while the earlier options are attempted — see [Performance](Performance.md). +> On the failure path the accumulated option errors are collected, which allocates. Matching a later option (e.g. number or boolean after a string option) also allocates while the earlier options are attempted — see [Performance](../quality/Performance.md). ## Typed union @@ -111,7 +111,7 @@ Failures are reported at the discriminator's path, with category `invalid_union` | `null` input | `invalid_type` | `Expected object, received null` (at the root) | Once an option is selected, its own errors are reported unchanged (for example `invalid_type` at `level`). -Discriminated unions export to JSON Schema as `oneOf` (see [JSON Schema Export](JsonSchema-Export.md)). +Discriminated unions export to JSON Schema as `oneOf` (see [JSON Schema Export](../serialization/JsonSchema-Export.md)). The builder (`ZodDiscriminatedUnionBuilder`) accepts untyped `IZodSchema` options via `Option(string value, IZodSchema schema)` and typed options via `Option(string value, IZodSchema schema)`, which wrap the schema for coercion and `null` handling. @@ -122,20 +122,20 @@ dispatching to the active case's schema, and a member typed as a C# 15 `closed` derived type's schema. Paths do not include the union's `Value`: an error in the `Cat` case of a member `public Pet Pet { get; set; }` is reported at `Pet.Name`, not `Pet.Value.Name`. Every derived type of a closed hierarchy needs its own `[ZodSchema]`; one without is reported as `ZODSGEN049` (see -[Source Generator Diagnostics](Source-Generator-Diagnostics.md)). +[Source Generator Diagnostics](../generator/Source-Generator-Diagnostics.md)). On .NET 11 the hosts understand unions too: - **Minimal APIs.** A JSON-body parameter typed as a union is validated through its active case, both by the Microsoft.Extensions.Validation pipeline (`AddZodSharpValidation()` claims union parameters without an application `AddValidation()` call) and by `WithZodSharpValidation()`. See - [Microsoft.Extensions.Validation](Microsoft-Extensions-Validation.md) and - [ASP.NET Core Integration](AspNetCore-Integration.md). + [Microsoft.Extensions.Validation](../hosting/Microsoft-Extensions-Validation.md) and + [ASP.NET Core Integration](../AspNetCore-Integration.md). - **Blazor.** A form model property of union type resolves its field through the case object, so messages - land next to the right input. See [Blazor Integration](Blazor-Integration.md). + land next to the right input. See [Blazor Integration](../hosting/Blazor-Integration.md). - **OpenAPI.** A C# union keeps the platform's `anyOf` shape and gets each case constrained; polymorphic (`[JsonPolymorphic]`) types are described with `oneOf` and a `discriminator`. See - [OpenAPI Integration](OpenAPI-Integration.md). + [OpenAPI Integration](../hosting/OpenAPI-Integration.md). ## Intersection diff --git a/docs/wiki/Validation-Rules-Reference.md b/docs/wiki/schemas/Validation-Rules-Reference.md similarity index 97% rename from docs/wiki/Validation-Rules-Reference.md rename to docs/wiki/schemas/Validation-Rules-Reference.md index aaf82e2b..f8acb867 100644 --- a/docs/wiki/Validation-Rules-Reference.md +++ b/docs/wiki/schemas/Validation-Rules-Reference.md @@ -21,8 +21,8 @@ public interface IValidationRule - Every rule exposes its error identity as public constants: `public const string ErrorCode` and `public const string MessageFormat` (a `{0}`-style positional template). `Code` returns the rule's effective code — a per-usage `code` override when supplied, otherwise `ErrorCode` — and `GetErrorMessage` formats `MessageFormat` with `RuleMessage.Format` (which caches the parsed `CompositeFormat`) using the invariant culture. - A constructor `message` parameter, when supplied, overrides the formatted `MessageFormat`. `MessageFormat` is the fallback, not the only message; a rule built without a message and `default(TRule)` behave the same. A custom message may use the rule's positional placeholders, and formatting **never throws**: a stray brace or an index the rule does not supply is written out literally. - Default messages do not quote string input: format, prefix/suffix/substring, compare, allowed/denied and null-or-non-whitespace rules describe what was expected (`"String must start with 'PUR-'"`). Numeric rules still state the failing number and length rules the length. `RegexRule`'s `{0}` is the pattern. -- Every built-in rule implements `IZodRule`, so its failure carries a full structured identity: `Origin` (`string`, `number`, `date`, `enum`, …), `Category` (the Zod issue code: `invalid_format` for string formats, `too_small`/`too_big` by direction for bound, length and range rules, `not_multiple_of`, `invalid_type` for `Int`, `invalid_value` for value lists), `Lower`/`Upper` bounds with the correct inclusivity (`GreaterThan`/`LessThan` are exclusive) and parameters such as `format`, `pattern`, `prefix`, `suffix`, `includes`, `divisor`, `options`, `version`, `expected` and `count`. See [Core Concepts: Error fields](Core-Concepts.md#error-fields). -- The analyzer reports `ZODSGEN042` when a source-declared rule omits `ErrorCode`/`MessageFormat`. See [Source Generator Diagnostics](Source-Generator-Diagnostics.md). +- Every built-in rule implements `IZodRule`, so its failure carries a full structured identity: `Origin` (`string`, `number`, `date`, `enum`, …), `Category` (the Zod issue code: `invalid_format` for string formats, `too_small`/`too_big` by direction for bound, length and range rules, `not_multiple_of`, `invalid_type` for `Int`, `invalid_value` for value lists), `Lower`/`Upper` bounds with the correct inclusivity (`GreaterThan`/`LessThan` are exclusive) and parameters such as `format`, `pattern`, `prefix`, `suffix`, `includes`, `divisor`, `options`, `version`, `expected` and `count`. See [Core Concepts: Error fields](../concepts/Core-Concepts.md#error-fields). +- The analyzer reports `ZODSGEN042` when a source-declared rule omits `ErrorCode`/`MessageFormat`. See [Source Generator Diagnostics](../generator/Source-Generator-Diagnostics.md). Rules can be used three ways: @@ -184,7 +184,7 @@ No default number message contains the input. Each rule still passes the value t | `NonSentinelRule` | `(string? message)` | `NonSentinel()` | `invalid_value` | `Value has a sentinel value, but got {0}` | | `NullOrNonWhiteSpaceRule` | `(string? message, string? code)` | — | `invalid_string` | `Value must be null or non-whitespace` | -`NonSentinelRule` rejects the framework default/boundary values an ORM commonly stores to mean "no value": `Guid.Empty`; `DateTime`, `DateTimeOffset`, `DateOnly`, and `TimeOnly` `MinValue`/`MaxValue`; and `null`/empty/whitespace strings. Types without a known sentinel always pass, so the rule never rejects a type it does not understand. It detects sentinels with a `typeof(T)` dispatch and reinterprets the value in place, so no boxing occurs. The `{0}` argument is a **description** of the sentinel (`an empty GUID`, `DateTime.MinValue`, `an empty string`, …) rather than the raw value, and a type-level `[NonSentinel]` on a `[Scalar]` type replaces the message's `Value` subject with the scalar's name — see [Value Objects Integration](Value-Objects-Integration.md#message-overrides). Its JSON Schema mapping is exact for `DateOnly`, `TimeOnly`, the deterministic `DateTime` wire forms, and `DateTimeOffset` (the default pattern is offset-aware); the optional UTC contract narrows the accepted offset — see [JSON Schema Export](JsonSchema-Export.md#temporal-sentinels-and-the-datetimeoffset-wire-form). +`NonSentinelRule` rejects the framework default/boundary values an ORM commonly stores to mean "no value": `Guid.Empty`; `DateTime`, `DateTimeOffset`, `DateOnly`, and `TimeOnly` `MinValue`/`MaxValue`; and `null`/empty/whitespace strings. Types without a known sentinel always pass, so the rule never rejects a type it does not understand. It detects sentinels with a `typeof(T)` dispatch and reinterprets the value in place, so no boxing occurs. The `{0}` argument is a **description** of the sentinel (`an empty GUID`, `DateTime.MinValue`, `an empty string`, …) rather than the raw value, and a type-level `[NonSentinel]` on a `[Scalar]` type replaces the message's `Value` subject with the scalar's name — see [Value Objects Integration](Value-Objects-Integration.md#message-overrides). Its JSON Schema mapping is exact for `DateOnly`, `TimeOnly`, the deterministic `DateTime` wire forms, and `DateTimeOffset` (the default pattern is offset-aware); the optional UTC contract narrows the accepted offset — see [JSON Schema Export](../serialization/JsonSchema-Export.md#temporal-sentinels-and-the-datetimeoffset-wire-form). `NullOrNonWhiteSpaceRule` is the null-tolerant counterpart: it accepts `null` but rejects an empty or whitespace-only string. It exists for nullable scalar value objects, which round-trip JSON `null`, so their schema must accept `null` while still rejecting whitespace — `[RequiredZod]` always rejects `null` and so cannot express it. Apply it to a nullable scalar type with `[NullOrNonWhiteSpace]`; a rule written against the underlying `string?` value is adapted to the value object automatically. @@ -230,7 +230,7 @@ These rules mirror `System.ComponentModel.DataAnnotations` attributes as first-c `EnumRule` is generic over `TEnum : struct, Enum` and rejects a value that is not a defined member of the enum type, or that resolves to a member the rule excludes. It is equivalent to Zod's `z.nativeEnum(Enum)` semantics. The defined members are resolved once per closed generic type, so the validation path performs no reflection. -The `[ZodSchema]` generator emits this rule automatically for every non-flags enum property — see [Source Generator](Source-Generator.md#automatic-enum-validation). The rule is closed with the property's enum type and receives the excluded members as its disallowed set: +The `[ZodSchema]` generator emits this rule automatically for every non-flags enum property — see [Source Generator](../generator/Source-Generator.md#automatic-enum-validation). The rule is closed with the property's enum type and receives the excluded members as its disallowed set: - every enum member marked `[ZodIgnore]` (a member that is defined but never a valid value, such as `Unspecified`), and - the values the property's `[DeniedValues]` attribute lists. @@ -342,12 +342,12 @@ The `[ZodSchema]` generator maps several built-in rules to their `System.Compone | `[Base64String]` | `Base64StringRule` | `invalid_string` | | `[RegularExpression]` | compiled `Regex`, not `RegexRule` | `invalid_string` | -Size and range attributes (`[Length]`, `[StringLength]`, `[MinLength]`, `[MaxLength]`, `[Range]`) are emitted as direct, typed codegen; the equivalent rule structs (`LengthRule`, `StringLengthRule`, `MinLengthRule`, `MaxLengthRule`, `RangeRule`) are also available directly and through their generated attributes. The rule-identity variants of the colliding names are the suffixed [generated attributes](#generated-attributes) (`[MinLengthZod]`, `[MaxLengthZod]`, `[UrlZod]`, `[PhoneZod]`, `[CreditCardZod]`, `[Base64StringZod]`, `[RequiredZod]`, `[RangeZod]`, `[LengthZod]`, `[StringLengthZod]`, `[CompareZod]`, `[AllowedValuesZod]`, `[DeniedValuesZod]`), which report the rule's own `Code` and accept a `Message`. The remaining built-in rules are covered by their own generated attributes — for example `[Regex("…")]` validates through `RegexRule`, whereas `[RegularExpression]` compiles the pattern inline. See [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) for the full attribute table and the generated metadata, and [Custom Rules](Custom-Rules.md) for mapping your own rules to attributes. +Size and range attributes (`[Length]`, `[StringLength]`, `[MinLength]`, `[MaxLength]`, `[Range]`) are emitted as direct, typed codegen; the equivalent rule structs (`LengthRule`, `StringLengthRule`, `MinLengthRule`, `MaxLengthRule`, `RangeRule`) are also available directly and through their generated attributes. The rule-identity variants of the colliding names are the suffixed [generated attributes](#generated-attributes) (`[MinLengthZod]`, `[MaxLengthZod]`, `[UrlZod]`, `[PhoneZod]`, `[CreditCardZod]`, `[Base64StringZod]`, `[RequiredZod]`, `[RangeZod]`, `[LengthZod]`, `[StringLengthZod]`, `[CompareZod]`, `[AllowedValuesZod]`, `[DeniedValuesZod]`), which report the rule's own `Code` and accept a `Message`. The remaining built-in rules are covered by their own generated attributes — for example `[Regex("…")]` validates through `RegexRule`, whereas `[RegularExpression]` compiles the pattern inline. See [Source Generator DataAnnotations](../generator/Source-Generator-DataAnnotations.md) for the full attribute table and the generated metadata, and [Custom Rules](Custom-Rules.md) for mapping your own rules to attributes. ## Related - [Custom Rules](Custom-Rules.md) — the `IValidationRule` contract, `IZodRule`, and attribute mapping. - [String Validation](String-Validation.md) — the `ZodString` fluent surface and span validation. - [Number Validation](Number-Validation.md) — the `ZodNumber` fluent surface. -- [Source Generator DataAnnotations](Source-Generator-DataAnnotations.md) — attributes the generator understands. -- [Source Generator Diagnostics](Source-Generator-Diagnostics.md) — `ZODSGEN042` and the rule conventions analyzer. +- [Source Generator DataAnnotations](../generator/Source-Generator-DataAnnotations.md) — attributes the generator understands. +- [Source Generator Diagnostics](../generator/Source-Generator-Diagnostics.md) — `ZODSGEN042` and the rule conventions analyzer. diff --git a/docs/wiki/Value-Objects-Integration.md b/docs/wiki/schemas/Value-Objects-Integration.md similarity index 98% rename from docs/wiki/Value-Objects-Integration.md rename to docs/wiki/schemas/Value-Objects-Integration.md index fa8ffd82..44ae0d59 100644 --- a/docs/wiki/Value-Objects-Integration.md +++ b/docs/wiki/schemas/Value-Objects-Integration.md @@ -367,5 +367,5 @@ Common causes: ## Related - [Custom Rules](Custom-Rules.md) — the rule contract, the public `AddRule`/`Rule` API, rule families, and attribute generation. -- [Source Generator](Source-Generator.md) — the `[ZodSchema]` generator, generated types, and the refinement hook. -- [Source Generator Diagnostics](Source-Generator-Diagnostics.md) — the full `ZODSGEN*` catalog. +- [Source Generator](../generator/Source-Generator.md) — the `[ZodSchema]` generator, generated types, and the refinement hook. +- [Source Generator Diagnostics](../generator/Source-Generator-Diagnostics.md) — the full `ZODSGEN*` catalog. diff --git a/docs/wiki/JsonSchema-Export.md b/docs/wiki/serialization/JsonSchema-Export.md similarity index 98% rename from docs/wiki/JsonSchema-Export.md rename to docs/wiki/serialization/JsonSchema-Export.md index 7f617637..3ff982de 100644 --- a/docs/wiki/JsonSchema-Export.md +++ b/docs/wiki/serialization/JsonSchema-Export.md @@ -72,7 +72,7 @@ if (factory.TryGetUntypedValidator(typeof(Order), out var validator)) } ``` -This is the entry point the OpenAPI transformers use; see [OpenAPI Integration](OpenAPI-Integration.md). +This is the entry point the OpenAPI transformers use; see [OpenAPI Integration](../hosting/OpenAPI-Integration.md). ## Export a generated `[ZodSchema]` @@ -106,7 +106,7 @@ apart: property names, nested objects, native enums, nullable fields, required f for null. A rule that rejects an absent value (for example `[RequiredZod]`) makes the field non-null even when its annotation is nullable. - **Collection items** follow the null-items setting (see - [Collection null items](Source-Generator.md#collection-null-items)): a collection whose elements may be null + [Collection null items](../generator/Source-Generator.md#collection-null-items)): a collection whose elements may be null exports `items` wrapped in `anyOf: [ …, { "type": "null" } ]`; otherwise `items` is the element's own schema. - **Scalar value objects** (`[Scalar]`) export as their underlying value's schema — a `Guid` scalar is a canonical 8-4-4-4-12 hexadecimal string (a `pattern`, not Zod's RFC `uuid` format, because C# `Guid` @@ -235,7 +235,7 @@ definition.SetVendorExtension("x-zod-note", "custom"); // the name must start wi They are exposed through methods rather than a property on purpose: the JSON Schema serializers write properties only, so the exported JSON Schema is unchanged. Hosts whose format carries vendor extensions read them - the OpenAPI transformers write them into the document when `ZodOpenApiOptions.EmitVendorExtensions` is -on (see [OpenAPI Integration](OpenAPI-Integration.md)). +on (see [OpenAPI Integration](../hosting/OpenAPI-Integration.md)). ## Temporal sentinels and the DateTimeOffset wire form diff --git a/docs/wiki/JsonSchema-Import.md b/docs/wiki/serialization/JsonSchema-Import.md similarity index 97% rename from docs/wiki/JsonSchema-Import.md rename to docs/wiki/serialization/JsonSchema-Import.md index f7f840b4..e2469630 100644 --- a/docs/wiki/JsonSchema-Import.md +++ b/docs/wiki/serialization/JsonSchema-Import.md @@ -105,4 +105,4 @@ var userSchema = Z.FromJsonSchema(jsonSchemaString); var result = userSchema.Validate(incomingData); ``` -See [Cross-Platform Interop](Cross-Platform-Interop.md) for the repository's fixture-based verification of this loop. \ No newline at end of file +See [Cross-Platform Interop](../quality/Cross-Platform-Interop.md) for the repository's fixture-based verification of this loop. \ No newline at end of file diff --git a/docs/wiki/NewtonsoftJson-Integration.md b/docs/wiki/serialization/NewtonsoftJson-Integration.md similarity index 100% rename from docs/wiki/NewtonsoftJson-Integration.md rename to docs/wiki/serialization/NewtonsoftJson-Integration.md diff --git a/docs/wiki/SystemTextJson-Integration.md b/docs/wiki/serialization/SystemTextJson-Integration.md similarity index 97% rename from docs/wiki/SystemTextJson-Integration.md rename to docs/wiki/serialization/SystemTextJson-Integration.md index 2fc17d1c..9aa75cfa 100644 --- a/docs/wiki/SystemTextJson-Integration.md +++ b/docs/wiki/serialization/SystemTextJson-Integration.md @@ -81,7 +81,7 @@ rules that resolve services run: | `ValidateAndSerializeAsync` | `ValueTask> ValidateAndSerializeAsync(this IZodSchemaValidator validator, T value, Stream output, JsonTypeInfo jsonTypeInfo, IServiceProvider? services, CancellationToken cancellationToken = default)` | The package is browser-safe (Blazor WebAssembly). For `HttpClient` bodies and server validation problems, see -[HttpClient Integration](Http-Integration.md). +[HttpClient Integration](../hosting/Http-Integration.md). ## Failure codes @@ -127,7 +127,7 @@ The errors are exactly those validating the hydrated model reports. `ZodJsonExce same re-rooting for any exception (`TryGetErrors`, `TryReroot`, `Reroot`, `ToMemberPath`). A bare `ZodException` thrown from a converter still propagates unchanged. -These are the same errors ASP.NET Core hosts project into ProblemDetails (see [ASP.NET Core Integration](AspNetCore-Integration.md)); OpenAPI documents describe the response shape (see [OpenAPI Integration](OpenAPI-Integration.md)). +These are the same errors ASP.NET Core hosts project into ProblemDetails (see [ASP.NET Core Integration](../AspNetCore-Integration.md)); OpenAPI documents describe the response shape (see [OpenAPI Integration](../hosting/OpenAPI-Integration.md)). ## JSON Schema import diff --git a/src/tests/Repository.UnitTests/DocumentationWikiTests.cs b/src/tests/Repository.UnitTests/DocumentationWikiTests.cs new file mode 100644 index 00000000..08c4df72 --- /dev/null +++ b/src/tests/Repository.UnitTests/DocumentationWikiTests.cs @@ -0,0 +1,446 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace ZodSharp.Repository; + +/// +/// Guards the shape of the documentation set in docs/wiki: every page sits in a topic folder, is listed once +/// in _Sidebar.md, is reachable from Home.md, and its links resolve to pages that exist. +/// +/// +/// +/// The folder tree is load-bearing, not decorative: purview.dev publishes each page at +/// /docs/zodsharp/<folder>/<page>/ and resolves every relative link against the page that contains +/// it, so a page's location is part of its URL and a link is only correct relative to the file it is written in. +/// These tests read the tree the way the sync does - paths relative to docs/wiki, links resolved from the +/// containing file - which is why they can catch a page that exists but is published nowhere discoverable. +/// +/// +/// Every assertion names the offending file, because the failure mode is silence: the site publishes whatever it +/// finds, in whatever order it finds it, and a page missing from the sidebar simply has no entry there. +/// +/// +public sealed partial class DocumentationWikiTests +{ + const string SidebarFile = "_Sidebar.md"; + const string HomeFile = "Home.md"; + const string LandingPage = "Getting-Started.md"; + + /// + /// The pages that must stay at the wiki root: the site's landing page (docs.rootPage) and the two pages + /// its project catalogue deep-links through docsPage. That slug is a single path segment and must exist + /// in every documentation channel, so those pages keep root-level URLs. + /// + static readonly string[] RootPages = ["AspNetCore-Integration.md", "Fluent-Schema-API.md", "Getting-Started.md"]; + + static readonly Lazy WikiFolder = new(FindWikiFolder); + + [Test] + public async Task WikiFolder_ResolvesToThisRepository() + { + // Arrange + var folder = WikiFolder.Value; + + // Act + var indexFiles = new[] { SidebarFile, HomeFile, LandingPage } + .Where(name => File.Exists(Path.Combine(folder, name))) + .ToArray(); + + // Assert - without the repository marker walk landing here, every other test in this class passes + // vacuously against files that were never found. + await Assert + .That(indexFiles) + .IsEquivalentTo([SidebarFile, HomeFile, LandingPage]) + .Because($"'{folder}' does not look like this repository's docs/wiki folder"); + + await Assert.That(PageFiles()).IsNotEmpty(); + } + + [Test] + public async Task Pages_GivenEveryPage_LiveAtTheRootOrInOneTopicFolder() + { + // Arrange + var pages = PageFiles(); + + // Act - a page's folder is the first segment of its published URL, so two levels of nesting would make + // the URL say more about the wiki's filing than about the page. + var tooDeep = pages.Where(path => path.Split('/').Length > 2).ToArray(); + var folders = pages + .Select(path => path.Split('/')) + .Where(segments => segments.Length > 1) + .Select(segments => segments[0]) + .Distinct(StringComparer.Ordinal) + .Order(StringComparer.Ordinal) + .ToArray(); + + // Assert + await Assert + .That(tooDeep) + .IsEmpty() + .Because("every page belongs at the wiki root or in exactly one folder: " + string.Join(", ", tooDeep)); + + await Assert + .That(folders) + .IsEquivalentTo(["concepts", "development", "generator", "hosting", "quality", "schemas", "serialization"]) + .Because($"the topic folders are documented in Contributing.md; found: {string.Join(", ", folders)}"); + } + + [Test] + public async Task RootPages_GivenTheSiteContracts_HoldTheLandingAndDeepLinkedPages() + { + // Arrange + var rootPages = PageFiles().Where(path => !path.Contains('/')).ToArray(); + + // Act + var unexpected = rootPages.Except(RootPages, StringComparer.Ordinal).Order(StringComparer.Ordinal).ToArray(); + var missing = RootPages.Except(rootPages, StringComparer.Ordinal).Order(StringComparer.Ordinal).ToArray(); + + // Assert - Getting-Started.md is docs.rootPage, and the other two are the docsPage deep links the site's + // catalogue resolves against the default channel. Moving any of them changes a published URL that another + // repository points at. + await Assert + .That(unexpected) + .IsEmpty() + .Because( + "a page at the wiki root needs a site contract behind it (docs.rootPage or a docsPage deep link); " + + "otherwise it belongs in a topic folder: " + + string.Join(", ", unexpected) + ); + + await Assert + .That(missing) + .IsEmpty() + .Because("these pages must stay at the wiki root: " + string.Join(", ", missing)); + } + + [Test] + public async Task Pages_GivenEveryFileAndFolder_UseUrlSafeNames() + { + // Arrange + var pages = PageFiles(); + + // Act - a page's folder and file name become its URL, lowercased: `schemas/String-Validation.md` is published + // at /docs/zodsharp/schemas/string-validation/. A space, a dot or a non-ASCII character would be rewritten + // into a dash by the sync, so the address would stop mirroring the tree even though it stayed legal. + var unsafeNames = pages + .SelectMany(path => path.Split('/')) + .Select(segment => segment.EndsWith(".md", StringComparison.OrdinalIgnoreCase) ? segment[..^3] : segment) + .Where(segment => !UrlSafeNamePattern().IsMatch(segment)) + .Distinct(StringComparer.Ordinal) + .Order(StringComparer.Ordinal) + .ToArray(); + + // Assert - only letters, digits and single dashes, so no published URL needs percent-encoding. + await Assert + .That(unsafeNames) + .IsEmpty() + .Because( + "folders and page names become URL segments verbatim, so they must be letters, digits and dashes: " + + string.Join(", ", unsafeNames) + ); + } + + [Test] + public async Task Sidebar_GivenEveryPageOnDisk_ListsItExactlyOnce(CancellationToken cancellationToken) + { + // Arrange + var sidebar = await ReadWikiFileAsync(SidebarFile, cancellationToken); + var listed = PageTargets(sidebar, SidebarFile); + var pages = PageFiles(); + + // Act + var unlisted = pages.Except(listed, StringComparer.Ordinal).Order(StringComparer.Ordinal).ToArray(); + var duplicated = listed + .GroupBy(path => path, StringComparer.Ordinal) + .Where(group => group.Count() > 1) + .Select(group => $"{group.Key} (listed {group.Count()} times)") + .Order(StringComparer.Ordinal) + .ToArray(); + + // Assert - the sidebar is the page-order authority for the published site, so an unlisted page is + // published last, and a duplicated one has two competing positions. + await Assert + .That(unlisted) + .IsEmpty() + .Because( + $"every page must appear in {SidebarFile}, inside its section's list, by its path from docs/wiki: " + + string.Join(", ", unlisted) + ); + + await Assert + .That(duplicated) + .IsEmpty() + .Because($"a page may appear in {SidebarFile} only once: " + string.Join(", ", duplicated)); + } + + [Test] + public async Task Home_GivenEveryPageOnDisk_LinksToIt(CancellationToken cancellationToken) + { + // Arrange + var home = await ReadWikiFileAsync(HomeFile, cancellationToken); + var linked = PageTargets(home, HomeFile); + var pages = PageFiles(); + + // Act + var unlisted = pages.Except(linked, StringComparer.Ordinal).Order(StringComparer.Ordinal).ToArray(); + + // Assert - Home.md is the index a reader browses the folders with, so a page missing from it is invisible + // to anyone who did not already know its name. A page may be linked more than once - the feature + // highlights cross-reference a few pages - which is why only reachability is asserted here; exactly-once + // ordering belongs to the sidebar. + await Assert + .That(unlisted) + .IsEmpty() + .Because($"every page must appear in {HomeFile}: " + string.Join(", ", unlisted)); + } + + [Test] + public async Task Home_GivenEverySidebarSection_HasAMatchingHeading(CancellationToken cancellationToken) + { + // Arrange + var sidebar = StripCode(await ReadWikiFileAsync(SidebarFile, cancellationToken)); + var home = StripCode(await ReadWikiFileAsync(HomeFile, cancellationToken)); + + // Act - the labels are what the sidebar shows and the headings are what Home.md shows; a section renamed + // in one place only leaves the two disagreeing about what the wiki contains. + var labels = SectionLabelPattern().Matches(sidebar).Select(match => match.Groups[1].Value.Trim()).ToArray(); + var headings = Heading2Pattern().Matches(home).Select(match => match.Groups[1].Value.Trim()).ToArray(); + var unmatched = labels.Except(headings, StringComparer.Ordinal).ToArray(); + + // Assert + await Assert.That(labels).IsNotEmpty(); + await Assert + .That(unmatched) + .IsEmpty() + .Because( + $"every '{SidebarFile}' section needs a matching '## ' heading in {HomeFile}: " + + string.Join(", ", unmatched) + ); + } + + [Test] + public async Task WikiFiles_GivenEveryRelativeMarkdownLink_ResolveToAnExistingPage( + CancellationToken cancellationToken + ) + { + // Arrange + var wikiFiles = PageFiles().Append(SidebarFile).Append(HomeFile).ToArray(); + List broken = new(); + + // Act + foreach (var file in wikiFiles) + { + var markdown = StripCode(await ReadWikiFileAsync(file, cancellationToken)); + foreach (var target in MarkdownLinkTargets(markdown)) + { + var path = target.Split('#', 2)[0].Split('?', 2)[0]; + if (!IsRelativeMarkdownLink(path)) + { + continue; + } + + var resolved = ResolveRelative(file, path); + if (resolved is null) + { + // A link above docs/wiki is a repository path, which the site rewrites to a GitHub blob URL. + continue; + } + + if (!File.Exists(Path.Combine(WikiFolder.Value, resolved.Replace('/', Path.DirectorySeparatorChar)))) + { + broken.Add($"{file} -> {target}"); + } + } + } + + // Assert - a link is resolved against the page it appears on, so a page that moved without its incoming + // links being updated breaks only for the reader who follows it, and the site rewrites the target to a + // GitHub blob URL rather than failing its own build. + await Assert + .That(broken) + .IsEmpty() + .Because("these links point at pages that do not exist: " + string.Join(", ", broken)); + } + + [Test] + public async Task Pages_GivenEveryPage_OpenWithASingleH1Title(CancellationToken cancellationToken) + { + // Arrange + List problems = new(); + + // Act + foreach (var page in PageFiles()) + { + var markdown = StripCode(await ReadWikiFileAsync(page, cancellationToken)); + var lines = markdown.Split('\n').Where(line => !string.IsNullOrWhiteSpace(line)).ToArray(); + var titles = lines.Count(line => Heading1Pattern().IsMatch(line)); + + if (titles != 1) + { + problems.Add($"{page} has {titles} '# ' headings"); + } + else if (!Heading1Pattern().IsMatch(lines[0])) + { + problems.Add($"{page} does not open with its '# ' heading"); + } + } + + // Assert - the first heading is the title the published sidebar shows, so a page without exactly one is + // either untitled or titled with whichever section heading happens to come first. + await Assert.That(problems).IsEmpty().Because(string.Join("; ", problems)); + } + + /// Every page in the wiki, as a path relative to docs/wiki with / separators. + static string[] PageFiles() => + Directory + .GetFiles(WikiFolder.Value, "*.md", SearchOption.AllDirectories) + .Select(RelativeToWiki) + .Where(path => path is not SidebarFile and not HomeFile) + .Order(StringComparer.Ordinal) + .ToArray(); + + static string RelativeToWiki(string absolutePath) => + Path.GetRelativePath(WikiFolder.Value, absolutePath).Replace('\\', '/'); + + /// + /// The pages a document links to, as paths relative to docs/wiki. Links are resolved against the + /// document's own folder, which is what both the site and GitHub do; a link that climbs above the wiki folder + /// is a repository path and is dropped. + /// + static string[] PageTargets(string markdown, string fromPath) => + MarkdownLinkTargets(StripCode(markdown)) + .Select(target => target.Split('#', 2)[0].Split('?', 2)[0]) + .Where(IsRelativeMarkdownLink) + .Select(target => ResolveRelative(fromPath, target)) + .OfType() + .ToArray(); + + /// Resolves a link the way a browser does, or null when it points outside the wiki folder. + static string? ResolveRelative(string fromPath, string target) + { + if (target.StartsWith('/') || target.Contains("://", StringComparison.Ordinal)) + { + return null; + } + + List segments = new(); + var fromSegments = fromPath.Split('/'); + foreach (var segment in fromSegments[..^1]) + { + segments.Add(segment); + } + + foreach (var segment in target.Split('/')) + { + if (segment is "" or ".") + { + continue; + } + if (segment == "..") + { + if (segments.Count == 0) + { + return null; + } + segments.RemoveAt(segments.Count - 1); + continue; + } + segments.Add(segment); + } + + return segments.Count == 0 ? null : string.Join('/', segments); + } + + static async Task ReadWikiFileAsync(string path, CancellationToken cancellationToken) => + ( + await File.ReadAllTextAsync( + Path.Combine(WikiFolder.Value, path.Replace('/', Path.DirectorySeparatorChar)), + cancellationToken + ) + ) + // Line endings are normalised so a markdown pattern does not have to tolerate CRLF; the files are + // checked out with whatever the machine's autocrlf setting produces. + .Replace("\r\n", "\n", StringComparison.Ordinal); + + /// + /// Removes fenced blocks and inline code spans. Neither is markdown structure - a quoted `# heading` or a + /// quoted link is an example - so reading them would report problems the reader never sees. + /// + static string StripCode(string markdown) + { + StringBuilder builder = new(markdown.Length); + var inFence = false; + + foreach (var line in markdown.Split('\n')) + { + if (line.TrimStart().StartsWith("```", StringComparison.Ordinal)) + { + inFence = !inFence; + continue; + } + + if (!inFence) + { + builder.Append(InlineCodePattern().Replace(line, string.Empty)).Append('\n'); + } + } + + return builder.ToString(); + } + + static string[] MarkdownLinkTargets(string markdown) => + LinkPattern().Matches(markdown).Select(match => match.Groups[1].Value.Trim()).ToArray(); + + /// + /// Whether a link target names a page: relative, not anchored, and neither an external URL nor a repository + /// path. + /// + static bool IsRelativeMarkdownLink(string path) => + path.Length > 0 + && !path.StartsWith('#') + && !path.StartsWith('/') + && !path.Contains("://", StringComparison.Ordinal) + && path.EndsWith(".md", StringComparison.OrdinalIgnoreCase); + + static string FindWikiFolder() + { + for ( + var directory = new DirectoryInfo(AppContext.BaseDirectory); + directory is not null; + directory = directory.Parent + ) + { + var wiki = Path.Combine(directory.FullName, "docs", "wiki"); + if ( + File.Exists(Path.Combine(directory.FullName, "AGENTS.md")) + && File.Exists(Path.Combine(wiki, SidebarFile)) + && File.Exists(Path.Combine(wiki, HomeFile)) + ) + { + return wiki; + } + } + + throw new InvalidOperationException( + $"No 'docs/wiki' carrying {SidebarFile} and {HomeFile} above '{AppContext.BaseDirectory}'." + ); + } + + [GeneratedRegex(@"!?\[[^\]]*\]\(([^)\s]+)\)")] + private static partial Regex LinkPattern(); + + [GeneratedRegex(@"`[^`\n]*`")] + private static partial Regex InlineCodePattern(); + + [GeneratedRegex(@"^#\s", RegexOptions.Multiline)] + private static partial Regex Heading1Pattern(); + + [GeneratedRegex(@"^##\s+(.+?)\s*$", RegexOptions.Multiline)] + private static partial Regex Heading2Pattern(); + + [GeneratedRegex(@"^[ \t]*-[ \t]+\*\*(.+?)\*\*[ \t]*$", RegexOptions.Multiline)] + private static partial Regex SectionLabelPattern(); + + [GeneratedRegex(@"^[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$")] + private static partial Regex UrlSafeNamePattern(); +}