diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d6f1f37..251f86d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,19 +1,23 @@ -name: Deploy Documentation +name: Docs on: push: branches: [ main ] paths: - 'docs/**' - - 'mkdocs.yml' + - 'zensical.toml' - '.github/workflows/docs.yml' + - 'pyproject.toml' + - 'uv.lock' pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: [ main ] - types: [ opened, synchronize, reopened, ready_for_review ] paths: - 'docs/**' - - 'mkdocs.yml' + - 'zensical.toml' - '.github/workflows/docs.yml' + - 'pyproject.toml' + - 'uv.lock' permissions: contents: read @@ -22,11 +26,12 @@ permissions: concurrency: group: "pages" - cancel-in-progress: false + cancel-in-progress: true jobs: build: - if: github.event.pull_request.draft == false + if: github.event_name != 'pull_request' || github.event.pull_request.draft == false + runs-on: ubuntu-latest steps: - name: Checkout @@ -34,34 +39,31 @@ jobs: with: fetch-depth: 0 + - name: Install pngquant + run: sudo apt-get update && sudo apt-get install -y pngquant + - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.x' + python-version-file: "pyproject.toml" - - name: Cache dependencies - uses: actions/cache@v4 + - name: Install uv + uses: astral-sh/setup-uv@v7 with: - key: mkdocs-material-${{ hashFiles('requirements.txt') }} - path: ~/.cache/pip - restore-keys: | - mkdocs-material- + enable-cache: true - - name: Install dependencies - run: | - pip install mkdocs-material - pip install mkdocs-minify-plugin + - name: Install the project + run: uv sync --locked --all-extras --dev - name: Setup Pages id: pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@v5 - name: Build documentation - run: | - mkdocs build --clean + run: uv run zensical build --clean - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: path: site @@ -75,4 +77,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 31643fd..72560e1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.0.0 + 1.1.0 MIT https://github.com/layeredcraft/optimized-enums git diff --git a/Directory.Packages.props b/Directory.Packages.props index 22754f9..a3858d4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,6 +21,7 @@ + diff --git a/LayeredCraft.OptimizedEnums.slnx b/LayeredCraft.OptimizedEnums.slnx index 0b1dabe..250d03d 100644 --- a/LayeredCraft.OptimizedEnums.slnx +++ b/LayeredCraft.OptimizedEnums.slnx @@ -3,8 +3,7 @@ - - + @@ -32,6 +31,7 @@ + @@ -46,15 +46,19 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index cc007e5..c991fda 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ | Package | NuGet | Downloads | |---------|-------|-----------| | **LayeredCraft.OptimizedEnums** | [![NuGet](https://img.shields.io/nuget/v/LayeredCraft.OptimizedEnums.svg)](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums) | [![Downloads](https://img.shields.io/nuget/dt/LayeredCraft.OptimizedEnums.svg)](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums/) | -| **LayeredCraft.OptimizedEnums.SystemTextJson** | _coming soon_ | | +| **LayeredCraft.OptimizedEnums.SystemTextJson** | [![NuGet](https://img.shields.io/nuget/v/LayeredCraft.OptimizedEnums.SystemTextJson.svg)](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums.SystemTextJson) | [![Downloads](https://img.shields.io/nuget/dt/LayeredCraft.OptimizedEnums.SystemTextJson.svg)](https://www.nuget.org/packages/LayeredCraft.OptimizedEnums.SystemTextJson/) | | **LayeredCraft.OptimizedEnums.EFCore** | _coming soon_ | | | **LayeredCraft.OptimizedEnums.Dapper** | _coming soon_ | | | **LayeredCraft.OptimizedEnums.AutoFixture** | _coming soon_ | | @@ -88,6 +88,37 @@ Benchmarks run on Apple M3 Max, .NET 9.0.8, BenchmarkDotNet v0.14.0. All lookups are O(1) via statically-cached dictionaries. `Count` is a compile-time constant. +## JSON Serialization + +Add `LayeredCraft.OptimizedEnums.SystemTextJson` for source-generated, zero-reflection `JsonConverter` support. One package is all you need — it pulls in the core package automatically: + +```bash +dotnet add package LayeredCraft.OptimizedEnums.SystemTextJson +``` + +Decorate your class with `[OptimizedEnumJsonConverter]` and the generator emits a concrete, AOT-safe converter and wires it up via `[JsonConverter]`: + +```csharp +using LayeredCraft.OptimizedEnums; +using LayeredCraft.OptimizedEnums.SystemTextJson; + +[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } +} +``` + +```json +{ "status": "Pending" } +``` + +Two strategies are available: `ByName` (serializes as the member name string) and `ByValue` (serializes as the underlying value). See the [JSON Serialization docs](https://layeredcraft.github.io/optimized-enums/usage/json-serialization/) for full details. + ## Installation ```bash diff --git a/docs/advanced/diagnostics.md b/docs/advanced/diagnostics.md index 1fd86c3..6754db2 100644 --- a/docs/advanced/diagnostics.md +++ b/docs/advanced/diagnostics.md @@ -83,6 +83,47 @@ public OrderStatus(int value, string name) : base(value, name) { } #pragma warning restore OE0101 ``` +## SystemTextJson Diagnostics + +The `LayeredCraft.OptimizedEnums.SystemTextJson` generator emits diagnostics with the `OE2xxx` prefix. + +### OE2001 — Not an OptimizedEnum + +**Message:** `The class '{0}' must inherit from OptimizedEnum to use [OptimizedEnumJsonConverter]` + +**Cause:** `[OptimizedEnumJsonConverter]` was applied to a class that does not inherit from `OptimizedEnum`. + +**Fix:** Remove the attribute, or make the class inherit from `OptimizedEnum`. + +### OE2002 — Must Be Partial + +**Message:** `The class '{0}' must be declared as partial for [OptimizedEnumJsonConverter] source generation` + +**Cause:** A class decorated with `[OptimizedEnumJsonConverter]` is missing the `partial` keyword. The generator cannot stamp the `[JsonConverter]` attribute onto the class. + +**Fix:** +```csharp +// Before +[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] +public sealed class OrderStatus : OptimizedEnum { ... } + +// After +[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] +public sealed partial class OrderStatus : OptimizedEnum { ... } +``` + +### OE2003 — Unknown Converter Type + +**Message:** `The class '{0}' specifies an unknown OptimizedEnumJsonConverterType value '{1}'; valid values are ByName (0) and ByValue (1)` + +**Cause:** An explicit integer cast was used to pass an undefined `OptimizedEnumJsonConverterType` value to `[OptimizedEnumJsonConverter]`. + +**Fix:** Use only the defined enum members: +```csharp +[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] // or ByValue +public sealed partial class OrderStatus : OptimizedEnum { ... } +``` + ## Generator Not Running? If you add the package but see no generated members, check: diff --git a/docs/changelog.md b/docs/changelog.md index 1460ea6..180b221 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -7,6 +7,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Added +- `LayeredCraft.OptimizedEnums.SystemTextJson` package — source-generated, zero-reflection `JsonConverter` support + - `[OptimizedEnumJsonConverter]` attribute with `ByName` and `ByValue` strategies + - Emits a concrete non-generic `JsonConverter` for each decorated class — no runtime reflection, full AOT/NativeAOT compatibility + - Stamps `[JsonConverter(typeof(...))]` on a generated partial class stub — no manual `JsonSerializerOptions` registration required + - Declares `LayeredCraft.OptimizedEnums` as a NuGet dependency — only one package reference needed + - `OE2001` diagnostic for classes not inheriting `OptimizedEnum` + - `OE2002` diagnostic for classes missing `partial` - `OptimizedEnum` single-parameter base class for `int`-valued enums - Inheritance-based generation trigger — `[OptimizedEnum]` attribute no longer required - `Microsoft.CSharp.dll` bundled in `analyzers/dotnet/cs/` for Scriban dynamic dispatch diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 063dc3b..8323cb5 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -36,6 +36,30 @@ The package bundles two assemblies: Both are delivered automatically by the single NuGet package. No separate runtime package reference is needed. +## Optional: JSON Serialization + +To add source-generated `System.Text.Json` converter support, install the SystemTextJson package. It declares the core package as a dependency, so only one `dotnet add` is needed: + +=== ".NET CLI" + + ```bash + dotnet add package LayeredCraft.OptimizedEnums.SystemTextJson + ``` + +=== "Package Manager" + + ```powershell + Install-Package LayeredCraft.OptimizedEnums.SystemTextJson + ``` + +=== "PackageReference" + + ```xml + + ``` + +See [JSON Serialization](../usage/json-serialization.md) for usage details. + ## Verifying the Installation After adding the package, define a type that inherits from `OptimizedEnum` and declare it `partial`. Build the project — the generator runs during compilation and produces the lookup members. You can inspect the generated output under `obj/` or via your IDE's "Go to definition" on any generated method. diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index 10e5520..aa28f17 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -80,8 +80,35 @@ public sealed partial class Color : OptimizedEnum } ``` +## 6. JSON Serialization + +To serialize/deserialize your enum with `System.Text.Json`, install the SystemTextJson package (it pulls in the core package automatically): + +```bash +dotnet add package LayeredCraft.OptimizedEnums.SystemTextJson +``` + +Then decorate your class with `[OptimizedEnumJsonConverter]`: + +```csharp +using LayeredCraft.OptimizedEnums.SystemTextJson; + +[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } +} +``` + +`OrderStatus` now serializes as `"Pending"` / `"Paid"` / `"Shipped"` with no manual converter registration. See [JSON Serialization](../usage/json-serialization.md) for full details on `ByName` vs `ByValue` and AOT safety. + ## Next Steps - [Core Concepts — How It Works](../core-concepts/how-it-works.md) - [Usage — Defining Enums](../usage/defining-enums.md) +- [Usage — JSON Serialization](../usage/json-serialization.md) - [API Reference — Generated Members](../api-reference/generated-members.md) diff --git a/docs/usage/json-serialization.md b/docs/usage/json-serialization.md new file mode 100644 index 0000000..b2f314b --- /dev/null +++ b/docs/usage/json-serialization.md @@ -0,0 +1,167 @@ +# JSON Serialization (System.Text.Json) + +The `LayeredCraft.OptimizedEnums.SystemTextJson` package adds source-generated, zero-reflection `JsonConverter` support for your OptimizedEnum types. Decorate a class with `[OptimizedEnumJsonConverter]` and the generator emits a concrete converter and wires it up via `[JsonConverter]` automatically — no factory, no runtime type-checking, full AOT compatibility. + +## Installation + +Install the SystemTextJson package. The core `LayeredCraft.OptimizedEnums` package is pulled in automatically as a dependency — only one `dotnet add` is needed: + +=== ".NET CLI" + + ```bash + dotnet add package LayeredCraft.OptimizedEnums.SystemTextJson + ``` + +=== "Package Manager" + + ```powershell + Install-Package LayeredCraft.OptimizedEnums.SystemTextJson + ``` + +=== "PackageReference" + + ```xml + + ``` + +## The Attribute + +Two serialization strategies are available, controlled by the `OptimizedEnumJsonConverterType` enum: + +| Strategy | Value | JSON representation | Deserialization input | +|---|---|---|---| +| `ByName` | `0` | `"Pending"` (the Name string) | JSON string | +| `ByValue` | `1` | `1` (the underlying Value) | JSON number / string / bool depending on TValue | + +Apply the attribute to your OptimizedEnum class: + +```csharp +using LayeredCraft.OptimizedEnums; +using LayeredCraft.OptimizedEnums.SystemTextJson; + +[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] +public sealed partial class OrderStatus : OptimizedEnum +{ + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } +} +``` + +That is all the user code required. The generator handles everything else. + +## What Gets Generated + +For the `ByName` example above, the generator emits two things into a single `.g.cs` file: + +**1. A partial class stub stamped with `[JsonConverter]`:** + +```csharp +[JsonConverter(typeof(OrderStatusNameJsonConverter))] +partial class OrderStatus { } +``` + +This is how System.Text.Json discovers the converter — the attribute is on the type itself, so no manual registration in `JsonSerializerOptions` is ever needed. + +**2. A concrete, non-generic converter:** + +```csharp +[GeneratedCode(...)] +internal sealed class OrderStatusNameJsonConverter + : JsonConverter +{ + public override OrderStatus Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + throw new JsonException(...); + + var name = reader.GetString()!; + if (!OrderStatus.TryFromName(name, out var result)) + throw new JsonException($"'{name}' is not a valid name for OrderStatus."); + + return result!; + } + + public override void Write( + Utf8JsonWriter writer, + OrderStatus value, + JsonSerializerOptions options) + => writer.WriteStringValue(value.Name); +} +``` + +## ByName Strategy + +Serializes using the member's **Name** string. Suitable when your JSON needs to be human-readable or stable across value changes. + +```csharp +[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] +public sealed partial class OrderStatus : OptimizedEnum { ... } +``` + +```json +{ "status": "Pending" } +``` + +Deserialization calls `TryFromName` with Ordinal string comparison (the same as the hand-written lookup tables). An unrecognised name throws `JsonException`. + +## ByValue Strategy + +Serializes using the member's **Value**. Suitable for compact payloads or when matching external integer/string codes. + +```csharp +[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByValue)] +public sealed partial class OrderStatus : OptimizedEnum { ... } +``` + +```json +{ "status": 1 } +``` + +Deserialization delegates to `JsonSerializer.Deserialize` for the raw value, then calls `TryFromValue`. An unrecognised value throws `JsonException`. + +## String-Valued Enums + +Both strategies work with any `TValue`, including `string`: + +```csharp +[OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByValue)] +public sealed partial class Color : OptimizedEnum +{ + public static readonly Color Red = new("red", nameof(Red)); + public static readonly Color Green = new("green", nameof(Green)); + public static readonly Color Blue = new("blue", nameof(Blue)); + + private Color(string value, string name) : base(value, name) { } +} +``` + +With `ByValue`, the JSON value is `"red"`/`"green"`/`"blue"`. With `ByName`, it is `"Red"`/`"Green"`/`"Blue"`. + +## AOT and Trimming Safety + +Because the generator emits a concrete, non-generic converter for each type, the **converter logic itself** is entirely reflection-free: + +- No `MakeGenericType` — the converter type has `TEnum` baked in at generation time +- No `Delegate.CreateDelegate` +- `TryFromName` / `TryFromValue` are themselves source-generated static dictionary lookups + +`[JsonConverter(typeof(...))]` is stamped on the partial class at compile time, so STJ's own source-generation pipeline (`JsonSerializerContext`) can see and wire up the converter without reflection. + +!!! note "Converter instantiation" + When using `JsonSerializer` without a `JsonSerializerContext`, STJ instantiates the converter class via `Activator.CreateInstance` at startup (once, then caches it). This is standard STJ behaviour and is not specific to this package. To eliminate that last reflection call in NativeAOT scenarios, use a `JsonSerializerContext` — STJ's source gen will hard-wire the converter creation directly. + +## Diagnostics + +The SystemTextJson generator emits its own diagnostics with the `OE2xxx` prefix. See [Diagnostics](../advanced/diagnostics.md#systemtextjson-diagnostics) for details. + +## Constraints + +- The class must inherit from `OptimizedEnum` (OE2001). +- The class must be declared `partial` (OE2002). +- Only one `[OptimizedEnumJsonConverter]` per class (enforced by `AllowMultiple = false` on the attribute and by `[JsonConverter]` itself). diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 4a232ae..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,137 +0,0 @@ -site_name: LayeredCraft.OptimizedEnums -site_description: High-performance, AOT-safe alternative to SmartEnum patterns using source generation -site_url: https://layeredcraft.github.io/optimized-enums/ -site_author: Nick Cipollina - -# Repository -repo_name: layeredcraft/optimized-enums -repo_url: https://github.com/layeredcraft/optimized-enums -edit_uri: edit/main/docs/ - -# Copyright -copyright: Copyright © 2025 LayeredCraft - -# Configuration -theme: - name: material - language: en - logo: assets/icon.png - favicon: assets/icon.png - - features: - - content.code.copy - - content.code.select - - navigation.expand - - navigation.footer - - navigation.instant - - navigation.sections - - navigation.tabs - - navigation.tabs.sticky - - navigation.top - - search.highlight - - search.share - - search.suggest - - toc.follow - - # Dark/Light mode toggle - palette: - # Light mode - - media: "(prefers-color-scheme: light)" - scheme: default - primary: deep purple - accent: deep purple - toggle: - icon: material/brightness-7 - name: Switch to dark mode - - # Dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: deep purple - accent: deep purple - toggle: - icon: material/brightness-4 - name: Switch to light mode - - font: - text: Roboto - code: Roboto Mono - -# Plugins -plugins: - - search - - minify: - minify_html: true - -# Extensions -markdown_extensions: - # Python Markdown - - abbr - - admonition - - attr_list - - def_list - - footnotes - - md_in_html - - toc: - permalink: true - - # Python Markdown Extensions - - pymdownx.betterem: - smart_enable: all - - pymdownx.caret - - pymdownx.details - - pymdownx.emoji: - emoji_generator: !!python/name:material.extensions.emoji.to_svg - emoji_index: !!python/name:material.extensions.emoji.twemoji - - pymdownx.highlight: - anchor_linenums: true - line_spans: __span - pygments_lang_class: true - - pymdownx.inlinehilite - - pymdownx.keys - - pymdownx.mark - - pymdownx.smartsymbols - - pymdownx.superfences - - pymdownx.tabbed: - alternate_style: true - - pymdownx.tasklist: - custom_checkbox: true - - pymdownx.tilde - -# Navigation -nav: - - Home: index.md - - Getting Started: - - Installation: getting-started/installation.md - - Quick Start: getting-started/quick-start.md - - Core Concepts: - - How It Works: core-concepts/how-it-works.md - - Source Generators: core-concepts/source-generators.md - - Inheritance Model: core-concepts/inheritance-model.md - - Usage: - - Defining Enums: usage/defining-enums.md - - Lookups & Queries: usage/lookups.md - - String Values: usage/string-values.md - - Advanced: - - Performance: advanced/performance.md - - Diagnostics: advanced/diagnostics.md - - AOT & Trimming: advanced/aot-trimming.md - - API Reference: - - Base Class: api-reference/base-class.md - - Generated Members: api-reference/generated-members.md - - Contributing: contributing.md - - Changelog: changelog.md - -# Social links -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/layeredcraft/optimized-enums - name: GitHub Repository - - icon: fontawesome/solid/download - link: https://www.nuget.org/packages/LayeredCraft.OptimizedEnums/ - name: NuGet Package - -# Custom CSS -extra_css: - - assets/css/extra.css diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2585d91 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "layeredcraft-optimized-enums-docs" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "zensical>=0.0.21", +] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 3ca83ae..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -mkdocs-material>=9.5.0 -mkdocs-minify-plugin>=0.8.0 diff --git a/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md b/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md index fa97a99..491d7fc 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md +++ b/src/LayeredCraft.OptimizedEnums.Generator/AnalyzerReleases.Unshipped.md @@ -11,3 +11,4 @@ OE0006 | OptimizedEnums.Usage | Error | DiagnosticDescriptors OE0101 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors OE0102 | OptimizedEnums.Usage | Warning | DiagnosticDescriptors + OE9001 | OptimizedEnums.Usage | Error | DiagnosticDescriptors diff --git a/src/LayeredCraft.OptimizedEnums.Generator/LayeredCraft.OptimizedEnums.Generator.csproj b/src/LayeredCraft.OptimizedEnums.Generator/LayeredCraft.OptimizedEnums.Generator.csproj index b05ace8..f559873 100644 --- a/src/LayeredCraft.OptimizedEnums.Generator/LayeredCraft.OptimizedEnums.Generator.csproj +++ b/src/LayeredCraft.OptimizedEnums.Generator/LayeredCraft.OptimizedEnums.Generator.csproj @@ -3,6 +3,7 @@ netstandard2.0 enable latest + $(DefineConstants);SCRIBAN_NO_SYSTEM_TEXT_JSON false true true diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Shipped.md b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..9c6fa74 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Unshipped.md b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..75a5894 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,11 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + + Rule ID | Category | Severity | Notes +---------|---------------------------------|----------|----------------------- + OE2001 | OptimizedEnums.SystemTextJson | Error | DiagnosticDescriptors + OE2002 | OptimizedEnums.SystemTextJson | Error | DiagnosticDescriptors + OE2003 | OptimizedEnums.SystemTextJson | Error | DiagnosticDescriptors + OE9002 | OptimizedEnums.SystemTextJson | Error | DiagnosticDescriptors diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AttributeSource.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AttributeSource.cs new file mode 100644 index 0000000..14ca153 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AttributeSource.cs @@ -0,0 +1,59 @@ +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator; + +/// +/// Source text injected into every consuming compilation via +/// RegisterPostInitializationOutput so that +/// [OptimizedEnumJsonConverter] is available without a separate runtime assembly. +/// +internal static class AttributeSource +{ + internal const string HintName = "OptimizedEnumJsonConverterAttribute.g.cs"; + + internal const string Source = """ + //------------------------------------------------------------------------------ + // + // This code was generated by a tool. + // + // Changes to this file may cause incorrect behavior and will be lost if + // the code is regenerated. + // + //------------------------------------------------------------------------------ + + #nullable enable + + namespace LayeredCraft.OptimizedEnums.SystemTextJson + { + /// + /// Controls how an OptimizedEnum is serialized to and deserialized from JSON. + /// + public enum OptimizedEnumJsonConverterType + { + /// Serialize as the member's Name string (e.g. "Pending"). + ByName = 0, + + /// Serialize as the member's underlying Value (e.g. 1). + ByValue = 1, + } + + /// + /// Instructs the OptimizedEnums source generator to emit a System.Text.Json converter + /// for the decorated OptimizedEnum class and stamp it with [JsonConverter] automatically. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Class, + AllowMultiple = false, + Inherited = false)] + public sealed class OptimizedEnumJsonConverterAttribute : global::System.Attribute + { + /// Initializes a new instance. + public OptimizedEnumJsonConverterAttribute(OptimizedEnumJsonConverterType converterType) + { + ConverterType = converterType; + } + + /// Gets the serialization strategy for this converter. + public OptimizedEnumJsonConverterType ConverterType { get; } + } + } + """; +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticDescriptors.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 0000000..1a6d60a --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,40 @@ +using Microsoft.CodeAnalysis; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Diagnostics; + +internal static class DiagnosticDescriptors +{ + private const string Category = "OptimizedEnums.SystemTextJson"; + + internal static readonly DiagnosticDescriptor MustInheritOptimizedEnum = new( + "OE2001", + "OptimizedEnumJsonConverter requires an OptimizedEnum subclass", + "The class '{0}' must inherit from OptimizedEnum to use [OptimizedEnumJsonConverter]", + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor MustBePartial = new( + "OE2002", + "OptimizedEnum class must be partial for JSON converter generation", + "The class '{0}' must be declared as partial for [OptimizedEnumJsonConverter] source generation", + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor UnknownConverterType = new( + "OE2003", + "Unknown OptimizedEnumJsonConverterType value", + "The class '{0}' specifies an unknown OptimizedEnumJsonConverterType value '{1}'; valid values are ByName (0) and ByValue (1)", + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor GeneratorInternalError = new( + "OE9002", + "OptimizedEnums SystemTextJson generator internal error", + "An unexpected error occurred while generating the JSON converter for '{0}': {1}", + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticInfo.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticInfo.cs new file mode 100644 index 0000000..a10d15d --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticInfo.cs @@ -0,0 +1,42 @@ +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Models; +using Microsoft.CodeAnalysis; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Diagnostics; + +internal sealed record DiagnosticInfo( + DiagnosticDescriptor DiagnosticDescriptor, + LocationInfo? LocationInfo = null, + params object?[] MessageArgs +) +{ + public bool Equals(DiagnosticInfo? other) => + other is not null + && DiagnosticDescriptor.Id == other.DiagnosticDescriptor.Id + && Equals(LocationInfo, other.LocationInfo) + && MessageArgs.SequenceEqual(other.MessageArgs); + + public override int GetHashCode() + { + var hash = new HashCode(); + hash.Add(DiagnosticDescriptor.Id); + hash.Add(LocationInfo); + foreach (var arg in MessageArgs) + hash.Add(arg); + return hash.ToHashCode(); + } +} + +internal static class DiagnosticInfoExtensions +{ + extension(DiagnosticInfo diagnosticInfo) + { + internal Diagnostic ToDiagnostic() => + Diagnostic.Create( + diagnosticInfo.DiagnosticDescriptor, + diagnosticInfo.LocationInfo?.ToLocation(), + diagnosticInfo.MessageArgs); + + internal void ReportDiagnostic(SourceProductionContext context) => + context.ReportDiagnostic(diagnosticInfo.ToDiagnostic()); + } +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs new file mode 100644 index 0000000..042e32e --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs @@ -0,0 +1,78 @@ +using System.Reflection; +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Diagnostics; +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Models; +using Microsoft.CodeAnalysis; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Emitters; + +internal static class JsonConverterEmitter +{ + private static string GeneratedCodeAttribute { get; } = BuildGeneratedCodeAttribute(); + + private static string BuildGeneratedCodeAttribute() + { + var asm = Assembly.GetExecutingAssembly(); + return $"""[global::System.CodeDom.Compiler.GeneratedCode("{asm.GetName().Name}", "{asm.GetName().Version}")]"""; + } + + internal static void Generate(SourceProductionContext context, JsonConverterInfo info) + { + var converterSuffix = info.ConverterType == OptimizedEnumJsonConverterType.ByName ? "Name" : "Value"; + var converterClassName = $"{info.ClassName}{converterSuffix}JsonConverter"; + var hintName = info.FullyQualifiedClassName.Replace("global::", "") + ".SystemTextJson.g.cs"; + + var model = new + { + GeneratedCodeAttribute, + ConverterClassName = converterClassName, + info.ClassName, + info.FullyQualifiedClassName, + info.ValueTypeFullyQualified, + info.ValueTypeIsReferenceType, + IsByName = info.ConverterType == OptimizedEnumJsonConverterType.ByName, + Preamble = BuildPreamble(info), + Suffix = BuildSuffix(info), + }; + + try + { + var source = TemplateHelper.Render("Templates.JsonConverter.scriban", model); + context.AddSource(hintName, source); + } + catch (Exception ex) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratorInternalError, + info.Location?.ToLocation(), + info.ClassName, + ex.Message)); + } + } + + private static string BuildPreamble(JsonConverterInfo info) + { + if (info.Namespace is null && info.ContainingTypeNames.Length == 0) + return string.Empty; + + var sb = new System.Text.StringBuilder(); + if (info.Namespace is not null) + sb.Append("namespace ").Append(info.Namespace).Append(";\n\n"); + + foreach (var ct in info.ContainingTypeNames) + sb.Append(ct).Append("\n{\n"); + + return sb.ToString(); + } + + private static string BuildSuffix(JsonConverterInfo info) + { + if (info.ContainingTypeNames.Length == 0) + return string.Empty; + + var sb = new System.Text.StringBuilder(); + for (var i = 0; i < info.ContainingTypeNames.Length; i++) + sb.Append("\n}"); + + return sb.ToString(); + } +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/TemplateHelper.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/TemplateHelper.cs new file mode 100644 index 0000000..bee21cc --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/TemplateHelper.cs @@ -0,0 +1,60 @@ +using System.Collections.Concurrent; +using System.Reflection; +using Scriban; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Emitters; + +internal static class TemplateHelper +{ + private static readonly ConcurrentDictionary Cache = new(); + + internal static string Render(string resourceName, TModel model) + { + var template = Cache.GetOrAdd(resourceName, LoadTemplate); + return template.Render(model); + } + + private static Template LoadTemplate(string relativePath) + { + var assembly = Assembly.GetExecutingAssembly(); + var baseName = assembly.GetName().Name; + + var templateName = relativePath + .TrimStart('.') + .Replace(Path.DirectorySeparatorChar, '.') + .Replace(Path.AltDirectorySeparatorChar, '.'); + + var manifestTemplateName = assembly + .GetManifestResourceNames() + .FirstOrDefault(x => x.EndsWith(templateName, StringComparison.Ordinal)); + + if (string.IsNullOrEmpty(manifestTemplateName)) + { + var availableResources = string.Join(", ", assembly.GetManifestResourceNames()); + throw new InvalidOperationException( + $"Did not find required resource ending in '{templateName}' in assembly '{baseName}'. " + + $"Available resources: {availableResources}"); + } + + using var stream = assembly.GetManifestResourceStream(manifestTemplateName); + if (stream == null) + throw new FileNotFoundException( + $"Template '{relativePath}' not found in embedded resources. " + + $"Manifest resource name: '{manifestTemplateName}'"); + + using var reader = new StreamReader(stream); + var templateContent = reader.ReadToEnd(); + + var template = Template.Parse(templateContent, relativePath); + if (!template.HasErrors) + return template; + + var errors = string.Join( + "\n", + template.Messages.Select(m => + $"{relativePath}({m.Span.Start.Line},{m.Span.Start.Column}): {m.Message}")); + + throw new InvalidOperationException( + $"Failed to parse template '{relativePath}':\n{errors}"); + } +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj new file mode 100644 index 0000000..cb55b51 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj @@ -0,0 +1,64 @@ + + + netstandard2.0 + enable + latest + $(DefineConstants);SCRIBAN_NO_SYSTEM_TEXT_JSON + false + true + true + true + LayeredCraft.OptimizedEnums.SystemTextJson + LayeredCraft.OptimizedEnums.SystemTextJson.Generator + LayeredCraft.OptimizedEnums.SystemTextJson.Generator + LayeredCraft.OptimizedEnums.SystemTextJson + System.Text.Json source-generated converters for LayeredCraft.OptimizedEnums. Decorate your OptimizedEnum class with [OptimizedEnumJsonConverter] to get a zero-reflection, AOT-safe JsonConverter stamped automatically. + enum;source-generator;smart-enum;dotnet;csharp;system-text-json;json;aot + true + true + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/EquatableArray.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/EquatableArray.cs new file mode 100644 index 0000000..219e176 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/EquatableArray.cs @@ -0,0 +1,50 @@ +using System.Collections; +using System.Collections.Immutable; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Models; + +internal readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + private readonly ImmutableArray _array; + + public EquatableArray(ImmutableArray array) => _array = array; + + public static readonly EquatableArray Empty = new(ImmutableArray.Empty); + + public int Length => _array.Length; + + public T this[int index] => _array[index]; + + public bool Equals(EquatableArray other) => _array.SequenceEqual(other._array); + + public override bool Equals(object? obj) => + obj is EquatableArray other && Equals(other); + + public override int GetHashCode() + { + var hash = new HashCode(); + foreach (var item in _array) + hash.Add(item); + return hash.ToHashCode(); + } + + public T[] ToArray() => _array.IsDefaultOrEmpty ? Array.Empty() : _array.ToArray(); + + public ImmutableArray.Enumerator GetEnumerator() => _array.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_array).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_array).GetEnumerator(); + + public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right); + + public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right); +} + +internal static class EquatableArrayExtensions +{ + internal static EquatableArray ToEquatableArray(this IEnumerable source) + where T : IEquatable => + new(source.ToImmutableArray()); +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/JsonConverterInfo.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/JsonConverterInfo.cs new file mode 100644 index 0000000..6c314be --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/JsonConverterInfo.cs @@ -0,0 +1,33 @@ +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Diagnostics; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Models; + +internal sealed record JsonConverterInfo( + string? Namespace, + string ClassName, + string FullyQualifiedClassName, + string ValueTypeFullyQualified, + bool ValueTypeIsReferenceType, + EquatableArray ContainingTypeNames, + OptimizedEnumJsonConverterType ConverterType, + EquatableArray Diagnostics, + LocationInfo? Location +) +{ + // Location intentionally excluded from equality — position-only changes should not + // bust the incremental cache and trigger unnecessary re-emission. + public bool Equals(JsonConverterInfo? other) => + other is not null + && Namespace == other.Namespace + && ClassName == other.ClassName + && FullyQualifiedClassName == other.FullyQualifiedClassName + && ValueTypeFullyQualified == other.ValueTypeFullyQualified + && ValueTypeIsReferenceType == other.ValueTypeIsReferenceType + && ContainingTypeNames == other.ContainingTypeNames + && ConverterType == other.ConverterType + && Diagnostics == other.Diagnostics; + + public override int GetHashCode() => + HashCode.Combine(Namespace, ClassName, FullyQualifiedClassName, ValueTypeFullyQualified, + ValueTypeIsReferenceType, ContainingTypeNames, ConverterType, Diagnostics); +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/LocationInfo.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/LocationInfo.cs new file mode 100644 index 0000000..05126f6 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/LocationInfo.cs @@ -0,0 +1,38 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Models; + +internal sealed record LocationInfo(string FilePath, TextSpan TextSpan, LinePositionSpan LineSpan); + +internal static class LocationInfoExtensions +{ + extension(LocationInfo locationInfo) + { + internal Location ToLocation() => + Location.Create(locationInfo.FilePath, locationInfo.TextSpan, locationInfo.LineSpan); + } + + extension(Location location) + { + internal LocationInfo? CreateLocationInfo() => + location.SourceTree is null + ? null + : new LocationInfo( + location.SourceTree.FilePath, + location.SourceSpan, + location.GetLineSpan().Span); + } + + extension(ISymbol symbol) + { + internal LocationInfo? CreateLocationInfo() => + symbol.Locations.FirstOrDefault()?.CreateLocationInfo(); + } + + extension(SyntaxNode syntaxNode) + { + internal LocationInfo? CreateLocationInfo() => + syntaxNode.GetLocation().CreateLocationInfo(); + } +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterGenerator.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterGenerator.cs new file mode 100644 index 0000000..eddb7cf --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterGenerator.cs @@ -0,0 +1,39 @@ +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Diagnostics; +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Emitters; +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Providers; +using Microsoft.CodeAnalysis; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator; + +/// Source generator that emits System.Text.Json converters for OptimizedEnum types. +[Generator] +public sealed class OptimizedEnumJsonConverterGenerator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(static ctx => + ctx.AddSource(AttributeSource.HintName, AttributeSource.Source)); + + var converterInfos = context.SyntaxProvider + .ForAttributeWithMetadataName( + JsonConverterSyntaxProvider.AttributeMetadataName, + JsonConverterSyntaxProvider.Predicate, + JsonConverterSyntaxProvider.Transform) + .WithTrackingName(TrackingNames.JsonConverterSyntaxProvider_Extract) + .Where(static x => x is not null) + .Select(static (x, _) => x!) + .WithTrackingName(TrackingNames.JsonConverterSyntaxProvider_FilterNotNull); + + context.RegisterSourceOutput(converterInfos, static (ctx, info) => + { + foreach (var diagnostic in info.Diagnostics) + diagnostic.ReportDiagnostic(ctx); + + if (info.Diagnostics.Any(d => d.DiagnosticDescriptor.DefaultSeverity == DiagnosticSeverity.Error)) + return; + + JsonConverterEmitter.Generate(ctx, info); + }); + } +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterType.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterType.cs new file mode 100644 index 0000000..83cca3b --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterType.cs @@ -0,0 +1,8 @@ +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator; + +/// Internal mirror of the public enum emitted into consuming compilations. +internal enum OptimizedEnumJsonConverterType +{ + ByName = 0, + ByValue = 1, +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs new file mode 100644 index 0000000..7ac7612 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs @@ -0,0 +1,166 @@ +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Diagnostics; +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Providers; + +internal static class JsonConverterSyntaxProvider +{ + internal const string AttributeMetadataName = + "LayeredCraft.OptimizedEnums.SystemTextJson.OptimizedEnumJsonConverterAttribute"; + + private const string OptimizedEnumBaseMetadataName = + "LayeredCraft.OptimizedEnums.OptimizedEnum`2"; + + internal static bool Predicate(SyntaxNode node, CancellationToken _) => + node is ClassDeclarationSyntax; + + internal static JsonConverterInfo? Transform( + GeneratorAttributeSyntaxContext context, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (context.TargetNode is not ClassDeclarationSyntax classDecl) + return null; + + if (context.TargetSymbol is not INamedTypeSymbol classSymbol) + return null; + + var attr = context.Attributes[0]; + + var diagnostics = new List(); + var location = classDecl.CreateLocationInfo(); + var className = classSymbol.Name; + + // OE2001: must inherit from OptimizedEnum<,> + var baseType = FindOptimizedEnumBase(classSymbol, context.SemanticModel.Compilation); + if (baseType is null) + { + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.MustInheritOptimizedEnum, + location, + className)); + + return new JsonConverterInfo( + Namespace: null, + ClassName: className, + FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ValueTypeFullyQualified: string.Empty, + ValueTypeIsReferenceType: false, + ContainingTypeNames: EquatableArray.Empty, + ConverterType: OptimizedEnumJsonConverterType.ByName, + Diagnostics: diagnostics.ToEquatableArray(), + Location: location); + } + + // OE2002: must be partial + if (!classDecl.Modifiers.Any(static m => m.IsKind(SyntaxKind.PartialKeyword))) + { + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.MustBePartial, + location, + className)); + + return new JsonConverterInfo( + Namespace: null, + ClassName: className, + FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ValueTypeFullyQualified: string.Empty, + ValueTypeIsReferenceType: false, + ContainingTypeNames: EquatableArray.Empty, + ConverterType: OptimizedEnumJsonConverterType.ByName, + Diagnostics: diagnostics.ToEquatableArray(), + Location: location); + } + + // Read ConverterType from the attribute constructor argument + var converterType = OptimizedEnumJsonConverterType.ByName; + if (attr.ConstructorArguments.Length > 0 && attr.ConstructorArguments[0].Value is int rawValue) + { + if (rawValue != (int)OptimizedEnumJsonConverterType.ByName && + rawValue != (int)OptimizedEnumJsonConverterType.ByValue) + { + diagnostics.Add(new DiagnosticInfo( + DiagnosticDescriptors.UnknownConverterType, + location, + className, + rawValue)); + + return new JsonConverterInfo( + Namespace: null, + ClassName: className, + FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ValueTypeFullyQualified: string.Empty, + ValueTypeIsReferenceType: false, + ContainingTypeNames: EquatableArray.Empty, + ConverterType: OptimizedEnumJsonConverterType.ByName, + Diagnostics: diagnostics.ToEquatableArray(), + Location: location); + } + + converterType = (OptimizedEnumJsonConverterType)rawValue; + } + + var valueTypeSymbol = baseType.TypeArguments[1]; + + return new JsonConverterInfo( + Namespace: GetNamespace(classSymbol), + ClassName: className, + FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ValueTypeFullyQualified: valueTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ValueTypeIsReferenceType: valueTypeSymbol.IsReferenceType, + ContainingTypeNames: GetContainingTypeDeclarations(classSymbol), + ConverterType: converterType, + Diagnostics: diagnostics.ToEquatableArray(), + Location: location); + } + + private static INamedTypeSymbol? FindOptimizedEnumBase( + INamedTypeSymbol classSymbol, + Compilation compilation) + { + var optimizedEnumBase = compilation.GetTypeByMetadataName(OptimizedEnumBaseMetadataName); + if (optimizedEnumBase is null) + return null; + + var current = classSymbol.BaseType; + while (current is not null) + { + if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, optimizedEnumBase)) + return current; + current = current.BaseType; + } + + return null; + } + + private static EquatableArray GetContainingTypeDeclarations(INamedTypeSymbol symbol) + { + var result = new List(); + var current = symbol.ContainingType; + while (current is not null) + { + var keyword = (current.IsRecord, current.TypeKind) switch + { + (true, TypeKind.Struct) => "record struct", + (true, _) => "record", + (_, TypeKind.Struct) => "struct", + (_, TypeKind.Interface) => "interface", + _ => "class" + }; + var staticModifier = current.IsStatic ? "static " : ""; + var nameWithTypeParams = current.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); + result.Insert(0, $"partial {staticModifier}{keyword} {nameWithTypeParams}"); + current = current.ContainingType; + } + return result.ToEquatableArray(); + } + + private static string? GetNamespace(INamedTypeSymbol symbol) => + symbol.ContainingNamespace.IsGlobalNamespace + ? null + : symbol.ContainingNamespace.ToDisplayString(); +} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban new file mode 100644 index 0000000..7bc7f36 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban @@ -0,0 +1,76 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +{{ preamble ~}} +[global::System.Text.Json.Serialization.JsonConverter(typeof({{ converter_class_name }}))] +partial class {{ class_name }} { } + +{{ generated_code_attribute }} +internal sealed class {{ converter_class_name }} + : global::System.Text.Json.Serialization.JsonConverter<{{ fully_qualified_class_name }}> +{ +{{ if is_by_name }} + public override {{ fully_qualified_class_name }} Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options) + { + if (reader.TokenType != global::System.Text.Json.JsonTokenType.String) + throw new global::System.Text.Json.JsonException( + $"Expected a JSON string for {{ class_name }} but got {reader.TokenType}."); + + var name = reader.GetString()!; + + if (!{{ fully_qualified_class_name }}.TryFromName(name, out var result)) + throw new global::System.Text.Json.JsonException( + $"'{name}' is not a valid name for {{ class_name }}."); + + return result!; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + {{ fully_qualified_class_name }} value, + global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Name); +{{ else }} + public override {{ fully_qualified_class_name }} Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options) + { +{{ if value_type_is_reference_type }} + if (reader.TokenType == global::System.Text.Json.JsonTokenType.Null) + throw new global::System.Text.Json.JsonException( + $"Expected a non-null JSON value for {{ class_name }}."); + + var value = global::System.Text.Json.JsonSerializer.Deserialize<{{ value_type_fully_qualified }}>(ref reader, options); + + if (value is null || !{{ fully_qualified_class_name }}.TryFromValue(value, out var result)) + throw new global::System.Text.Json.JsonException( + $"'{value}' is not a valid value for {{ class_name }}."); +{{ else }} + var value = global::System.Text.Json.JsonSerializer.Deserialize<{{ value_type_fully_qualified }}>(ref reader, options); + + if (!{{ fully_qualified_class_name }}.TryFromValue(value, out var result)) + throw new global::System.Text.Json.JsonException( + $"'{value}' is not a valid value for {{ class_name }}."); +{{ end }} + return result!; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + {{ fully_qualified_class_name }} value, + global::System.Text.Json.JsonSerializerOptions options) + => global::System.Text.Json.JsonSerializer.Serialize(writer, value.Value, options); +{{ end ~}} +}{{ suffix }} diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/TrackingNames.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/TrackingNames.cs new file mode 100644 index 0000000..ff265cb --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/TrackingNames.cs @@ -0,0 +1,9 @@ +// ReSharper disable InconsistentNaming + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator; + +internal static class TrackingNames +{ + internal const string JsonConverterSyntaxProvider_Extract = nameof(JsonConverterSyntaxProvider_Extract); + internal const string JsonConverterSyntaxProvider_FilterNotNull = nameof(JsonConverterSyntaxProvider_FilterNotNull); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorTestHelpers.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorTestHelpers.cs new file mode 100644 index 0000000..5f5edee --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorTestHelpers.cs @@ -0,0 +1,147 @@ +using System.Text.RegularExpressions; +using Basic.Reference.Assemblies; +using LayeredCraft.OptimizedEnums; +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Tests; + +internal class VerifyTestOptions +{ + internal required string SourceCode { get; init; } + internal string CodePath { get; init; } = "Program.cs"; + internal LanguageVersion LanguageVersion { get; init; } = LanguageVersion.CSharp13; + internal string AssemblyName { get; init; } = "TestsAssembly"; + internal string? ExpectedDiagnosticId { get; init; } + internal int? ExpectedTrees { get; init; } +} + +internal static class GeneratorTestHelpers +{ + internal static Task Verify(VerifyTestOptions options, CancellationToken cancellationToken = default) + { + var (driver, originalCompilation) = GenerateFromSource(options, cancellationToken); + + var result = driver.GetRunResult(); + + result.Diagnostics + .Should() + .BeEmpty( + "code should be generated without errors, but found:\n" + + string.Join("\n---\n", result.Diagnostics.Select(e => $" - {e.Id}: {e.GetMessage()} at {e.Location}"))); + + var parseOptions = originalCompilation.SyntaxTrees.First().Options; + var reparsedTrees = result.GeneratedTrees + .Select(tree => CSharpSyntaxTree.ParseText(tree.GetText(), (CSharpParseOptions)parseOptions)) + .ToArray(); + + var outputCompilation = originalCompilation.AddSyntaxTrees(reparsedTrees); + var errors = outputCompilation + .GetDiagnostics(cancellationToken) + .Where(d => d.Severity == DiagnosticSeverity.Error) + .ToList(); + + errors.Should().BeEmpty( + "generated code should compile without errors, but found:\n" + + string.Join("\n---\n", errors.Select(e => $" - {e.Id}: {e.GetMessage()} at {e.Location}"))); + + if (options.ExpectedTrees is not null) + result.GeneratedTrees.Length.Should().Be(options.ExpectedTrees); + + return Verifier + .Verify(driver) + .UseDirectory("Snapshots") + .DisableDiff() + .ScrubLinesWithReplace(line => + { + if (line.Contains("global::System.CodeDom.Compiler.GeneratedCode")) + return RegexHelper.GeneratedCodeAttributeRegex().Replace(line, "REPLACED"); + return line; + }); + } + + internal static Task VerifyFailure(VerifyTestOptions options, CancellationToken cancellationToken = default) + { + var (driver, _) = GenerateFromSource(options, cancellationToken); + + var result = driver.GetRunResult(); + + result.Diagnostics.Should().NotBeEmpty("expected diagnostic errors to be generated"); + + if (options.ExpectedDiagnosticId is not null) + { + result.Diagnostics + .Should() + .Contain( + d => d.Id == options.ExpectedDiagnosticId, + $"expected diagnostic {options.ExpectedDiagnosticId} to be present, but found:\n" + + string.Join("\n---\n", result.Diagnostics.Select(e => $" - {e.Id}: {e.GetMessage()} at {e.Location}"))); + } + + return Verifier + .Verify(driver) + .UseDirectory("Snapshots") + .DisableDiff() + .ScrubLinesWithReplace(line => + { + if (line.Contains("global::System.CodeDom.Compiler.GeneratedCode")) + return RegexHelper.GeneratedCodeAttributeRegex().Replace(line, "REPLACED"); + return line; + }); + } + + private static (GeneratorDriver driver, Compilation compilation) GenerateFromSource( + VerifyTestOptions options, + CancellationToken cancellationToken = default) + { + var parseOptions = CSharpParseOptions.Default.WithLanguageVersion(options.LanguageVersion); + + var syntaxTree = CSharpSyntaxTree.ParseText( + options.SourceCode, + parseOptions, + options.CodePath, + cancellationToken: cancellationToken); + + List references = + [ +#if NET10_0_OR_GREATER + .. Net100.References.All.ToList(), +#elif NET9_0 + .. Net90.References.All.ToList(), +#else + .. Net80.References.All.ToList(), +#endif + MetadataReference.CreateFromFile(typeof(OptimizedEnum<,>).Assembly.Location), + ]; + + var compilation = CSharpCompilation.Create( + options.AssemblyName, + [syntaxTree], + references, + new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + nullableContextOptions: NullableContextOptions.Enable)); + + // Run both generators: the main one produces FromName/FromValue, + // the STJ one produces the JsonConverter. + // Pass parseOptions so post-init output uses the same language version as the compilation. + var driver = CSharpGeneratorDriver.Create( + generators: + [ + new LayeredCraft.OptimizedEnums.Generator.OptimizedEnumGenerator().AsSourceGenerator(), + new OptimizedEnumJsonConverterGenerator().AsSourceGenerator(), + ], + parseOptions: parseOptions); + + var updatedDriver = driver.RunGenerators(compilation, cancellationToken); + + return (updatedDriver, compilation); + } +} + +internal static partial class RegexHelper +{ + [GeneratedRegex("""(\d+\.\d+\.\d+\.\d+)""", RegexOptions.None, "en-US")] + internal static partial Regex GeneratedCodeAttributeRegex(); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorVerifyTests.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorVerifyTests.cs new file mode 100644 index 0000000..a76d7e9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorVerifyTests.cs @@ -0,0 +1,196 @@ +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Tests; + +public class GeneratorVerifyTests +{ + [Fact] + public async Task ByName_WithNamespace() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + using LayeredCraft.OptimizedEnums.SystemTextJson; + + namespace MyApp.Domain; + + [OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] + public sealed partial class OrderStatus : OptimizedEnum + { + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } + } + """, + ExpectedTrees = 3, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task ByValue_WithNamespace() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + using LayeredCraft.OptimizedEnums.SystemTextJson; + + namespace MyApp.Domain; + + [OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByValue)] + public sealed partial class OrderStatus : OptimizedEnum + { + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + public static readonly OrderStatus Paid = new(2, nameof(Paid)); + public static readonly OrderStatus Shipped = new(3, nameof(Shipped)); + + private OrderStatus(int value, string name) : base(value, name) { } + } + """, + ExpectedTrees = 3, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task ByName_GlobalNamespace() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + using LayeredCraft.OptimizedEnums.SystemTextJson; + + [OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] + public sealed partial class Priority : OptimizedEnum + { + public static readonly Priority Low = new(1, nameof(Low)); + public static readonly Priority Medium = new(2, nameof(Medium)); + public static readonly Priority High = new(3, nameof(High)); + + private Priority(int value, string name) : base(value, name) { } + } + """, + ExpectedTrees = 3, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task ByName_StringValueType() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + using LayeredCraft.OptimizedEnums.SystemTextJson; + + namespace MyApp.Domain; + + [OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] + public sealed partial class Color : OptimizedEnum + { + public static readonly Color Red = new("red", nameof(Red)); + public static readonly Color Green = new("green", nameof(Green)); + public static readonly Color Blue = new("blue", nameof(Blue)); + + private Color(string value, string name) : base(value, name) { } + } + """, + ExpectedTrees = 3, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task ByValue_StringValueType() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + using LayeredCraft.OptimizedEnums.SystemTextJson; + + namespace MyApp.Domain; + + [OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByValue)] + public sealed partial class Color : OptimizedEnum + { + public static readonly Color Red = new("red", nameof(Red)); + public static readonly Color Green = new("green", nameof(Green)); + public static readonly Color Blue = new("blue", nameof(Blue)); + + private Color(string value, string name) : base(value, name) { } + } + """, + ExpectedTrees = 3, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task ByName_NestedType() => + await GeneratorTestHelpers.Verify( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + using LayeredCraft.OptimizedEnums.SystemTextJson; + + namespace MyApp.Domain; + + public partial class Outer + { + [OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] + public sealed partial class Status : OptimizedEnum + { + public static readonly Status Active = new(1, nameof(Active)); + public static readonly Status Inactive = new(2, nameof(Inactive)); + + private Status(int value, string name) : base(value, name) { } + } + } + """, + ExpectedTrees = 3, + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task Error_NotOptimizedEnum() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums.SystemTextJson; + + namespace MyApp.Domain; + + [OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] + public sealed partial class NotAnEnum + { + } + """, + ExpectedDiagnosticId = "OE2001", + }, + TestContext.Current.CancellationToken); + + [Fact] + public async Task Error_NotPartial() => + await GeneratorTestHelpers.VerifyFailure( + new VerifyTestOptions + { + SourceCode = """ + using LayeredCraft.OptimizedEnums; + using LayeredCraft.OptimizedEnums.SystemTextJson; + + namespace MyApp.Domain; + + [OptimizedEnumJsonConverter(OptimizedEnumJsonConverterType.ByName)] + public sealed class OrderStatus : OptimizedEnum + { + public static readonly OrderStatus Pending = new(1, nameof(Pending)); + + private OrderStatus(int value, string name) : base(value, name) { } + } + """, + ExpectedDiagnosticId = "OE2002", + }, + TestContext.Current.CancellationToken); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests.csproj b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests.csproj new file mode 100644 index 0000000..f7588f5 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests.csproj @@ -0,0 +1,52 @@ + + + enable + enable + Exe + LayeredCraft.OptimizedEnums.SystemTextJson.Tests + net8.0;net9.0;net10.0 + true + false + default + MSB3243 + true + true + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/ModuleInitializer.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/ModuleInitializer.cs new file mode 100644 index 0000000..423cfbd --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/ModuleInitializer.cs @@ -0,0 +1,9 @@ +using System.Runtime.CompilerServices; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Tests; + +public static class ModuleInitializer +{ + [ModuleInitializer] + public static void Init() => VerifySourceGenerators.Initialize(); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..4586fe9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: OptimizedEnumJsonConverterAttribute.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace LayeredCraft.OptimizedEnums.SystemTextJson +{ + /// + /// Controls how an OptimizedEnum is serialized to and deserialized from JSON. + /// + public enum OptimizedEnumJsonConverterType + { + /// Serialize as the member's Name string (e.g. "Pending"). + ByName = 0, + + /// Serialize as the member's underlying Value (e.g. 1). + ByValue = 1, + } + + /// + /// Instructs the OptimizedEnums source generator to emit a System.Text.Json converter + /// for the decorated OptimizedEnum class and stamp it with [JsonConverter] automatically. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Class, + AllowMultiple = false, + Inherited = false)] + public sealed class OptimizedEnumJsonConverterAttribute : global::System.Attribute + { + /// Initializes a new instance. + public OptimizedEnumJsonConverterAttribute(OptimizedEnumJsonConverterType converterType) + { + ConverterType = converterType; + } + + /// Gets the serialization strategy for this converter. + public OptimizedEnumJsonConverterType ConverterType { get; } + } +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.SystemTextJson.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.SystemTextJson.g.verified.cs new file mode 100644 index 0000000..a749c20 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.SystemTextJson.g.verified.cs @@ -0,0 +1,44 @@ +//HintName: Priority.SystemTextJson.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +[global::System.Text.Json.Serialization.JsonConverter(typeof(PriorityNameJsonConverter))] +partial class Priority { } + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.SystemTextJson.Generator", "REPLACED")] +internal sealed class PriorityNameJsonConverter + : global::System.Text.Json.Serialization.JsonConverter +{ + + public override global::Priority Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options) + { + if (reader.TokenType != global::System.Text.Json.JsonTokenType.String) + throw new global::System.Text.Json.JsonException( + $"Expected a JSON string for Priority but got {reader.TokenType}."); + + var name = reader.GetString()!; + + if (!global::Priority.TryFromName(name, out var result)) + throw new global::System.Text.Json.JsonException( + $"'{name}' is not a valid name for Priority."); + + return result!; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + global::Priority value, + global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Name); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs new file mode 100644 index 0000000..591ee43 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs @@ -0,0 +1,101 @@ +//HintName: Priority.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Priority +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::Priority[] + { + Low, + Medium, + High + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Low.Name, + Medium.Name, + High.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Low.Value, + Medium.Value, + High.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal) + { + [Low.Name] = Low, + [Medium.Name] = Medium, + [High.Name] = High + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(3) + { + [Low.Value] = Low, + [Medium.Value] = Medium, + [High.Value] = High + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 3; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::Priority FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for Priority"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::Priority? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::Priority FromValue(int value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for Priority"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::Priority? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.SystemTextJson.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.SystemTextJson.g.verified.cs new file mode 100644 index 0000000..1c78bf0 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.SystemTextJson.g.verified.cs @@ -0,0 +1,49 @@ +//HintName: MyApp.Domain.Outer.Status.SystemTextJson.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +partial class Outer +{ +[global::System.Text.Json.Serialization.JsonConverter(typeof(StatusNameJsonConverter))] +partial class Status { } + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.SystemTextJson.Generator", "REPLACED")] +internal sealed class StatusNameJsonConverter + : global::System.Text.Json.Serialization.JsonConverter +{ + + public override global::MyApp.Domain.Outer.Status Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options) + { + if (reader.TokenType != global::System.Text.Json.JsonTokenType.String) + throw new global::System.Text.Json.JsonException( + $"Expected a JSON string for Status but got {reader.TokenType}."); + + var name = reader.GetString()!; + + if (!global::MyApp.Domain.Outer.Status.TryFromName(name, out var result)) + throw new global::System.Text.Json.JsonException( + $"'{name}' is not a valid name for Status."); + + return result!; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + global::MyApp.Domain.Outer.Status value, + global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Name); +} +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.g.verified.cs new file mode 100644 index 0000000..b25eccc --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.g.verified.cs @@ -0,0 +1,101 @@ +//HintName: MyApp.Domain.Outer.Status.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +partial class Outer +{ +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Status +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Outer.Status[] + { + Active, + Inactive + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Active.Name, + Inactive.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Active.Value, + Inactive.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(2, global::System.StringComparer.Ordinal) + { + [Active.Name] = Active, + [Inactive.Name] = Inactive + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(2) + { + [Active.Value] = Active, + [Inactive.Value] = Inactive + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 2; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Outer.Status FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for Status"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.Outer.Status? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Outer.Status FromValue(int value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for Status"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.Outer.Status? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); +} +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#OptimizedEnumJsonConverterAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#OptimizedEnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..23abac6 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#OptimizedEnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: OptimizedEnumJsonConverterAttribute.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace LayeredCraft.OptimizedEnums.SystemTextJson +{ + /// + /// Controls how an OptimizedEnum is serialized to and deserialized from JSON. + /// + public enum OptimizedEnumJsonConverterType + { + /// Serialize as the member's Name string (e.g. "Pending"). + ByName = 0, + + /// Serialize as the member's underlying Value (e.g. 1). + ByValue = 1, + } + + /// + /// Instructs the OptimizedEnums source generator to emit a System.Text.Json converter + /// for the decorated OptimizedEnum class and stamp it with [JsonConverter] automatically. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Class, + AllowMultiple = false, + Inherited = false)] + public sealed class OptimizedEnumJsonConverterAttribute : global::System.Attribute + { + /// Initializes a new instance. + public OptimizedEnumJsonConverterAttribute(OptimizedEnumJsonConverterType converterType) + { + ConverterType = converterType; + } + + /// Gets the serialization strategy for this converter. + public OptimizedEnumJsonConverterType ConverterType { get; } + } +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs new file mode 100644 index 0000000..ca4aa70 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: MyApp.Domain.Color.SystemTextJson.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.Text.Json.Serialization.JsonConverter(typeof(ColorNameJsonConverter))] +partial class Color { } + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.SystemTextJson.Generator", "REPLACED")] +internal sealed class ColorNameJsonConverter + : global::System.Text.Json.Serialization.JsonConverter +{ + + public override global::MyApp.Domain.Color Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options) + { + if (reader.TokenType != global::System.Text.Json.JsonTokenType.String) + throw new global::System.Text.Json.JsonException( + $"Expected a JSON string for Color but got {reader.TokenType}."); + + var name = reader.GetString()!; + + if (!global::MyApp.Domain.Color.TryFromName(name, out var result)) + throw new global::System.Text.Json.JsonException( + $"'{name}' is not a valid name for Color."); + + return result!; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + global::MyApp.Domain.Color value, + global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Name); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs new file mode 100644 index 0000000..f48c64b --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs @@ -0,0 +1,103 @@ +//HintName: MyApp.Domain.Color.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Color +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Color[] + { + Red, + Green, + Blue + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Red.Name, + Green.Name, + Blue.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new string[] + { + Red.Value, + Green.Value, + Blue.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal) + { + [Red.Name] = Red, + [Green.Name] = Green, + [Blue.Name] = Blue + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(3) + { + [Red.Value] = Red, + [Green.Value] = Green, + [Blue.Value] = Blue + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 3; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Color FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for Color"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.Color? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Color FromValue(string value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for Color"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(string value, out global::MyApp.Domain.Color? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(string value) => s_byValue.ContainsKey(value); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#OptimizedEnumJsonConverterAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#OptimizedEnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..4586fe9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#OptimizedEnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: OptimizedEnumJsonConverterAttribute.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace LayeredCraft.OptimizedEnums.SystemTextJson +{ + /// + /// Controls how an OptimizedEnum is serialized to and deserialized from JSON. + /// + public enum OptimizedEnumJsonConverterType + { + /// Serialize as the member's Name string (e.g. "Pending"). + ByName = 0, + + /// Serialize as the member's underlying Value (e.g. 1). + ByValue = 1, + } + + /// + /// Instructs the OptimizedEnums source generator to emit a System.Text.Json converter + /// for the decorated OptimizedEnum class and stamp it with [JsonConverter] automatically. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Class, + AllowMultiple = false, + Inherited = false)] + public sealed class OptimizedEnumJsonConverterAttribute : global::System.Attribute + { + /// Initializes a new instance. + public OptimizedEnumJsonConverterAttribute(OptimizedEnumJsonConverterType converterType) + { + ConverterType = converterType; + } + + /// Gets the serialization strategy for this converter. + public OptimizedEnumJsonConverterType ConverterType { get; } + } +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs new file mode 100644 index 0000000..7fc04a4 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: MyApp.Domain.OrderStatus.SystemTextJson.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.Text.Json.Serialization.JsonConverter(typeof(OrderStatusNameJsonConverter))] +partial class OrderStatus { } + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.SystemTextJson.Generator", "REPLACED")] +internal sealed class OrderStatusNameJsonConverter + : global::System.Text.Json.Serialization.JsonConverter +{ + + public override global::MyApp.Domain.OrderStatus Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options) + { + if (reader.TokenType != global::System.Text.Json.JsonTokenType.String) + throw new global::System.Text.Json.JsonException( + $"Expected a JSON string for OrderStatus but got {reader.TokenType}."); + + var name = reader.GetString()!; + + if (!global::MyApp.Domain.OrderStatus.TryFromName(name, out var result)) + throw new global::System.Text.Json.JsonException( + $"'{name}' is not a valid name for OrderStatus."); + + return result!; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + global::MyApp.Domain.OrderStatus value, + global::System.Text.Json.JsonSerializerOptions options) + => writer.WriteStringValue(value.Name); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs new file mode 100644 index 0000000..b85c817 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs @@ -0,0 +1,103 @@ +//HintName: MyApp.Domain.OrderStatus.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class OrderStatus +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.OrderStatus[] + { + Pending, + Paid, + Shipped + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Pending.Name, + Paid.Name, + Shipped.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Pending.Value, + Paid.Value, + Shipped.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal) + { + [Pending.Name] = Pending, + [Paid.Name] = Paid, + [Shipped.Name] = Shipped + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(3) + { + [Pending.Value] = Pending, + [Paid.Value] = Paid, + [Shipped.Value] = Shipped + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 3; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.OrderStatus FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.OrderStatus FromValue(int value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..4586fe9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: OptimizedEnumJsonConverterAttribute.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace LayeredCraft.OptimizedEnums.SystemTextJson +{ + /// + /// Controls how an OptimizedEnum is serialized to and deserialized from JSON. + /// + public enum OptimizedEnumJsonConverterType + { + /// Serialize as the member's Name string (e.g. "Pending"). + ByName = 0, + + /// Serialize as the member's underlying Value (e.g. 1). + ByValue = 1, + } + + /// + /// Instructs the OptimizedEnums source generator to emit a System.Text.Json converter + /// for the decorated OptimizedEnum class and stamp it with [JsonConverter] automatically. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Class, + AllowMultiple = false, + Inherited = false)] + public sealed class OptimizedEnumJsonConverterAttribute : global::System.Attribute + { + /// Initializes a new instance. + public OptimizedEnumJsonConverterAttribute(OptimizedEnumJsonConverterType converterType) + { + ConverterType = converterType; + } + + /// Gets the serialization strategy for this converter. + public OptimizedEnumJsonConverterType ConverterType { get; } + } +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs new file mode 100644 index 0000000..7f63ce4 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs @@ -0,0 +1,47 @@ +//HintName: MyApp.Domain.Color.SystemTextJson.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.Text.Json.Serialization.JsonConverter(typeof(ColorValueJsonConverter))] +partial class Color { } + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.SystemTextJson.Generator", "REPLACED")] +internal sealed class ColorValueJsonConverter + : global::System.Text.Json.Serialization.JsonConverter +{ + + public override global::MyApp.Domain.Color Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options) + { + + if (reader.TokenType == global::System.Text.Json.JsonTokenType.Null) + throw new global::System.Text.Json.JsonException( + $"Expected a non-null JSON value for Color."); + + var value = global::System.Text.Json.JsonSerializer.Deserialize(ref reader, options); + + if (value is null || !global::MyApp.Domain.Color.TryFromValue(value, out var result)) + throw new global::System.Text.Json.JsonException( + $"'{value}' is not a valid value for Color."); + + return result!; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + global::MyApp.Domain.Color value, + global::System.Text.Json.JsonSerializerOptions options) + => global::System.Text.Json.JsonSerializer.Serialize(writer, value.Value, options); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs new file mode 100644 index 0000000..f48c64b --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs @@ -0,0 +1,103 @@ +//HintName: MyApp.Domain.Color.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class Color +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.Color[] + { + Red, + Green, + Blue + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Red.Name, + Green.Name, + Blue.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new string[] + { + Red.Value, + Green.Value, + Blue.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal) + { + [Red.Name] = Red, + [Green.Name] = Green, + [Blue.Name] = Blue + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(3) + { + [Red.Value] = Red, + [Green.Value] = Green, + [Blue.Value] = Blue + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 3; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Color FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for Color"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.Color? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.Color FromValue(string value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for Color"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(string value, out global::MyApp.Domain.Color? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(string value) => s_byValue.ContainsKey(value); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#OptimizedEnumJsonConverterAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#OptimizedEnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..4586fe9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#OptimizedEnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: OptimizedEnumJsonConverterAttribute.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace LayeredCraft.OptimizedEnums.SystemTextJson +{ + /// + /// Controls how an OptimizedEnum is serialized to and deserialized from JSON. + /// + public enum OptimizedEnumJsonConverterType + { + /// Serialize as the member's Name string (e.g. "Pending"). + ByName = 0, + + /// Serialize as the member's underlying Value (e.g. 1). + ByValue = 1, + } + + /// + /// Instructs the OptimizedEnums source generator to emit a System.Text.Json converter + /// for the decorated OptimizedEnum class and stamp it with [JsonConverter] automatically. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Class, + AllowMultiple = false, + Inherited = false)] + public sealed class OptimizedEnumJsonConverterAttribute : global::System.Attribute + { + /// Initializes a new instance. + public OptimizedEnumJsonConverterAttribute(OptimizedEnumJsonConverterType converterType) + { + ConverterType = converterType; + } + + /// Gets the serialization strategy for this converter. + public OptimizedEnumJsonConverterType ConverterType { get; } + } +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs new file mode 100644 index 0000000..339c8d8 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs @@ -0,0 +1,43 @@ +//HintName: MyApp.Domain.OrderStatus.SystemTextJson.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.Text.Json.Serialization.JsonConverter(typeof(OrderStatusValueJsonConverter))] +partial class OrderStatus { } + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.SystemTextJson.Generator", "REPLACED")] +internal sealed class OrderStatusValueJsonConverter + : global::System.Text.Json.Serialization.JsonConverter +{ + + public override global::MyApp.Domain.OrderStatus Read( + ref global::System.Text.Json.Utf8JsonReader reader, + global::System.Type typeToConvert, + global::System.Text.Json.JsonSerializerOptions options) + { + + var value = global::System.Text.Json.JsonSerializer.Deserialize(ref reader, options); + + if (!global::MyApp.Domain.OrderStatus.TryFromValue(value, out var result)) + throw new global::System.Text.Json.JsonException( + $"'{value}' is not a valid value for OrderStatus."); + + return result!; + } + + public override void Write( + global::System.Text.Json.Utf8JsonWriter writer, + global::MyApp.Domain.OrderStatus value, + global::System.Text.Json.JsonSerializerOptions options) + => global::System.Text.Json.JsonSerializer.Serialize(writer, value.Value, options); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs new file mode 100644 index 0000000..b85c817 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs @@ -0,0 +1,103 @@ +//HintName: MyApp.Domain.OrderStatus.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace MyApp.Domain; + +[global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] +partial class OrderStatus +{ + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_all = + global::System.Array.AsReadOnly(new global::MyApp.Domain.OrderStatus[] + { + Pending, + Paid, + Shipped + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_names = + global::System.Array.AsReadOnly(new string[] + { + Pending.Name, + Paid.Name, + Shipped.Name + }); + + private static readonly global::System.Collections.ObjectModel.ReadOnlyCollection s_values = + global::System.Array.AsReadOnly(new int[] + { + Pending.Value, + Paid.Value, + Shipped.Value + }); + + private static readonly global::System.Collections.Generic.Dictionary s_byName = + new global::System.Collections.Generic.Dictionary(3, global::System.StringComparer.Ordinal) + { + [Pending.Name] = Pending, + [Paid.Name] = Paid, + [Shipped.Name] = Shipped + }; + + private static readonly global::System.Collections.Generic.Dictionary s_byValue = + new global::System.Collections.Generic.Dictionary(3) + { + [Pending.Value] = Pending, + [Paid.Value] = Paid, + [Shipped.Value] = Shipped + }; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList All => s_all; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Names => s_names; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::System.Collections.Generic.IReadOnlyList Values => s_values; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public const int Count = 3; + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.OrderStatus FromName(string name) + { + if (!s_byName.TryGetValue(name, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{name}' is not a valid name for OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromName(string name, out global::MyApp.Domain.OrderStatus? result) => + s_byName.TryGetValue(name, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static global::MyApp.Domain.OrderStatus FromValue(int value) + { + if (!s_byValue.TryGetValue(value, out var result)) + throw new global::System.Collections.Generic.KeyNotFoundException( + $"'{value}' is not a valid value for OrderStatus"); + + return result; + } + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool TryFromValue(int value, out global::MyApp.Domain.OrderStatus? result) => + s_byValue.TryGetValue(value, out result); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsName(string name) => s_byName.ContainsKey(name); + + [global::System.CodeDom.Compiler.GeneratedCode("LayeredCraft.OptimizedEnums.Generator", "REPLACED")] + public static bool ContainsValue(int value) => s_byValue.ContainsKey(value); +} diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..4586fe9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: OptimizedEnumJsonConverterAttribute.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace LayeredCraft.OptimizedEnums.SystemTextJson +{ + /// + /// Controls how an OptimizedEnum is serialized to and deserialized from JSON. + /// + public enum OptimizedEnumJsonConverterType + { + /// Serialize as the member's Name string (e.g. "Pending"). + ByName = 0, + + /// Serialize as the member's underlying Value (e.g. 1). + ByValue = 1, + } + + /// + /// Instructs the OptimizedEnums source generator to emit a System.Text.Json converter + /// for the decorated OptimizedEnum class and stamp it with [JsonConverter] automatically. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Class, + AllowMultiple = false, + Inherited = false)] + public sealed class OptimizedEnumJsonConverterAttribute : global::System.Attribute + { + /// Initializes a new instance. + public OptimizedEnumJsonConverterAttribute(OptimizedEnumJsonConverterType converterType) + { + ConverterType = converterType; + } + + /// Gets the serialization strategy for this converter. + public OptimizedEnumJsonConverterType ConverterType { get; } + } +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotOptimizedEnum#OptimizedEnumJsonConverterAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotOptimizedEnum#OptimizedEnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..4586fe9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotOptimizedEnum#OptimizedEnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: OptimizedEnumJsonConverterAttribute.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace LayeredCraft.OptimizedEnums.SystemTextJson +{ + /// + /// Controls how an OptimizedEnum is serialized to and deserialized from JSON. + /// + public enum OptimizedEnumJsonConverterType + { + /// Serialize as the member's Name string (e.g. "Pending"). + ByName = 0, + + /// Serialize as the member's underlying Value (e.g. 1). + ByValue = 1, + } + + /// + /// Instructs the OptimizedEnums source generator to emit a System.Text.Json converter + /// for the decorated OptimizedEnum class and stamp it with [JsonConverter] automatically. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Class, + AllowMultiple = false, + Inherited = false)] + public sealed class OptimizedEnumJsonConverterAttribute : global::System.Attribute + { + /// Initializes a new instance. + public OptimizedEnumJsonConverterAttribute(OptimizedEnumJsonConverterType converterType) + { + ConverterType = converterType; + } + + /// Gets the serialization strategy for this converter. + public OptimizedEnumJsonConverterType ConverterType { get; } + } +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotOptimizedEnum.verified.txt b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotOptimizedEnum.verified.txt new file mode 100644 index 0000000..1a15aad --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotOptimizedEnum.verified.txt @@ -0,0 +1,17 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (4,0)-(7,1), + Message: The class 'NotAnEnum' must inherit from OptimizedEnum to use [OptimizedEnumJsonConverter], + Severity: Error, + Descriptor: { + Id: OE2001, + Title: OptimizedEnumJsonConverter requires an OptimizedEnum subclass, + MessageFormat: The class '{0}' must inherit from OptimizedEnum to use [OptimizedEnumJsonConverter], + Category: OptimizedEnums.SystemTextJson, + DefaultSeverity: Error, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial#OptimizedEnumJsonConverterAttribute.g.verified.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial#OptimizedEnumJsonConverterAttribute.g.verified.cs new file mode 100644 index 0000000..4586fe9 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial#OptimizedEnumJsonConverterAttribute.g.verified.cs @@ -0,0 +1,46 @@ +//HintName: OptimizedEnumJsonConverterAttribute.g.cs +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +#nullable enable + +namespace LayeredCraft.OptimizedEnums.SystemTextJson +{ + /// + /// Controls how an OptimizedEnum is serialized to and deserialized from JSON. + /// + public enum OptimizedEnumJsonConverterType + { + /// Serialize as the member's Name string (e.g. "Pending"). + ByName = 0, + + /// Serialize as the member's underlying Value (e.g. 1). + ByValue = 1, + } + + /// + /// Instructs the OptimizedEnums source generator to emit a System.Text.Json converter + /// for the decorated OptimizedEnum class and stamp it with [JsonConverter] automatically. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Class, + AllowMultiple = false, + Inherited = false)] + public sealed class OptimizedEnumJsonConverterAttribute : global::System.Attribute + { + /// Initializes a new instance. + public OptimizedEnumJsonConverterAttribute(OptimizedEnumJsonConverterType converterType) + { + ConverterType = converterType; + } + + /// Gets the serialization strategy for this converter. + public OptimizedEnumJsonConverterType ConverterType { get; } + } +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial.verified.txt b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial.verified.txt new file mode 100644 index 0000000..286e26f --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial.verified.txt @@ -0,0 +1,30 @@ +{ + Diagnostics: [ + { + Location: Program.cs: (5,0)-(11,1), + Message: The class 'OrderStatus' must be declared as partial for OptimizedEnum source generation, + Severity: Error, + Descriptor: { + Id: OE0001, + Title: OptimizedEnum class must be partial, + MessageFormat: The class '{0}' must be declared as partial for OptimizedEnum source generation, + Category: OptimizedEnums.Usage, + DefaultSeverity: Error, + IsEnabledByDefault: true + } + }, + { + Location: Program.cs: (5,0)-(11,1), + Message: The class 'OrderStatus' must be declared as partial for [OptimizedEnumJsonConverter] source generation, + Severity: Error, + Descriptor: { + Id: OE2002, + Title: OptimizedEnum class must be partial for JSON converter generation, + MessageFormat: The class '{0}' must be declared as partial for [OptimizedEnumJsonConverter] source generation, + Category: OptimizedEnums.SystemTextJson, + DefaultSeverity: Error, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/xunit.runner.json b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/xunit.runner.json new file mode 100644 index 0000000..86c7ea0 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..0e72df8 --- /dev/null +++ b/uv.lock @@ -0,0 +1,139 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "deepmerge" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" }, +] + +[[package]] +name = "layeredcraft-optimized-enums-docs" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "zensical" }, +] + +[package.metadata] +requires-dist = [{ name = "zensical", specifier = ">=0.0.21" }] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "zensical" +version = "0.0.30" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "deepmerge" }, + { name = "markdown" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/53/5e551f8912718816733a75adcb53a0787b2d2edca5869c156325aaf82e24/zensical-0.0.30.tar.gz", hash = "sha256:408b531683f6bcb6cc5ab928146d2c68afbc16fac4eda87ae3dd20af1498180f", size = 3844287, upload-time = "2026-03-28T17:55:52.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/e3/ac0eb77a8a7f793613813de68bde26776d0da68d8041fa9eb8d0b986a449/zensical-0.0.30-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b67fca8bfcd71c94b331045a591bf6e24fe123a66fba94587aa3379faf521a16", size = 12313786, upload-time = "2026-03-28T17:55:18.839Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6a/73e461dfa27d3bc415e48396f83a3287b43df2fd3361e25146bc86360aab/zensical-0.0.30-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:8ceadfece1153edc26506e8ddf68d9818afe8517cf3bcdb6bfe4cb2793ae247b", size = 12186136, upload-time = "2026-03-28T17:55:21.836Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bc/9022156b4c28c1b95209acb64319b1e5cd0af2e97035bdd461e58408cb46/zensical-0.0.30-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e100b2b654337ac5306ba12818f3c5336c66d0d34c593ef05e316c124a5819cb", size = 12556115, upload-time = "2026-03-28T17:55:24.849Z" }, + { url = "https://files.pythonhosted.org/packages/0b/29/9e8f5bd6d33b35f4c368ae8b13d431dc42b2de17ea6eccbd71d48122eba6/zensical-0.0.30-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdf641ffddaf21c6971b91a4426b81cd76271c5b1adb7176afcce3f1508328b1", size = 12498121, upload-time = "2026-03-28T17:55:27.637Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e1/b8dfa0769050e62cd731358145fdeb67af35e322197bd7e7727250596e7b/zensical-0.0.30-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd909a0c2116e26190c7f3ec4fb55837c417b7a8d99ebf4f3deb26b07b97e49", size = 12854142, upload-time = "2026-03-28T17:55:30.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/11/62a36cfb81522b6108db8f9e96d36da8cccb306b02c15ad19e1b333fa7c8/zensical-0.0.30-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16fd2da09fe4e5cbec2ca74f31abc70f32f7330d56593b647e0a114bb329171a", size = 12598341, upload-time = "2026-03-28T17:55:32.988Z" }, + { url = "https://files.pythonhosted.org/packages/a7/a4/8c7a6725fb226aa71d19209403d974e45f39d757e725f9558c6ed8d350a5/zensical-0.0.30-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:896b36eaef7fed5f8fc6f2c8264b2751aad63c2d66d3d8650e38481b6b4f6f7b", size = 12732307, upload-time = "2026-03-28T17:55:35.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a1/7858fb3f6ac67d7d24a8acbe834cbe26851d6bd151ece6fba3fc88b0f878/zensical-0.0.30-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:a1f515ec67a0d0250e53846327bf0c69635a1f39749da3b04feb68431188d3c6", size = 12770962, upload-time = "2026-03-28T17:55:38.627Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/228298112a69d0b74e6e93041bffcf1fc96d03cf252be94a354f277d4789/zensical-0.0.30-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:ce33d1002438838a35fa43358a1f43d74f874586596d3d116999d3756cded00e", size = 12919256, upload-time = "2026-03-28T17:55:41.413Z" }, + { url = "https://files.pythonhosted.org/packages/de/c7/5b4ea036f7f7d84abf907f7f7a3e8420b054c89279c5273ca248d3bc9f48/zensical-0.0.30-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:029dad561568f4ae3056dde16a81012efd92c426d4eb7101f960f448c1168196", size = 12869760, upload-time = "2026-03-28T17:55:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/36/b4/77bef2132e43108db718ae014a5961fc511e88fc446c11f1c3483def429e/zensical-0.0.30-cp310-abi3-win32.whl", hash = "sha256:0105672850f053c326fba9fdd95adf60e9f90308f8cc1c08e3a00e15a8d5e90f", size = 11905658, upload-time = "2026-03-28T17:55:47.416Z" }, + { url = "https://files.pythonhosted.org/packages/a1/59/23b6c7ff062e2b299cc60e333095e853f9d38d1b5abe743c7b94c4ac432c/zensical-0.0.30-cp310-abi3-win_amd64.whl", hash = "sha256:b879dbf4c69d3ea41694bae33e1b948847e635dcbcd6ec8c522920833379dd48", size = 12101867, upload-time = "2026-03-28T17:55:50.083Z" }, +] diff --git a/zensical.toml b/zensical.toml new file mode 100644 index 0000000..4a57de8 --- /dev/null +++ b/zensical.toml @@ -0,0 +1,127 @@ +[project] +site_name = "LayeredCraft.OptimizedEnums" +site_description = "High-performance, AOT-safe alternative to SmartEnum patterns using source generation" +site_author = "Nick Cipollina" +site_url = "https://layeredcraft.github.io/optimized-enums/" +repo_name = "layeredcraft/optimized-enums" +repo_url = "https://github.com/layeredcraft/optimized-enums" +edit_uri = "edit/main/docs/" +copyright = "Copyright © 2025 LayeredCraft" +docs_dir = "docs" + +nav = [ + { "Home" = "index.md" }, + { "Getting Started" = [ + { "Installation" = "getting-started/installation.md" }, + { "Quick Start" = "getting-started/quick-start.md" } + ] }, + { "Core Concepts" = [ + { "How It Works" = "core-concepts/how-it-works.md" }, + { "Source Generators" = "core-concepts/source-generators.md" }, + { "Inheritance Model" = "core-concepts/inheritance-model.md" } + ] }, + { "Usage" = [ + { "Defining Enums" = "usage/defining-enums.md" }, + { "Lookups & Queries" = "usage/lookups.md" }, + { "String Values" = "usage/string-values.md" }, + { "JSON Serialization" = "usage/json-serialization.md" } + ] }, + { "Advanced" = [ + { "Performance" = "advanced/performance.md" }, + { "Diagnostics" = "advanced/diagnostics.md" }, + { "AOT & Trimming" = "advanced/aot-trimming.md" } + ] }, + { "API Reference" = [ + { "Base Class" = "api-reference/base-class.md" }, + { "Generated Members" = "api-reference/generated-members.md" } + ] }, + { "Contributing" = "contributing.md" }, + { "Changelog" = "changelog.md" } +] + +[project.markdown_extensions.abbr] +[project.markdown_extensions.admonition] +[project.markdown_extensions.attr_list] +[project.markdown_extensions.def_list] +[project.markdown_extensions.footnotes] +[project.markdown_extensions.md_in_html] + +[project.markdown_extensions.toc] +permalink = true + +[project.markdown_extensions.pymdownx.betterem] +smart_enable = "all" + +[project.markdown_extensions.pymdownx.caret] +[project.markdown_extensions.pymdownx.details] +[project.markdown_extensions.pymdownx.emoji] +[project.markdown_extensions.pymdownx.inlinehilite] +[project.markdown_extensions.pymdownx.keys] +[project.markdown_extensions.pymdownx.mark] +[project.markdown_extensions.pymdownx.smartsymbols] +[project.markdown_extensions.pymdownx.superfences] +[project.markdown_extensions.pymdownx.tilde] + +[project.markdown_extensions.pymdownx.highlight] +anchor_linenums = true +line_spans = "__span" +pygments_lang_class = true + +[project.markdown_extensions.pymdownx.tabbed] +alternate_style = true + +[project.markdown_extensions.pymdownx.tasklist] +custom_checkbox = true + +[project.theme] +logo = "assets/icon.png" +favicon = "assets/icon.png" +language = "en" +features = [ + "content.code.copy", + "content.code.select", + "navigation.expand", + "navigation.footer", + "navigation.instant", + "navigation.sections", + "navigation.tabs", + "navigation.tabs.sticky", + "navigation.top", + "search.highlight", + "search.share", + "search.suggest", + "toc.follow" +] + +[project.theme.font] +text = "Roboto" +code = "Roboto Mono" + +[[project.theme.palette]] +media = "(prefers-color-scheme: light)" +scheme = "default" +primary = "deep purple" +accent = "deep purple" +toggle.icon = "material/brightness-7" +toggle.name = "Switch to dark mode" + +[[project.theme.palette]] +media = "(prefers-color-scheme: dark)" +scheme = "slate" +primary = "deep purple" +accent = "deep purple" +toggle.icon = "material/brightness-4" +toggle.name = "Switch to light mode" + +[project.extra] +homepage = "https://github.com/layeredcraft/optimized-enums" + +[[project.extra.social]] +icon = "fontawesome/brands/github" +link = "https://github.com/layeredcraft/optimized-enums" +name = "GitHub Repository" + +[[project.extra.social]] +icon = "fontawesome/solid/download" +link = "https://www.nuget.org/packages/LayeredCraft.OptimizedEnums/" +name = "NuGet Package" \ No newline at end of file