From f918e875cd119f1ed32767c7ddebf06726fd7809 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 30 Mar 2026 22:13:56 -0400 Subject: [PATCH 01/14] feat: add LayeredCraft.OptimizedEnums.SystemTextJson source generator Adds a new source generator package that emits zero-reflection, AOT-safe System.Text.Json converters for OptimizedEnum types. Decorate any OptimizedEnum subclass with [OptimizedEnumJsonConverter(ByName|ByValue)] and the generator stamps [JsonConverter] on a generated partial class and emits a concrete, non-generic converter calling TryFromName/TryFromValue directly. The attribute and OptimizedEnumJsonConverterType enum are injected into consuming compilations via RegisterPostInitializationOutput (no separate runtime DLL needed). Converter code is rendered via a Scriban template consistent with the main generator. Two diagnostics are emitted for invalid usage: OE2001 (must inherit OptimizedEnum) and OE2002 (must be partial). All 21 snapshot tests pass across net8.0/net9.0/net10.0. Also bumps System.Text.Json to 10.0.5 (Scriban 7.0.6 requires it) and adds SCRIBAN_NO_SYSTEM_TEXT_JSON to both generator projects so Scriban's STJ integration code is compiled out. Co-Authored-By: Claude Sonnet 4.6 --- Directory.Packages.props | 1 + LayeredCraft.OptimizedEnums.slnx | 2 + ...yeredCraft.OptimizedEnums.Generator.csproj | 1 + .../AnalyzerReleases.Shipped.md | 2 + .../AnalyzerReleases.Unshipped.md | 9 + .../AttributeSource.cs | 59 ++++++ .../Diagnostics/DiagnosticDescriptors.cs | 24 +++ .../Diagnostics/DiagnosticInfo.cs | 42 +++++ .../Emitters/JsonConverterEmitter.cs | 83 +++++++++ .../Emitters/TemplateHelper.cs | 60 +++++++ ...mizedEnums.SystemTextJson.Generator.csproj | 73 ++++++++ .../Models/EquatableArray.cs | 50 ++++++ .../Models/JsonConverterInfo.cs | 31 ++++ .../Models/LocationInfo.cs | 38 ++++ .../OptimizedEnumJsonConverterGenerator.cs | 38 ++++ .../OptimizedEnumJsonConverterType.cs | 8 + .../Providers/JsonConverterSyntaxProvider.cs | 151 ++++++++++++++++ .../Templates/JsonConverter.scriban | 65 +++++++ .../TrackingNames.cs | 9 + .../GeneratorTestHelpers.cs | 147 +++++++++++++++ .../GeneratorVerifyTests.cs | 169 ++++++++++++++++++ ...OptimizedEnums.SystemTextJson.Tests.csproj | 52 ++++++ .../ModuleInitializer.cs | 9 + ...edEnumJsonConverterAttribute.g.verified.cs | 46 +++++ ...pace#Priority.SystemTextJson.g.verified.cs | 44 +++++ ...ame_GlobalNamespace#Priority.g.verified.cs | 101 +++++++++++ ....Domain.Color.SystemTextJson.g.verified.cs | 46 +++++ ...ValueType#MyApp.Domain.Color.g.verified.cs | 103 +++++++++++ ...edEnumJsonConverterAttribute.g.verified.cs | 46 +++++ ...n.OrderStatus.SystemTextJson.g.verified.cs | 46 +++++ ...ace#MyApp.Domain.OrderStatus.g.verified.cs | 103 +++++++++++ ...edEnumJsonConverterAttribute.g.verified.cs | 46 +++++ ....Domain.Color.SystemTextJson.g.verified.cs | 42 +++++ ...ValueType#MyApp.Domain.Color.g.verified.cs | 103 +++++++++++ ...edEnumJsonConverterAttribute.g.verified.cs | 46 +++++ ...n.OrderStatus.SystemTextJson.g.verified.cs | 42 +++++ ...ace#MyApp.Domain.OrderStatus.g.verified.cs | 103 +++++++++++ ...edEnumJsonConverterAttribute.g.verified.cs | 46 +++++ ...edEnumJsonConverterAttribute.g.verified.cs | 46 +++++ ...yTests.Error_NotOptimizedEnum.verified.txt | 17 ++ ...edEnumJsonConverterAttribute.g.verified.cs | 46 +++++ ...rVerifyTests.Error_NotPartial.verified.txt | 30 ++++ .../xunit.runner.json | 3 + 43 files changed, 2228 insertions(+) create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Shipped.md create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Unshipped.md create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AttributeSource.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticDescriptors.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticInfo.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/TemplateHelper.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/EquatableArray.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/JsonConverterInfo.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/LocationInfo.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterGenerator.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterType.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban create mode 100644 src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/TrackingNames.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorTestHelpers.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorVerifyTests.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests.csproj create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/ModuleInitializer.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.SystemTextJson.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_GlobalNamespace#Priority.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#MyApp.Domain.Color.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_StringValueType#OptimizedEnumJsonConverterAttribute.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_WithNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#OptimizedEnumJsonConverterAttribute.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#OptimizedEnumJsonConverterAttribute.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotOptimizedEnum#OptimizedEnumJsonConverterAttribute.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotOptimizedEnum.verified.txt create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial#OptimizedEnumJsonConverterAttribute.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.Error_NotPartial.verified.txt create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/xunit.runner.json 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..10d629b 100644 --- a/LayeredCraft.OptimizedEnums.slnx +++ b/LayeredCraft.OptimizedEnums.slnx @@ -51,10 +51,12 @@ + + \ No newline at end of file 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..a3b6a57 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Unshipped.md @@ -0,0 +1,9 @@ +; 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 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..c36ac0a --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,24 @@ +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); +} 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..3b0a564 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs @@ -0,0 +1,83 @@ +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 readonly string GeneratedCodeAttribute = 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, + 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( + new DiagnosticDescriptor( + "OE9002", + "OptimizedEnums SystemTextJson generator internal error", + "An unexpected error occurred while generating the JSON converter for '{0}': {1}", + "OptimizedEnums.SystemTextJson", + DiagnosticSeverity.Error, + isEnabledByDefault: true), + 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..90757e3 --- /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.InvariantCulture)); + + 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..c27a66e --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj @@ -0,0 +1,73 @@ + + + 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 + + + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + 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..41c3138 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/JsonConverterInfo.cs @@ -0,0 +1,31 @@ +using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Diagnostics; + +namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Models; + +internal sealed record JsonConverterInfo( + string? Namespace, + string ClassName, + string FullyQualifiedClassName, + string ValueTypeFullyQualified, + 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 + && ContainingTypeNames == other.ContainingTypeNames + && ConverterType == other.ConverterType + && Diagnostics == other.Diagnostics; + + public override int GetHashCode() => + HashCode.Combine(Namespace, ClassName, FullyQualifiedClassName, ValueTypeFullyQualified, + 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..63e61aa --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterGenerator.cs @@ -0,0 +1,38 @@ +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 + .CreateSyntaxProvider( + 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..3687a8a --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs @@ -0,0 +1,151 @@ +using System.Collections.Immutable; +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 +{ + private 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 { AttributeLists.Count: > 0 }; + + internal static JsonConverterInfo? Transform( + GeneratorSyntaxContext context, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (context.Node is not ClassDeclarationSyntax classDecl) + return null; + + if (context.SemanticModel.GetDeclaredSymbol(classDecl, cancellationToken) + is not { } classSymbol) + return null; + + // Only process classes with [OptimizedEnumJsonConverter] + var attributeType = context.SemanticModel.Compilation + .GetTypeByMetadataName(AttributeMetadataName); + if (attributeType is null) + return null; + + var attr = classSymbol.GetAttributes() + .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); + if (attr is null) + return null; + + 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, + 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, + 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) + 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), + 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..b225f68 --- /dev/null +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban @@ -0,0 +1,65 @@ +//------------------------------------------------------------------------------ +// +// 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) + { + 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 }}."); + + 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..2c10d57 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorVerifyTests.cs @@ -0,0 +1,169 @@ +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 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_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..bc9ac75 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_StringValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs @@ -0,0 +1,42 @@ +//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) + { + var value = global::System.Text.Json.JsonSerializer.Deserialize(ref reader, options); + + if (!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..d505de3 --- /dev/null +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByValue_WithNamespace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs @@ -0,0 +1,42 @@ +//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" +} From 5986cfc0edfe82b423264d57541686d9f5a489df Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 30 Mar 2026 22:23:55 -0400 Subject: [PATCH 02/14] feat: declare LayeredCraft.OptimizedEnums as a public dependency of the STJ package Adds a ProjectReference (ReferenceOutputAssembly=false) from the STJ generator to the main generator so that NuGet pack includes LayeredCraft.OptimizedEnums in the .nuspec dependency group. Consumers now only need to install LayeredCraft.OptimizedEnums.SystemTextJson and the core package is pulled in automatically. Co-Authored-By: Claude Sonnet 4.6 --- ...redCraft.OptimizedEnums.SystemTextJson.Generator.csproj | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj index c27a66e..16fff0d 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj @@ -18,6 +18,13 @@ true + + + + + From aaa8315ecf1843959b8d5689c7e3ef715cbd83ea Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Mon, 30 Mar 2026 22:31:42 -0400 Subject: [PATCH 03/14] fix: resolve Microsoft.CSharp duplicate reference by removing direct dep from STJ generator The plain ProjectReference to the main generator caused CS1703 because both projects declared Microsoft.CSharp, resulting in both lib/ and ref/ versions being added to the compile references. Since the main generator's GetDependencyTargetPaths target makes Microsoft.CSharp.dll available transitively, the STJ generator no longer needs its own direct reference, pack item, or GetDependencyTargetPaths target for it. The ProjectReference (without ReferenceOutputAssembly=false) now correctly declares LayeredCraft.OptimizedEnums as a public NuGet dependency, so consumers only need to install LayeredCraft.OptimizedEnums.SystemTextJson. Co-Authored-By: Claude Sonnet 4.6 --- ...mizedEnums.SystemTextJson.Generator.csproj | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj index 16fff0d..cb55b51 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/LayeredCraft.OptimizedEnums.SystemTextJson.Generator.csproj @@ -19,33 +19,17 @@ - + + Include="..\LayeredCraft.OptimizedEnums.Generator\LayeredCraft.OptimizedEnums.Generator.csproj" /> - - - - $(GetTargetPathDependsOn);GetDependencyTargetPaths - - - - - - - @@ -77,4 +61,4 @@ PackagePath="analyzers/dotnet/cs" Visible="false" /> - + \ No newline at end of file From 79dea795ff8cbea729218d2d33877362cf672975 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 07:36:17 -0400 Subject: [PATCH 04/14] feat: add SystemTextJson package docs and bump version to 1.1.0 - Add docs/usage/json-serialization.md covering ByName/ByValue strategies, what gets generated, string-valued enums, AOT safety, and OE2xxx diagnostics - Update README with SystemTextJson NuGet badge and JSON serialization section - Update installation, quick-start, diagnostics, and changelog docs - Add json-serialization.md to mkdocs.yml nav and .slnx solution folder - Bump VersionPrefix from 1.0.0 to 1.1.0 Co-Authored-By: Claude Sonnet 4.6 --- Directory.Build.props | 2 +- LayeredCraft.OptimizedEnums.slnx | 1 + README.md | 33 +++++- docs/advanced/diagnostics.md | 29 +++++ docs/changelog.md | 7 ++ docs/getting-started/installation.md | 24 ++++ docs/getting-started/quick-start.md | 27 +++++ docs/usage/json-serialization.md | 165 +++++++++++++++++++++++++++ mkdocs.yml | 1 + 9 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 docs/usage/json-serialization.md 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/LayeredCraft.OptimizedEnums.slnx b/LayeredCraft.OptimizedEnums.slnx index 10d629b..558cddf 100644 --- a/LayeredCraft.OptimizedEnums.slnx +++ b/LayeredCraft.OptimizedEnums.slnx @@ -32,6 +32,7 @@ + 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..1541ef8 100644 --- a/docs/advanced/diagnostics.md +++ b/docs/advanced/diagnostics.md @@ -83,6 +83,35 @@ 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:** `'{0}' does not inherit from OptimizedEnum and cannot have a JSON converter generated` + +**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 { ... } +``` + ## 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..c9dba13 --- /dev/null +++ b/docs/usage/json-serialization.md @@ -0,0 +1,165 @@ +# 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, there is no runtime reflection anywhere in the deserialization path: + +- No `Activator.CreateInstance` +- No `MakeGenericType` +- No `Delegate.CreateDelegate` +- `[JsonConverter]` is stamped on the partial class at compile time, so STJ's source-gen pipeline sees it + +The generated converter calls `TryFromName` / `TryFromValue` directly — methods that are themselves source-generated static dictionary lookups. + +## 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 index 4a232ae..863c382 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -112,6 +112,7 @@ nav: - 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 From 0849da5deba27f3775e9d51564f84bc3a676ca04 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 08:34:42 -0400 Subject: [PATCH 05/14] fix: address PR review feedback on STJ generator and docs - Switch CreateSyntaxProvider to ForAttributeWithMetadataName to prevent duplicate emissions when a partial class has the attribute on one declaration and an unrelated attribute on another - Expose AttributeMetadataName as internal const for use in generator registration - Remove manual attribute lookup in Transform; use context.Attributes[0] directly - Remove unused System.Collections.Immutable using - Fix OE2001/OE2002 diagnostic messages in docs to match exact MessageFormat strings - Clarify AOT section: converter logic is reflection-free, but STJ uses Activator.CreateInstance for converter instantiation unless JsonSerializerContext is used; add note explaining how to eliminate that in NativeAOT scenarios Co-Authored-By: Claude Sonnet 4.6 --- docs/advanced/diagnostics.md | 4 +-- docs/usage/json-serialization.md | 12 +++++---- .../OptimizedEnumJsonConverterGenerator.cs | 3 ++- .../Providers/JsonConverterSyntaxProvider.cs | 26 +++++++------------ 4 files changed, 20 insertions(+), 25 deletions(-) diff --git a/docs/advanced/diagnostics.md b/docs/advanced/diagnostics.md index 1541ef8..81ea9e3 100644 --- a/docs/advanced/diagnostics.md +++ b/docs/advanced/diagnostics.md @@ -89,7 +89,7 @@ The `LayeredCraft.OptimizedEnums.SystemTextJson` generator emits diagnostics wit ### OE2001 — Not an OptimizedEnum -**Message:** `'{0}' does not inherit from OptimizedEnum and cannot have a JSON converter generated` +**Message:** `The class '{0}' must inherit from OptimizedEnum to use [OptimizedEnumJsonConverter]` **Cause:** `[OptimizedEnumJsonConverter]` was applied to a class that does not inherit from `OptimizedEnum`. @@ -97,7 +97,7 @@ The `LayeredCraft.OptimizedEnums.SystemTextJson` generator emits diagnostics wit ### OE2002 — Must Be Partial -**Message:** `The class '{0}' must be declared as partial for OptimizedEnumJsonConverter source generation` +**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. diff --git a/docs/usage/json-serialization.md b/docs/usage/json-serialization.md index c9dba13..b2f314b 100644 --- a/docs/usage/json-serialization.md +++ b/docs/usage/json-serialization.md @@ -145,14 +145,16 @@ With `ByValue`, the JSON value is `"red"`/`"green"`/`"blue"`. With `ByName`, it ## AOT and Trimming Safety -Because the generator emits a concrete, non-generic converter for each type, there is no runtime reflection anywhere in the deserialization path: +Because the generator emits a concrete, non-generic converter for each type, the **converter logic itself** is entirely reflection-free: -- No `Activator.CreateInstance` -- No `MakeGenericType` +- No `MakeGenericType` — the converter type has `TEnum` baked in at generation time - No `Delegate.CreateDelegate` -- `[JsonConverter]` is stamped on the partial class at compile time, so STJ's source-gen pipeline sees it +- `TryFromName` / `TryFromValue` are themselves source-generated static dictionary lookups -The generated converter calls `TryFromName` / `TryFromValue` directly — methods that 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 diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterGenerator.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterGenerator.cs index 63e61aa..eddb7cf 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterGenerator.cs +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/OptimizedEnumJsonConverterGenerator.cs @@ -16,7 +16,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) ctx.AddSource(AttributeSource.HintName, AttributeSource.Source)); var converterInfos = context.SyntaxProvider - .CreateSyntaxProvider( + .ForAttributeWithMetadataName( + JsonConverterSyntaxProvider.AttributeMetadataName, JsonConverterSyntaxProvider.Predicate, JsonConverterSyntaxProvider.Transform) .WithTrackingName(TrackingNames.JsonConverterSyntaxProvider_Extract) diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs index 3687a8a..f2782f8 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs @@ -1,4 +1,3 @@ -using System.Collections.Immutable; using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Diagnostics; using LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Models; using Microsoft.CodeAnalysis; @@ -9,38 +8,28 @@ namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Providers; internal static class JsonConverterSyntaxProvider { - private const string AttributeMetadataName = + 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 { AttributeLists.Count: > 0 }; + node is ClassDeclarationSyntax; internal static JsonConverterInfo? Transform( - GeneratorSyntaxContext context, + GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - if (context.Node is not ClassDeclarationSyntax classDecl) + if (context.TargetNode is not ClassDeclarationSyntax classDecl) return null; - if (context.SemanticModel.GetDeclaredSymbol(classDecl, cancellationToken) - is not { } classSymbol) + if (context.TargetSymbol is not INamedTypeSymbol classSymbol) return null; - // Only process classes with [OptimizedEnumJsonConverter] - var attributeType = context.SemanticModel.Compilation - .GetTypeByMetadataName(AttributeMetadataName); - if (attributeType is null) - return null; - - var attr = classSymbol.GetAttributes() - .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeType)); - if (attr is null) - return null; + var attr = context.Attributes[0]; var diagnostics = new List(); var location = classDecl.CreateLocationInfo(); @@ -60,6 +49,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => ClassName: className, FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ValueTypeFullyQualified: string.Empty, + ValueTypeIsReferenceType: false, ContainingTypeNames: EquatableArray.Empty, ConverterType: OptimizedEnumJsonConverterType.ByName, Diagnostics: diagnostics.ToEquatableArray(), @@ -79,6 +69,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => ClassName: className, FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ValueTypeFullyQualified: string.Empty, + ValueTypeIsReferenceType: false, ContainingTypeNames: EquatableArray.Empty, ConverterType: OptimizedEnumJsonConverterType.ByName, Diagnostics: diagnostics.ToEquatableArray(), @@ -97,6 +88,7 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => ClassName: className, FullyQualifiedClassName: classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ValueTypeFullyQualified: valueTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + ValueTypeIsReferenceType: valueTypeSymbol.IsReferenceType, ContainingTypeNames: GetContainingTypeDeclarations(classSymbol), ConverterType: converterType, Diagnostics: diagnostics.ToEquatableArray(), From 76866864cbd74cc0ce5e690e4286c7de74f3d0a0 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 08:35:33 -0400 Subject: [PATCH 06/14] fix: apply sr-net-reviewer fixes for null safety, OE9001, and nested type test - Add ValueTypeIsReferenceType to JsonConverterInfo model; populate from ITypeSymbol.IsReferenceType in provider; thread through emitter model - Conditionalize null token guard in JsonConverter.scriban on value_type_is_reference_type to fix ArgumentNullException crash when TValue is a reference type (e.g. string) and JSON token is null - Add OE9001 to AnalyzerReleases.Unshipped.md to fix pre-existing RS2000 error - Add ByName_NestedType verify test and snapshots to cover containing-type preamble/suffix code path - Update ByValue_WithNamespace and ByValue_StringValueType snapshots to reflect null-guard changes Co-Authored-By: Claude Sonnet 4.6 --- .../AnalyzerReleases.Unshipped.md | 1 + .../Emitters/JsonConverterEmitter.cs | 1 + .../Models/JsonConverterInfo.cs | 4 +- .../Templates/JsonConverter.scriban | 13 ++- .../GeneratorVerifyTests.cs | 27 +++++ ....Outer.Status.SystemTextJson.g.verified.cs | 49 +++++++++ ...pe#MyApp.Domain.Outer.Status.g.verified.cs | 101 ++++++++++++++++++ ...edEnumJsonConverterAttribute.g.verified.cs | 46 ++++++++ ....Domain.Color.SystemTextJson.g.verified.cs | 9 +- ...n.OrderStatus.SystemTextJson.g.verified.cs | 3 +- 10 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.SystemTextJson.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#MyApp.Domain.Outer.Status.g.verified.cs create mode 100644 tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/Snapshots/GeneratorVerifyTests.ByName_NestedType#OptimizedEnumJsonConverterAttribute.g.verified.cs 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.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs index 3b0a564..8c0ad74 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs @@ -28,6 +28,7 @@ internal static void Generate(SourceProductionContext context, JsonConverterInfo info.ClassName, info.FullyQualifiedClassName, info.ValueTypeFullyQualified, + info.ValueTypeIsReferenceType, IsByName = info.ConverterType == OptimizedEnumJsonConverterType.ByName, Preamble = BuildPreamble(info), Suffix = BuildSuffix(info), diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/JsonConverterInfo.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/JsonConverterInfo.cs index 41c3138..6c314be 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/JsonConverterInfo.cs +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Models/JsonConverterInfo.cs @@ -7,6 +7,7 @@ internal sealed record JsonConverterInfo( string ClassName, string FullyQualifiedClassName, string ValueTypeFullyQualified, + bool ValueTypeIsReferenceType, EquatableArray ContainingTypeNames, OptimizedEnumJsonConverterType ConverterType, EquatableArray Diagnostics, @@ -21,11 +22,12 @@ other is not null && 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, - ContainingTypeNames, ConverterType, Diagnostics); + ValueTypeIsReferenceType, ContainingTypeNames, ConverterType, Diagnostics); } diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban index b225f68..7bc7f36 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban @@ -47,12 +47,23 @@ internal sealed class {{ converter_class_name }} 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 (!{{ fully_qualified_class_name }}.TryFromValue(value, out var result)) + 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!; } diff --git a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorVerifyTests.cs b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorVerifyTests.cs index 2c10d57..a76d7e9 100644 --- a/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorVerifyTests.cs +++ b/tests/LayeredCraft.OptimizedEnums.SystemTextJson.Tests/GeneratorVerifyTests.cs @@ -125,6 +125,33 @@ private Color(string value, string name) : base(value, name) { } }, 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( 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..2bed326 --- /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.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 index bc9ac75..918aa4c 100644 --- 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 @@ -1,4 +1,4 @@ -//HintName: MyApp.Domain.Color.SystemTextJson.g.cs +//HintName: MyApp.Domain.Color.SystemTextJson.g.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. @@ -25,9 +25,14 @@ internal sealed class ColorValueJsonConverter 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 (!global::MyApp.Domain.Color.TryFromValue(value, out var result)) + 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."); 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 index d505de3..eb41495 100644 --- 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 @@ -1,4 +1,4 @@ -//HintName: MyApp.Domain.OrderStatus.SystemTextJson.g.cs +//HintName: MyApp.Domain.OrderStatus.SystemTextJson.g.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. @@ -25,6 +25,7 @@ internal sealed class OrderStatusValueJsonConverter 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)) From 3d2cfcc828c64b6923f93cf960b59f80e5b03bb6 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 08:59:58 -0400 Subject: [PATCH 07/14] fix: address second round of PR review feedback - Promote OE9002 to static DiagnosticDescriptors field; remove inline descriptor from JsonConverterEmitter catch block - Add OE2003 diagnostic for unknown OptimizedEnumJsonConverterType values; validate rawValue against defined enum members in provider and return early with error diagnostic instead of silently casting - Add OE9002 and OE2003 to AnalyzerReleases.Unshipped.md - Change TemplateHelper resource name matching from InvariantCulture to Ordinal; manifest resource names are identifiers, not locale-sensitive text - Add HandleNull => true override to generated converter so STJ always invokes Read for null JSON tokens rather than silently producing null, ensuring non-null enum assumptions are enforced at deserialization - Document OE2003 in docs/advanced/diagnostics.md Co-Authored-By: Claude Sonnet 4.6 --- docs/advanced/diagnostics.md | 12 ++++++++++ .../AnalyzerReleases.Unshipped.md | 2 ++ .../Diagnostics/DiagnosticDescriptors.cs | 16 +++++++++++++ .../Emitters/JsonConverterEmitter.cs | 8 +------ .../Emitters/TemplateHelper.cs | 2 +- .../Providers/JsonConverterSyntaxProvider.cs | 23 +++++++++++++++++++ .../Templates/JsonConverter.scriban | 2 ++ 7 files changed, 57 insertions(+), 8 deletions(-) diff --git a/docs/advanced/diagnostics.md b/docs/advanced/diagnostics.md index 81ea9e3..6754db2 100644 --- a/docs/advanced/diagnostics.md +++ b/docs/advanced/diagnostics.md @@ -112,6 +112,18 @@ public sealed class OrderStatus : OptimizedEnum { ... } 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/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Unshipped.md b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Unshipped.md index a3b6a57..75a5894 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Unshipped.md +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/AnalyzerReleases.Unshipped.md @@ -7,3 +7,5 @@ ---------|---------------------------------|----------|----------------------- 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/Diagnostics/DiagnosticDescriptors.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticDescriptors.cs index c36ac0a..1a6d60a 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticDescriptors.cs +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -21,4 +21,20 @@ internal static class DiagnosticDescriptors 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/Emitters/JsonConverterEmitter.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs index 8c0ad74..2b38978 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs @@ -42,13 +42,7 @@ internal static void Generate(SourceProductionContext context, JsonConverterInfo catch (Exception ex) { context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor( - "OE9002", - "OptimizedEnums SystemTextJson generator internal error", - "An unexpected error occurred while generating the JSON converter for '{0}': {1}", - "OptimizedEnums.SystemTextJson", - DiagnosticSeverity.Error, - isEnabledByDefault: true), + DiagnosticDescriptors.GeneratorInternalError, info.Location?.ToLocation(), info.ClassName, ex.Message)); diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/TemplateHelper.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/TemplateHelper.cs index 90757e3..bee21cc 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/TemplateHelper.cs +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/TemplateHelper.cs @@ -26,7 +26,7 @@ private static Template LoadTemplate(string relativePath) var manifestTemplateName = assembly .GetManifestResourceNames() - .FirstOrDefault(x => x.EndsWith(templateName, StringComparison.InvariantCulture)); + .FirstOrDefault(x => x.EndsWith(templateName, StringComparison.Ordinal)); if (string.IsNullOrEmpty(manifestTemplateName)) { diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs index f2782f8..7ac7612 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Providers/JsonConverterSyntaxProvider.cs @@ -79,7 +79,30 @@ internal static bool Predicate(SyntaxNode node, CancellationToken _) => // 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]; diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban index 7bc7f36..1f67dde 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban @@ -17,6 +17,8 @@ partial class {{ class_name }} { } internal sealed class {{ converter_class_name }} : global::System.Text.Json.Serialization.JsonConverter<{{ fully_qualified_class_name }}> { + public override bool HandleNull => true; + {{ if is_by_name }} public override {{ fully_qualified_class_name }} Read( ref global::System.Text.Json.Utf8JsonReader reader, From cfa80f9608982793afa4541d13840325be9cd024 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 09:01:48 -0400 Subject: [PATCH 08/14] test: accept updated snapshots for HandleNull and null-guard changes Co-Authored-By: Claude Sonnet 4.6 --- ...Name_GlobalNamespace#Priority.SystemTextJson.g.verified.cs | 2 ++ ...ype#MyApp.Domain.Outer.Status.SystemTextJson.g.verified.cs | 4 +++- ...gValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs | 2 ++ ...pace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs | 2 ++ ...gValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs | 4 +++- ...pace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs | 4 +++- 6 files changed, 15 insertions(+), 3 deletions(-) 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 index a749c20..7ade8c3 100644 --- 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 @@ -17,6 +17,8 @@ partial class Priority { } internal sealed class PriorityNameJsonConverter : global::System.Text.Json.Serialization.JsonConverter { + public override bool HandleNull => true; + public override global::Priority Read( ref global::System.Text.Json.Utf8JsonReader reader, 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 index 2bed326..435641c 100644 --- 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 @@ -1,4 +1,4 @@ -//HintName: MyApp.Domain.Outer.Status.SystemTextJson.g.cs +//HintName: MyApp.Domain.Outer.Status.SystemTextJson.g.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. @@ -21,6 +21,8 @@ partial class Status { } internal sealed class StatusNameJsonConverter : global::System.Text.Json.Serialization.JsonConverter { + public override bool HandleNull => true; + public override global::MyApp.Domain.Outer.Status Read( ref global::System.Text.Json.Utf8JsonReader reader, 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 index ca4aa70..c4e2ccc 100644 --- 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 @@ -19,6 +19,8 @@ partial class Color { } internal sealed class ColorNameJsonConverter : global::System.Text.Json.Serialization.JsonConverter { + public override bool HandleNull => true; + public override global::MyApp.Domain.Color Read( ref global::System.Text.Json.Utf8JsonReader reader, 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 index 7fc04a4..f21f9c6 100644 --- 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 @@ -19,6 +19,8 @@ partial class OrderStatus { } internal sealed class OrderStatusNameJsonConverter : global::System.Text.Json.Serialization.JsonConverter { + public override bool HandleNull => true; + public override global::MyApp.Domain.OrderStatus Read( ref global::System.Text.Json.Utf8JsonReader reader, 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 index 918aa4c..0678a98 100644 --- 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 @@ -1,4 +1,4 @@ -//HintName: MyApp.Domain.Color.SystemTextJson.g.cs +//HintName: MyApp.Domain.Color.SystemTextJson.g.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. @@ -19,6 +19,8 @@ partial class Color { } internal sealed class ColorValueJsonConverter : global::System.Text.Json.Serialization.JsonConverter { + public override bool HandleNull => true; + public override global::MyApp.Domain.Color Read( ref global::System.Text.Json.Utf8JsonReader reader, 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 index eb41495..1adda24 100644 --- 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 @@ -1,4 +1,4 @@ -//HintName: MyApp.Domain.OrderStatus.SystemTextJson.g.cs +//HintName: MyApp.Domain.OrderStatus.SystemTextJson.g.cs //------------------------------------------------------------------------------ // // This code was generated by a tool. @@ -19,6 +19,8 @@ partial class OrderStatus { } internal sealed class OrderStatusValueJsonConverter : global::System.Text.Json.Serialization.JsonConverter { + public override bool HandleNull => true; + public override global::MyApp.Domain.OrderStatus Read( ref global::System.Text.Json.Utf8JsonReader reader, From 449a1ca31c3552aabd4afebfbfe023831b9b20c8 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 09:09:22 -0400 Subject: [PATCH 09/14] fix: remove HandleNull override from generated converter Setting HandleNull to true caused Read to throw on JsonTokenType.Null and Write to dereference value without a null guard, breaking nullable scenarios like OrderStatus? properties. STJ's default HandleNull behavior (false for reference types) correctly handles null tokens/values without involving the converter. Co-Authored-By: Claude Sonnet 4.6 --- .../Templates/JsonConverter.scriban | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban index 1f67dde..7bc7f36 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Templates/JsonConverter.scriban @@ -17,8 +17,6 @@ partial class {{ class_name }} { } internal sealed class {{ converter_class_name }} : global::System.Text.Json.Serialization.JsonConverter<{{ fully_qualified_class_name }}> { - public override bool HandleNull => true; - {{ if is_by_name }} public override {{ fully_qualified_class_name }} Read( ref global::System.Text.Json.Utf8JsonReader reader, From 4b72f37db06fbc05712e11769e2e1dc0c377f7c4 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 09:11:47 -0400 Subject: [PATCH 10/14] test: accept snapshots after removing HandleNull override Co-Authored-By: Claude Sonnet 4.6 --- ...ByName_GlobalNamespace#Priority.SystemTextJson.g.verified.cs | 2 -- ...dType#MyApp.Domain.Outer.Status.SystemTextJson.g.verified.cs | 2 -- ...ingValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs | 2 -- ...espace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs | 2 -- ...ingValueType#MyApp.Domain.Color.SystemTextJson.g.verified.cs | 2 -- ...espace#MyApp.Domain.OrderStatus.SystemTextJson.g.verified.cs | 2 -- 6 files changed, 12 deletions(-) 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 index 7ade8c3..a749c20 100644 --- 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 @@ -17,8 +17,6 @@ partial class Priority { } internal sealed class PriorityNameJsonConverter : global::System.Text.Json.Serialization.JsonConverter { - public override bool HandleNull => true; - public override global::Priority Read( ref global::System.Text.Json.Utf8JsonReader reader, 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 index 435641c..1c78bf0 100644 --- 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 @@ -21,8 +21,6 @@ partial class Status { } internal sealed class StatusNameJsonConverter : global::System.Text.Json.Serialization.JsonConverter { - public override bool HandleNull => true; - public override global::MyApp.Domain.Outer.Status Read( ref global::System.Text.Json.Utf8JsonReader reader, 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 index c4e2ccc..ca4aa70 100644 --- 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 @@ -19,8 +19,6 @@ partial class Color { } internal sealed class ColorNameJsonConverter : global::System.Text.Json.Serialization.JsonConverter { - public override bool HandleNull => true; - public override global::MyApp.Domain.Color Read( ref global::System.Text.Json.Utf8JsonReader reader, 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 index f21f9c6..7fc04a4 100644 --- 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 @@ -19,8 +19,6 @@ partial class OrderStatus { } internal sealed class OrderStatusNameJsonConverter : global::System.Text.Json.Serialization.JsonConverter { - public override bool HandleNull => true; - public override global::MyApp.Domain.OrderStatus Read( ref global::System.Text.Json.Utf8JsonReader reader, 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 index 0678a98..7f63ce4 100644 --- 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 @@ -19,8 +19,6 @@ partial class Color { } internal sealed class ColorValueJsonConverter : global::System.Text.Json.Serialization.JsonConverter { - public override bool HandleNull => true; - public override global::MyApp.Domain.Color Read( ref global::System.Text.Json.Utf8JsonReader reader, 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 index 1adda24..339c8d8 100644 --- 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 @@ -19,8 +19,6 @@ partial class OrderStatus { } internal sealed class OrderStatusValueJsonConverter : global::System.Text.Json.Serialization.JsonConverter { - public override bool HandleNull => true; - public override global::MyApp.Domain.OrderStatus Read( ref global::System.Text.Json.Utf8JsonReader reader, From 0d016356d288a158943ff96dab3caeb5a3b1077c Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 09:19:35 -0400 Subject: [PATCH 11/14] refactor: convert GeneratedCodeAttribute to static get-only property Co-Authored-By: Claude Sonnet 4.6 --- .../Emitters/JsonConverterEmitter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs index 2b38978..042e32e 100644 --- a/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs +++ b/src/LayeredCraft.OptimizedEnums.SystemTextJson.Generator/Emitters/JsonConverterEmitter.cs @@ -7,7 +7,7 @@ namespace LayeredCraft.OptimizedEnums.SystemTextJson.Generator.Emitters; internal static class JsonConverterEmitter { - private static readonly string GeneratedCodeAttribute = BuildGeneratedCodeAttribute(); + private static string GeneratedCodeAttribute { get; } = BuildGeneratedCodeAttribute(); private static string BuildGeneratedCodeAttribute() { From baaed864b3984e8a7e4d25af8d28914466baa2d1 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 09:51:30 -0400 Subject: [PATCH 12/14] chore: migrate docs from MkDocs to Zensical - Add zensical.toml with full config converted from mkdocs.yml - Add pyproject.toml with zensical dependency for uv package management - Update docs workflow to use uv + zensical build instead of pip + mkdocs - Remove mkdocs.yml and requirements.txt - Update .slnx to reference new config files Note: run `uv lock` locally and commit uv.lock to enable --locked installs in CI Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/docs.yml | 46 ++++++----- LayeredCraft.OptimizedEnums.slnx | 4 +- mkdocs.yml | 138 ------------------------------- pyproject.toml | 7 ++ requirements.txt | 2 - zensical.toml | 127 ++++++++++++++++++++++++++++ 6 files changed, 160 insertions(+), 164 deletions(-) delete mode 100644 mkdocs.yml create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100644 zensical.toml 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/LayeredCraft.OptimizedEnums.slnx b/LayeredCraft.OptimizedEnums.slnx index 558cddf..cd2e76e 100644 --- a/LayeredCraft.OptimizedEnums.slnx +++ b/LayeredCraft.OptimizedEnums.slnx @@ -3,8 +3,7 @@ - - + @@ -47,6 +46,7 @@ + diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 863c382..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,138 +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 - - 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 - -# 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/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 From cdcac5c99b322bfcff6a8edbc3f8f30b0f92425f Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 09:54:15 -0400 Subject: [PATCH 13/14] chore: add uv.lock for reproducible doc dependency resolution Co-Authored-By: Claude Sonnet 4.6 --- uv.lock | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 uv.lock 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" }, +] From 5e8217b2e2c1d0f2517181449602bc2abeac7996 Mon Sep 17 00:00:00 2001 From: Nick Cipollina Date: Tue, 31 Mar 2026 09:56:43 -0400 Subject: [PATCH 14/14] chore: add uv.lock to solution file Co-Authored-By: Claude Sonnet 4.6 --- LayeredCraft.OptimizedEnums.slnx | 1 + 1 file changed, 1 insertion(+) diff --git a/LayeredCraft.OptimizedEnums.slnx b/LayeredCraft.OptimizedEnums.slnx index cd2e76e..250d03d 100644 --- a/LayeredCraft.OptimizedEnums.slnx +++ b/LayeredCraft.OptimizedEnums.slnx @@ -47,6 +47,7 @@ +