From 0f997046b024a8a9fb31da1e56a12886eacf65bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Deniz=20=C4=B0rgin?= Date: Sat, 17 Oct 2026 18:21:59 +0300 Subject: [PATCH] refactor(generator): ref chains, pure policies and schema-name resolvers each get one owner, and an error-union refusal no longer cascades RefChain replaces the four ref-chain walks, with one walk and a result naming the last key. The pure Binding and Ingestion/Projection policies become static: comparer-only Binding types go from 10 to 0, the `_comparer` fields from 11 to 0, and their constructor threading and DI registrations go with them. SchemaNameResolver's four facet-root resolvers become their own static collaborators (SchemaNameResolver 410 -> 205 lines). SchemaPlanBinder is 154 lines. The four CA1822/S2325 exemptions in .editorconfig go, with their eight suppression-ledger rows. Latent duplicates that the static conversions made textually identical each get one owner. #436: a refused error union drops its membership claims before the graph is published, so a duplicate-tag refusal reports one error instead of two misleading absent-union errors. Generated output is byte-identical. Closes #436 --- .editorconfig | 25 -- .../Binding/CSharpNamePolicyTests.cs | 5 +- .../Binding/ModelReachCollectorTests.cs | 7 +- .../Binding/OperationPlanBinderTests.cs | 18 +- .../PendingOperationBindabilityProbeTests.cs | 4 +- .../Binding/QueryRequestFacetBinderTests.cs | 46 +++- .../Generator/Binding/RefChainTests.cs | 49 ++++ .../Generator/Binding/SpecBinderTests.cs | 37 ++- .../Binding/StabilizeDuplicatePolicyTests.cs | 12 +- .../Binding/UnionMemberHoistBinderTests.cs | 2 +- .../Binding/UnionMembershipValidatorTests.cs | 2 +- .../Binding/UpstreamClientQueryTypeTests.cs | 4 +- .../Emission/Support/EmitterPlanFixture.cs | 2 +- .../Projection/GraphKeyBuilderTests.cs | 15 +- .../Projection/LiteralClassifierTests.cs | 2 +- .../Projection/SchemaProjectorTests.cs | 3 +- .../Support/OperationProjectionTestHost.cs | 9 +- .../Support/SchemaProjectionTestHost.cs | 10 +- ...Suppression_In_The_Repository.verified.txt | 10 +- .../Support/BindingTestHost.cs | 16 +- .../Support/UnionHoistPlanData.cs | 6 +- .../OpenCode.Sdk.Tools.Tests/ToolAppTests.cs | 4 +- .../Generator/Binding/CSharpNamePolicy.cs | 8 +- .../Generator/Binding/CurationValidator.cs | 18 +- .../Binding/EffectStreamRootNameResolver.cs | 44 ++++ .../Generator/Binding/EnvelopeFacetBinder.cs | 31 +-- .../EnvelopePayloadRootNameResolver.cs | 103 ++++++++ .../Generator/Binding/ErrorUnionBinder.cs | 2 +- .../Binding/ExperimentalModelPolicy.cs | 7 +- .../Generator/Binding/MarkedUnionBinder.cs | 7 +- .../Generator/Binding/ModelReachCollector.cs | 10 +- .../Binding/Models/RefChainResult.cs | 5 + .../Binding/Models/SchemaBindingContext.cs | 8 +- .../Binding/OperationFacetContext.cs | 20 +- .../Generator/Binding/OperationPlanBinder.cs | 48 ++-- .../Binding/OperationWireShapeWall.cs | 2 +- .../PendingOperationBindabilityProbe.cs | 3 +- .../Binding/QueryEnumRootNameResolver.cs | 36 +++ .../Generator/Binding/QueryEnumShapePolicy.cs | 12 +- .../QueryParameterCurationValidator.cs | 27 ++- .../Binding/ReachableSchemaCollector.cs | 23 +- .../Generator/Binding/RefChain.cs | 25 ++ .../Binding/RequestBodyRootNameResolver.cs | 44 ++++ .../Generator/Binding/SchemaAliasApplier.cs | 12 +- .../Generator/Binding/SchemaAliasValidator.cs | 10 +- .../Generator/Binding/SchemaNameResolver.cs | 227 +----------------- .../Generator/Binding/SchemaPlanBinder.cs | 70 +++--- .../Generator/Binding/SpecBinder.cs | 103 ++++---- .../Binding/StabilizeDuplicatePolicy.cs | 22 +- .../Binding/StructuralUnionPlanBinder.cs | 69 +++--- .../Generator/Binding/TypePlanBinder.cs | 6 +- .../Binding/UnionDiscriminatorSelector.cs | 12 +- .../Binding/UnionMemberHoistBinder.cs | 22 +- .../Binding/UnionMembershipValidator.cs | 8 +- .../Binding/UnstructuredUnionPolicy.cs | 9 +- .../Binding/WireDocumentationPolicy.cs | 2 +- .../Projection/EffectStreamProjector.cs | 12 +- .../Projection/EnumSchemaProjector.cs | 4 +- .../Projection/ErrorStyleClassifier.cs | 4 +- .../Ingestion/Projection/GraphKeyBuilder.cs | 8 +- .../Projection/JsonStringSchemaProjector.cs | 8 +- .../Ingestion/Projection/LiteralClassifier.cs | 37 ++- .../Projection/MediaTypeProjector.cs | 8 + .../Projection/ObjectSchemaProjector.cs | 22 +- .../Projection/OperationProjector.cs | 15 +- .../Projection/ParameterProjector.cs | 8 +- .../Projection/PathTemplateValidator.cs | 12 +- .../Projection/PrefixItemsAdapter.cs | 22 +- .../Projection/RequestBodyProjector.cs | 14 +- .../Ingestion/Projection/ResponseProjector.cs | 16 +- .../Ingestion/Projection/SchemaProjector.cs | 27 +-- .../Ingestion/Projection/UnionNormalizer.cs | 25 +- .../Generator/Ingestion/SpecIngestion.cs | 7 +- tools/OpenCode.Sdk.Tools/ToolApp.cs | 12 - tools/jscpd/tests.baseline.json | 3 +- tools/jscpd/tools.baseline.json | 7 +- 76 files changed, 777 insertions(+), 837 deletions(-) create mode 100644 tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/RefChainTests.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/EffectStreamRootNameResolver.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopePayloadRootNameResolver.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/Models/RefChainResult.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumRootNameResolver.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/RefChain.cs create mode 100644 tools/OpenCode.Sdk.Tools/Generator/Binding/RequestBodyRootNameResolver.cs diff --git a/.editorconfig b/.editorconfig index e138b659..0a38ce15 100644 --- a/.editorconfig +++ b/.editorconfig @@ -745,31 +745,6 @@ dotnet_diagnostic.VSTHRD111.severity = none [tools/opencode-tool.cs] dotnet_diagnostic.MA0048.severity = none -# GraphKeyBuilder centralizes pointer and branch-identity policy shared by projection -# collaborators. Winner: one sealed policy owner instead of duplicated static mechanics; -# static-member rules remain enabled for every other file. -[tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs] -dotnet_diagnostic.CA1822.severity = none -dotnet_diagnostic.S2325.severity = none - -# ErrorStyleClassifier is a named policy collaborator constructed by object projection; -# making its only operation static would erase that ownership boundary. Winner: the sealed -# classifier contract; static-member rules remain enabled for every other file. -[tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs] -dotnet_diagnostic.CA1822.severity = none -dotnet_diagnostic.S2325.severity = none - -# EnumSchemaProjector and LiteralClassifier are named schema-policy collaborators composed by -# SchemaProjector; static operations would erase that ownership. Winner: the sealed collaborator -# contracts; static-member rules remain enabled for every other file. -[tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs] -dotnet_diagnostic.CA1822.severity = none -dotnet_diagnostic.S2325.severity = none - -[tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs] -dotnet_diagnostic.CA1822.severity = none -dotnet_diagnostic.S2325.severity = none - # The abstract adapter base carries shared, non-virtual mapping behavior for the # generated response adapters, and default interface members cannot carry shared # behavior on the net472/netstandard2.0 targets (CS8701). Winner: the diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs index 978e481d..a3371dbd 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/CSharpNamePolicyTests.cs @@ -32,9 +32,8 @@ public async Task ToPascalCase_Should_Keep_A_Brand_Spelling(string wireName, str } /// - /// A whole name that opens with a digit needs a letter prefix; a fragment appended to a - /// stem does not, and taking it there would spell an interior underscore the repository's - /// format pass rewrites out of the declaration without renaming the file it was written to. + /// A whole name that opens with a digit needs the Value prefix; a fragment appended to a + /// non-empty stem does not. /// [Test] [Arguments("0", "Value0", "0")] diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/ModelReachCollectorTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/ModelReachCollectorTests.cs index 99586354..e04e9ca5 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/ModelReachCollectorTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/ModelReachCollectorTests.cs @@ -157,11 +157,10 @@ private static async Task>> Co var document = await BindingTestHost.IngestAsync(ReachScenario()); var operationsById = document.Operations.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal); IReadOnlyList selected = [.. operationIds.Select(operationId => operationsById[operationId])]; - var reachableSchemas = new ReachableSchemaCollector(); - var reachable = reachableSchemas.Collect(document, selected, errors); - var typeNames = new SchemaNameResolver().Resolve(document, reachable, selected, Curation(Groups()), errors); + var reachable = ReachableSchemaCollector.Collect(document, selected, errors); + var typeNames = SchemaNameResolver.Resolve(document, reachable, selected, Curation(Groups()), errors); - var reach = ModelReachCollector.Collect(reachableSchemas, new ReachScope(document, selected, Clients(selected), typeNames), + var reach = ModelReachCollector.Collect(new ReachScope(document, selected, Clients(selected), typeNames), candidates, errors); errors.ThrowIfAny(); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs index d6acbabe..4ef0345d 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/OperationPlanBinderTests.cs @@ -106,7 +106,7 @@ private static IReadOnlyList NameCollisions(OwnershipCandidates ca { var inventory = GeneratedTypeNameInventory.Create(candidates, clients, [], schemaNames ?? new Dictionary(StringComparer.Ordinal)); var errors = new BindingErrorCollector(); - new OperationPlanBinder().CheckTypeNameCollisions(inventory, clients, errors); + OperationPlanBinder.CheckTypeNameCollisions(inventory, clients, errors); return errors.Snapshot(); } @@ -135,7 +135,7 @@ public async Task Bind_Should_Preserve_Type_Name_Claim_Order() HoistedInterfaces = [] }; var errors = new BindingErrorCollector(); - new OperationPlanBinder().CheckTypeNameCollisions(GeneratedTypeNameInventory.Create(candidates, clients, [], new Dictionary(StringComparer.Ordinal)), clients, errors); + OperationPlanBinder.CheckTypeNameCollisions(GeneratedTypeNameInventory.Create(candidates, clients, [], new Dictionary(StringComparer.Ordinal)), clients, errors); await Assert.That(errors.Snapshot()).IsEquivalentTo([ new BindingError(BindingErrorCategory.Naming, "WidgetsClient", "client type name 'WidgetsClient' collides with another generated type"), new BindingError(BindingErrorCategory.Naming, "widget.list", "multiple operations map to response type 'WidgetListResponse'"), @@ -447,17 +447,17 @@ public async Task Bind_Should_Refuse_Invalid_Envelope_Payload_Rows(string variat { if (variation == "invalid-binder") { - var reachable = new ReachableSchemaCollector().Collect(document, document.Operations, errors); - var schemas = new SchemaPlanBinder(new StructuralUnionPlanBinder(), new UnionMembershipValidator(), - new UnionDiscriminatorSelector(), new UnionMemberHoistBinder()).Bind(document, reachable, curation, - new Dictionary(StringComparer.Ordinal), errors); - _ = new OperationPlanBinder().Bind(document, document.Operations, curation, schemas, errors); + var reachable = ReachableSchemaCollector.Collect(document, document.Operations, errors); + var names = new Dictionary(StringComparer.Ordinal); + var context = SchemaBindingContext.Create(document, reachable, names, errors); + var schemas = new SchemaPlanBinder(context).Bind(curation); + _ = OperationPlanBinder.Bind(document, document.Operations, curation, schemas, errors); errors.ThrowIfAny(); } else if (variation == "invalid") { - new CurationValidator().Validate(document, document.Operations, - new ReachableSchemaCollector().Collect(document, document.Operations, errors), curation, errors); + CurationValidator.Validate(document, document.Operations, + ReachableSchemaCollector.Collect(document, document.Operations, errors), curation, errors); errors.ThrowIfAny(); } else diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs index 8af28429..d9021785 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/PendingOperationBindabilityProbeTests.cs @@ -69,7 +69,7 @@ public async Task Probe_Should_Mark_A_Walled_Operation_Refused_With_The_Walls_Ve await Assert.That(marks.Count).IsEqualTo(1); await Assert.That(marks[0].OperationId).IsEqualTo("widget.connect"); await Assert.That(marks[0].IsBindable).IsFalse(); - await Assert.That(marks[0].RefusalMessage).IsEqualTo("WebSocket operations are not supported in M1"); + await Assert.That(marks[0].RefusalMessage).IsEqualTo("WebSocket operations are not supported"); } [Test] @@ -83,7 +83,7 @@ public async Task Probe_Should_List_Every_Independent_Wall_In_Binder_Order_When_ await Assert.That(marks.Count).IsEqualTo(1); await Assert.That(marks[0].IsBindable).IsFalse(); await Assert.That(marks[0].RefusalMessage).IsEqualTo( - "WebSocket operations are not supported in M1; PUT operations must carry a request body"); + "WebSocket operations are not supported; PUT operations must carry a request body"); } [Test] diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/QueryRequestFacetBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/QueryRequestFacetBinderTests.cs index 2183a46f..d0536d80 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/QueryRequestFacetBinderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/QueryRequestFacetBinderTests.cs @@ -1,5 +1,6 @@ using OpenCode.Sdk.Tools.Generator.Binding; using OpenCode.Sdk.Tools.Generator.Binding.Models; +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; using OpenCode.Sdk.Tools.Tests.Support; using TUnit.Assertions.Enums; using static OpenCode.Sdk.Tools.Tests.Support.BindingScenarioData; @@ -38,10 +39,16 @@ public async Task Bind_Should_Refuse_The_Whole_Facet_When_One_Parameter_Is_Uncla } [Test] - public async Task Bind_Should_Refuse_An_Enum_Without_A_Generated_Model() + [Arguments(false)] + [Arguments(true)] + public async Task Bind_Should_Refuse_An_Enum_Without_A_Generated_Model(bool referencedNullable) { - var context = await CreateContextAsync(static operation => operation.Parameter("state", "query", - static schema => schema.Type("string").Enum("open", "closed"))); + var context = referencedNullable + ? await CreateContextAsync(static operation => operation.Parameter("state", "query", static schema => schema.Ref("State")), + static spec => spec.WithSchema("State", static schema => schema.AnyOf( + static branch => branch.Type("string").Enum("open", "closed"), static branch => branch.Type("null")))) + : await CreateContextAsync(static operation => operation.Parameter("state", "query", + static schema => schema.Type("string").Enum("open", "closed"))); var result = new QueryRequestFacetBinder(context).Bind(); await Assert.That(result).IsTypeOf.Refused>(); await Assert.That(((FacetResult.Refused)result).Errors).IsEquivalentTo([ @@ -49,6 +56,30 @@ await Assert.That(((FacetResult.Refused)result).Errors).IsEqu ], CollectionOrdering.Matching); } + [Test] + [Arguments(false)] + [Arguments(true)] + public async Task Bind_Should_Keep_A_Referenced_Nullable_Enum_Refused(bool fullPipeline) + { + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(static spec => spec + .WithSchema("State", static schema => schema.AnyOf( + static branch => branch.Type("string").Enum("open", "closed"), static branch => branch.Type("null"))) + .WithOperation("widget.list", configure: static operation => operation + .WithoutResponse(200).Response(204) + .Parameter("state", "query", static schema => schema.Ref("State"))))); + if (!fullPipeline) + { + await Assert.That(QueryEnumShapePolicy.ResolveModelKey(new RefNode { Target = "State" }, document.Schemas)).IsNull(); + return; + } + + var exception = Assert.Throws(() => new BindingTestHost().Bind(document, + Selection("widget.list"), Curation(Groups("widget", RootGroup())))); + await Assert.That(exception.Errors).IsEquivalentTo([ + new BindingError(BindingErrorCategory.Operation, "widget.list", "query parameter 'state' binds an enum that has no generated model"), + ], CollectionOrdering.Matching); + } + [Test] public async Task Bind_Should_Distinguish_An_Absent_Query_From_A_Refused_Query() { @@ -63,9 +94,14 @@ await Assert.That(((FacetResult.Refused)refused).Errors).IsEq ], CollectionOrdering.Matching); } - private static async Task CreateContextAsync(Action configure) + private static async Task CreateContextAsync(Action configure, + Action? configureSchemas = null) { - var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => spec.WithOperation("widget.list", configure: configure))); + var document = await BindingTestHost.IngestAsync(SpecScenario.Define(spec => + { + configureSchemas?.Invoke(spec); + spec.WithOperation("widget.list", configure: configure); + })); var errors = new BindingErrorCollector(); var names = new Dictionary(StringComparer.Ordinal); return new OperationFacetContext(document, document.Operations.Single(), Curation(Groups("widget", RootGroup())), names, errors, diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/RefChainTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/RefChainTests.cs new file mode 100644 index 00000000..a58eaaae --- /dev/null +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/RefChainTests.cs @@ -0,0 +1,49 @@ +using OpenCode.Sdk.Tools.Generator.Binding; +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; + +namespace OpenCode.Sdk.Tools.Tests.Generator.Binding; + +public sealed class RefChainTests +{ + [Test] + [Arguments("terminal", "B")] + [Arguments("missing", "A")] + [Arguments("self-cycle", "A")] + [Arguments("two-node-cycle", "B")] + [Arguments("nullable", "B")] + public async Task Resolve_Should_Return_The_Last_Node_And_Last_Successful_Key(string shape, string lastKey) + { + var lookups = 0; + // A broken cycle guard fails deterministically, rather than hanging the test host. + var comparer = EqualityComparer.Create((left, right) => ++lookups > 32 + ? throw new InvalidOperationException("Reference traversal revisited the cycle") + : StringComparer.Ordinal.Equals(left, right), StringComparer.Ordinal.GetHashCode); + var terminal = new EnumNode { Values = ["open", "closed"] }; + SchemaNode expected = shape switch + { + "missing" => new RefNode { Target = "absent" }, + "self-cycle" or "two-node-cycle" => new RefNode { Target = "A" }, + "nullable" => new NullableNode { Inner = terminal }, + _ => terminal, + }; + var graph = new Dictionary(comparer) + { + ["A"] = shape is "missing" or "self-cycle" ? expected : new RefNode { Target = "B" }, + ["B"] = expected, + }; + + var result = RefChain.Resolve(new RefNode { Target = "A" }, graph); + + await Assert.That(result.Node).IsSameReferenceAs(expected); + await Assert.That(result.LastKey).IsEqualTo(lastKey); + } + + [Test] + public async Task Resolve_Should_Not_Unwrap_A_Nullable_Node() + { + var node = new NullableNode { Inner = new RefNode { Target = "A" } }; + var result = RefChain.Resolve(node, new Dictionary(StringComparer.Ordinal)); + await Assert.That(result.Node).IsSameReferenceAs(node); + await Assert.That(result.LastKey).IsNull(); + } +} diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs index f652854c..8da9b908 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/SpecBinderTests.cs @@ -11,6 +11,31 @@ namespace OpenCode.Sdk.Tools.Tests.Generator.Binding; public sealed class SpecBinderTests { + [Test] + [Arguments(false, "JSON-encoded strings are not supported by the emitter")] + [Arguments(true, "tuple schemas are not supported by the emitter")] + public async Task Bind_Should_Refuse_Unsupported_Type_Shapes_Without_Milestone_Text(bool tuple, string problem) + { + var document = await IngestAsync(SpecScenario.Define(spec => spec + .WithObject("ItemInfo", schema => schema.Property("value", value => + { + if (tuple) + { + value.Type("array").PrefixItems(static item => item.Type("string"), static item => item.Type("integer")) + .MinItems(2).MaxItems(2); + } + else + { + value.Type("string").ContentSchema("application/json", static inner => inner.Type("string")); + } + }, required: true)) + .WithJsonOperation("health.get", "/health", static schema => schema.Ref("ItemInfo")))); + var exception = RefuseHealth(document); + await Assert.That(exception.Errors).IsEquivalentTo([ + new BindingError(BindingErrorCategory.Schema, "ItemInfo.value", problem), + ], CollectionOrdering.Matching); + } + [Test] public async Task Bind_Should_Preserve_The_Marker_Refusal_For_An_Unnamed_Error_Schema() { @@ -180,11 +205,9 @@ public async Task Bind_Should_Retain_Per_Schema_Error_Variants_And_Refusals() schemas["CBadMarker"] = (ObjectNode)schemas["CBadMarker"] with { LiteralMarkers = [] }; document = document with { Schemas = schemas }; var errors = new BindingErrorCollector(); - var reachable = new ReachableSchemaCollector().Collect(document, document.Operations, errors); + var reachable = ReachableSchemaCollector.Collect(document, document.Operations, errors); var names = new Dictionary(StringComparer.Ordinal) { ["AAdmitted"] = "AdmittedError", ["CBadMarker"] = "BadMarkerError" }; - var result = new ErrorUnionBinder(new SchemaBindingContext(document, reachable, - reachable.ResponseRootKeys.ToHashSet(StringComparer.Ordinal), reachable.StreamCauseKeys.ToHashSet(StringComparer.Ordinal), names, - new TypePlanBinder(schemas, names, errors), new SchemaInhabitationPolicy(schemas), errors)).Bind(); + var result = new ErrorUnionBinder(SchemaBindingContext.Create(document, reachable, names, errors)).Bind(); var expectedRefusals = new Dictionary(StringComparer.Ordinal) { ["BUnnamed"] = new(BindingErrorCategory.Naming, "BUnnamed", "error schema has no unique C# type name"), @@ -266,7 +289,7 @@ public async Task Bind_Should_Refuse_Invalid_Group_Configuration(string fault, o experimentalDiagnostic: fault == "shared-diagnostic" ? "OPENCODE001" : null); } var errors = new BindingErrorCollector(); - new CurationValidator().Validate(document, selected, new ReachableSchemaCollector().Collect(document, selected, errors), Curation(groups), errors); + CurationValidator.Validate(document, selected, ReachableSchemaCollector.Collect(document, selected, errors), Curation(groups), errors); var identity = expected[0]; await Assert.That(Assert.Throws(errors.ThrowIfAny).Errors .Where(error => error.Category == identity.Category && error.Subject == identity.Subject)) @@ -302,7 +325,7 @@ public async Task Bind_Should_Refuse_Invalid_Transport_Owned_Rows(string fault, var errors = new BindingErrorCollector(); var selected = document.Operations.Where(operation => operation.OperationId == "health.get" || fault == "selected").ToArray(); var declared = fault switch { "malformed" => "bad", "fingerprint" => new string('0', 64), _ => hash }; - new CurationValidator().Validate(document, selected, new ReachableSchemaCollector().Collect(document, selected, errors), + CurationValidator.Validate(document, selected, ReachableSchemaCollector.Collect(document, selected, errors), Curation(Groups("health", RootGroup()), transportOwned: [TransportOwned(subject, declared)]), errors); var expected = new BindingError(BindingErrorCategory.Curation, subject, problem); var exception = Assert.Throws(errors.ThrowIfAny); @@ -1847,8 +1870,6 @@ public async Task Bind_Should_Refuse_A_Tag_Owned_By_Two_Closure_Schemas(bool res Curation(Groups("health", RootGroup()), schemaNames: responseCollision ? [SchemaName("ItemInfo", "HealthResponse")] : []))); BindingError[] expected = [ new(BindingErrorCategory.Schema, "OpenCodeError", "multiple error schemas declare tag 'SharedError' (GoneError, LostError)"), - new(BindingErrorCategory.Schema, "GoneError", "union membership references absent union 'IOpenCodeError'"), - new(BindingErrorCategory.Schema, "LostError", "union membership references absent union 'IOpenCodeError'"), ]; if (responseCollision) { diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/StabilizeDuplicatePolicyTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/StabilizeDuplicatePolicyTests.cs index d36a258a..492a24bf 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/StabilizeDuplicatePolicyTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/StabilizeDuplicatePolicyTests.cs @@ -36,8 +36,8 @@ public async Task Resolve_Should_Refuse_Invalid_Duplicate_Curation(bool identica } })); var errors = new BindingErrorCollector(); - var reachable = new ReachableSchemaCollector().Collect(document, document.Operations, errors); - _ = new StabilizeDuplicatePolicy().Resolve(document, reachable, identical ? [SchemaName("Widget_1", "ReviewedWidget")] : [], errors); + var reachable = ReachableSchemaCollector.Collect(document, document.Operations, errors); + _ = StabilizeDuplicatePolicy.Resolve(document, reachable, identical ? [SchemaName("Widget_1", "ReviewedWidget")] : [], errors); await Assert.That(Assert.Throws(errors.ThrowIfAny).Errors.Single()).IsEqualTo(new BindingError(category, "Widget_1", problem)); } @@ -270,9 +270,9 @@ public async Task Resolve_Should_Fold_Equivalent_Pinned_Duplicates_And_Retain_Th var operations = document .Operations.Where(operation => selection.OperationIds.Contains(operation.OperationId, StringComparer.Ordinal)) .ToArray(); - var reachable = new ReachableSchemaCollector().Collect(document, operations, errors); + var reachable = ReachableSchemaCollector.Collect(document, operations, errors); - var collapse = new StabilizeDuplicatePolicy().Resolve(document, reachable, curation.SchemaNames, errors); + var collapse = StabilizeDuplicatePolicy.Resolve(document, reachable, curation.SchemaNames, errors); await Assert.That(errors.Count).IsEqualTo(0); await Assert.That(collapse.Aliases.Count).IsGreaterThan(0); @@ -320,8 +320,8 @@ private static IReadOnlyList ReachableStabilizeDuplicates(SpecDocument d private static async Task ResolveAsync(SpecScenario scenario, BindingErrorCollector errors) { var document = await BindingTestHost.IngestAsync(scenario); - var reachable = new ReachableSchemaCollector().Collect(document, document.Operations, errors); - return new StabilizeDuplicatePolicy().Resolve(document, reachable, [], errors); + var reachable = ReachableSchemaCollector.Collect(document, document.Operations, errors); + return StabilizeDuplicatePolicy.Resolve(document, reachable, [], errors); } private static IReadOnlyList Folds(StabilizeDuplicateCollapse collapse) => diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMemberHoistBinderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMemberHoistBinderTests.cs index 4ee2dd44..c4bb41da 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMemberHoistBinderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMemberHoistBinderTests.cs @@ -447,7 +447,7 @@ private static UnionHoistPlan Hoist(IReadOnlyList unions, IReadOnlyLi GenerationCuration? curation = null) { var errors = new BindingErrorCollector(); - var plan = new UnionMemberHoistBinder().Bind(unions, models, curation ?? Curation(Groups()), errors); + var plan = UnionMemberHoistBinder.Bind(unions, models, curation ?? Curation(Groups()), errors); CheckNames(plan, errors); errors.ThrowIfAny(); return plan; diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMembershipValidatorTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMembershipValidatorTests.cs index ef30e9cb..e6630167 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMembershipValidatorTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UnionMembershipValidatorTests.cs @@ -19,7 +19,7 @@ public async Task Validate_Should_Refuse_The_Same_Marker_Name_With_Different_Kin var unions = new[] { Union("IBooleanEvent", LiteralKind.Boolean), Union("IStringEvent", LiteralKind.String), }; var errors = new BindingErrorCollector(); - new UnionMembershipValidator().Validate([model], unions, errors); + UnionMembershipValidator.Validate([model], unions, errors); var exception = Assert.Throws(errors.ThrowIfAny); await Assert.That(exception.Errors.Single().Category).IsEqualTo(BindingErrorCategory.Schema); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UpstreamClientQueryTypeTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UpstreamClientQueryTypeTests.cs index 5c8de5aa..1d757ec9 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UpstreamClientQueryTypeTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Binding/UpstreamClientQueryTypeTests.cs @@ -136,8 +136,8 @@ .. plan.Clients /// A plain string or a number once references and a nullable wrapper are followed, as the binder classifies it. private static bool IsScalarText(SpecDocument document, SchemaNode schema) { - var resolved = OperationFacetContext.Resolve(document, schema); - var value = resolved is NullableNode { Format: null } nullable ? OperationFacetContext.Resolve(document, nullable.Inner) : resolved; + var resolved = RefChain.Resolve(schema, document.Schemas).Node; + var value = resolved is NullableNode { Format: null } nullable ? RefChain.Resolve(nullable.Inner, document.Schemas).Node : resolved; return value is PrimitiveNode { Kind: PrimitiveKind.String, Format: null } or PrimitiveNode { Kind: PrimitiveKind.Integer or PrimitiveKind.Number }; } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/Support/EmitterPlanFixture.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/Support/EmitterPlanFixture.cs index 1c43b61c..53196f1f 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/Support/EmitterPlanFixture.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Emission/Support/EmitterPlanFixture.cs @@ -1038,7 +1038,7 @@ public static EmitPlan CreateHoistPlan() }; var unions = new[] { CreateHoistEvent(), CreateHoistPhase(), }; var errors = new BindingErrorCollector(); - var hoisted = new UnionMemberHoistBinder().Bind(unions, models, HoistCuration(), errors); + var hoisted = UnionMemberHoistBinder.Bind(unions, models, HoistCuration(), errors); errors.ThrowIfAny(); return new EmitPlan { diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/GraphKeyBuilderTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/GraphKeyBuilderTests.cs index 0b892d2f..d11d74a7 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/GraphKeyBuilderTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/GraphKeyBuilderTests.cs @@ -8,9 +8,7 @@ public sealed class GraphKeyBuilderTests [Test] public async Task Append_Should_Escape_Slash_In_Segment() { - var keys = new GraphKeyBuilder(); - - var pointer = keys.Append(string.Empty, "a/b"); + var pointer = GraphKeyBuilder.Append(string.Empty, "a/b"); await Assert.That(pointer).IsEqualTo("/a~1b"); } @@ -18,9 +16,7 @@ public async Task Append_Should_Escape_Slash_In_Segment() [Test] public async Task Append_Should_Escape_Tilde_In_Segment() { - var keys = new GraphKeyBuilder(); - - var pointer = keys.Append(string.Empty, "a~b"); + var pointer = GraphKeyBuilder.Append(string.Empty, "a~b"); await Assert.That(pointer).IsEqualTo("/a~0b"); } @@ -28,7 +24,6 @@ public async Task Append_Should_Escape_Tilde_In_Segment() [Test] public async Task UnionBranch_Should_Use_And_Escape_Literal_Marker_Identity() { - var keys = new GraphKeyBuilder(); var marker = new LiteralMarker { PropertyName = "type/name", @@ -36,7 +31,7 @@ public async Task UnionBranch_Should_Use_And_Escape_Literal_Marker_Identity() Value = "created~new", }; - var pointer = keys.UnionBranch(string.Empty, "anyOf", 0, marker); + var pointer = GraphKeyBuilder.UnionBranch(string.Empty, "anyOf", 0, marker); await Assert.That(pointer).IsEqualTo("/anyOf/type~1name=created~0new"); } @@ -44,9 +39,7 @@ public async Task UnionBranch_Should_Use_And_Escape_Literal_Marker_Identity() [Test] public async Task UnionBranch_Should_Use_Ordinal_For_Unmarked_Branch() { - var keys = new GraphKeyBuilder(); - - var pointer = keys.UnionBranch(string.Empty, "oneOf", 3, marker: null); + var pointer = GraphKeyBuilder.UnionBranch(string.Empty, "oneOf", 3, marker: null); await Assert.That(pointer).IsEqualTo("/oneOf/3"); } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/LiteralClassifierTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/LiteralClassifierTests.cs index c9233e6f..d4e6f499 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/LiteralClassifierTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/LiteralClassifierTests.cs @@ -118,7 +118,7 @@ public async Task FindFirstPrefixMarker_Should_Return_The_First_Required_Prefix_ .Property("later", property => property.Type("string").Pattern("^x\\.[\\s\\S]*?$")))); var schema = await SchemaProjectionTestHost.LoadSchemaAsync(scenario, "Event"); - var marker = new LiteralClassifier().FindFirstPrefixMarker(schema); + var marker = LiteralClassifier.FindFirstPrefixMarker(schema); await Assert.That(marker).IsNotNull(); await Assert.That(marker.PropertyName).IsEqualTo("type"); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/SchemaProjectorTests.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/SchemaProjectorTests.cs index b2bbb6b6..c6a43c9b 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/SchemaProjectorTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Projection/SchemaProjectorTests.cs @@ -314,8 +314,7 @@ public async Task Project_Should_Refuse_When_Promoted_Graph_Key_Collides() var context = scenario.Build(); var errors = new IngestionErrorCollector(); var loaded = await new SpecReader(context.FileSystem).LoadAsync(context.SpecPath, errors, CancellationToken.None); - var keys = new GraphKeyBuilder(); - var projector = new SchemaProjector(keys); + var projector = new SchemaProjector(); var state = new ProjectionState(errors, loaded.Document); _ = projector.Project(loaded.Document.Components!.Schemas!["First"], "Shared", "/value", state); diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/OperationProjectionTestHost.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/OperationProjectionTestHost.cs index 59bb0f27..5f4de63c 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/OperationProjectionTestHost.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/OperationProjectionTestHost.cs @@ -7,7 +7,7 @@ namespace OpenCode.Sdk.Tools.Tests.Generator.Ingestion.Support; internal sealed class OperationProjectionTestHost { - private readonly GraphKeyBuilder _keys = new(); + private readonly SchemaProjector _schemas = new(); public async Task ProjectAsync(SpecScenario scenario, IReadOnlyDictionary? operationIdentities = null) @@ -34,17 +34,16 @@ private async Task ProjectCoreAsync(SpecScenario scen var errors = new IngestionErrorCollector(); try { - var loaded = await new SpecReader(context.FileSystem).LoadAsync(context.SpecPath, errors, CancellationToken.None); - var schemaProjector = new SchemaProjector(_keys); + var loaded = await SchemaProjectionTestHost.LoadAsync(context, errors); var state = new ProjectionState(errors, loaded.Document); - var operations = new OperationProjector(schemaProjector, _keys) + var operations = new OperationProjector(_schemas) .Project(loaded, state, operationIdentities ?? new Dictionary(StringComparer.Ordinal)); if (loaded.Document.Components?.Schemas is not null) { foreach (var (name, schema) in loaded.Document.Components.Schemas) { - _ = schemaProjector.Project(schema, _keys.Root(name), string.Empty, state); + _ = _schemas.Project(schema, GraphKeyBuilder.Root(name), string.Empty, state); } } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/SchemaProjectionTestHost.cs b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/SchemaProjectionTestHost.cs index fef0edae..e7875eff 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/SchemaProjectionTestHost.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Generator/Ingestion/Support/SchemaProjectionTestHost.cs @@ -8,7 +8,7 @@ namespace OpenCode.Sdk.Tools.Tests.Generator.Ingestion.Support; internal sealed class SchemaProjectionTestHost { - private readonly GraphKeyBuilder _keys = new(); + private readonly SchemaProjector _projector = new(); public async Task ProjectAsync(SpecScenario scenario) { @@ -48,15 +48,14 @@ private async Task ProjectCoreAsync(SpecScenario scenari var errors = new IngestionErrorCollector(); try { - var loaded = await new SpecReader(context.FileSystem).LoadAsync(context.SpecPath, errors, CancellationToken.None); - var projector = new SchemaProjector(_keys); + var loaded = await LoadAsync(context, errors); var state = new ProjectionState(errors, loaded.Document); if (loaded.Document.Components?.Schemas is not null) { foreach (var (name, schema) in loaded.Document.Components.Schemas) { - _ = projector.Project(schema, _keys.Root(name), string.Empty, state); + _ = _projector.Project(schema, GraphKeyBuilder.Root(name), string.Empty, state); } } @@ -68,4 +67,7 @@ private async Task ProjectCoreAsync(SpecScenario scenari return new SchemaProjectionResult(new Dictionary(StringComparer.Ordinal), exception); } } + + internal static Task LoadAsync(ScenarioContext context, IngestionErrorCollector errors) => + new SpecReader(context.FileSystem).LoadAsync(context.SpecPath, errors, CancellationToken.None); } diff --git a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/SuppressionLedgerTests.Ledger_Should_Count_Every_Suppression_In_The_Repository.verified.txt b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/SuppressionLedgerTests.Ledger_Should_Count_Every_Suppression_In_The_Repository.verified.txt index 48a78ce4..b0aab1fa 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Snapshots/SuppressionLedgerTests.Ledger_Should_Count_Every_Suppression_In_The_Repository.verified.txt +++ b/tests/OpenCode.Sdk.Tools.Tests/Snapshots/SuppressionLedgerTests.Ledger_Should_Count_Every_Suppression_In_The_Repository.verified.txt @@ -2,7 +2,7 @@ A change that adds, removes or alters a suppression updates this file in the same commit. build-switch: 12 -editorconfig: 158 +editorconfig: 150 globalconfig: 1 no-warn: 18 pragma: 26 @@ -170,14 +170,6 @@ editorconfig .editorconfig [tests/OpenCode.Sdk.Tests/Support/ServerDiagnosticSce editorconfig .editorconfig [tests/OpenCode.Sdk.Tests/Support/SessionEventProbe.cs] dotnet_diagnostic.VSTHRD003.severity = none editorconfig .editorconfig [tests/OpenCode.Sdk.Tests/Support/SessionEventProbeTests.cs] dotnet_diagnostic.VSTHRD003.severity = none editorconfig .editorconfig [tests/Shared/ProcessMark.cs] dotnet_diagnostic.MA0045.severity = none -editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs] dotnet_diagnostic.CA1822.severity = none -editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs] dotnet_diagnostic.S2325.severity = none -editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs] dotnet_diagnostic.CA1822.severity = none -editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs] dotnet_diagnostic.S2325.severity = none -editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs] dotnet_diagnostic.CA1822.severity = none -editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs] dotnet_diagnostic.S2325.severity = none -editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs] dotnet_diagnostic.CA1822.severity = none -editorconfig .editorconfig [tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs] dotnet_diagnostic.S2325.severity = none editorconfig .editorconfig [tools/opencode-tool.cs] dotnet_diagnostic.MA0048.severity = none globalconfig .globalconfig dotnet_diagnostic.VSTHRD010.severity = none no-warn src/OpenCode.Sdk.Extensions/OpenCode.Sdk.Extensions.csproj OPENCODE001 diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs index f0efebf3..df2bf7f7 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/BindingTestHost.cs @@ -9,21 +9,7 @@ namespace OpenCode.Sdk.Tools.Tests.Support; internal sealed class BindingTestHost { - private readonly SpecBinder _binder; - - public BindingTestHost() - { - _binder = new SpecBinder( - new ReachableSchemaCollector(), - new StabilizeDuplicatePolicy(), - new CurationValidator(), - new SchemaAliasValidator(), - new SchemaAliasApplier(), - new SchemaNameResolver(), - new SchemaPlanBinder(new StructuralUnionPlanBinder(), new UnionMembershipValidator(), new UnionDiscriminatorSelector(), - new UnionMemberHoistBinder()), - new OperationPlanBinder()); - } + private readonly SpecBinder _binder = new(); /// The production binder pipeline, exposed for callers that drive it themselves (e.g. the pending-operation probe). public ISpecBinder Binder => _binder; diff --git a/tests/OpenCode.Sdk.Tools.Tests/Support/UnionHoistPlanData.cs b/tests/OpenCode.Sdk.Tools.Tests/Support/UnionHoistPlanData.cs index 51780a37..a8660328 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/Support/UnionHoistPlanData.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/Support/UnionHoistPlanData.cs @@ -15,7 +15,7 @@ public static UnionHoistPlan HoistDurableCarrier(GenerationCuration curation) { var errors = new BindingErrorCollector(); var arms = new[] { (Name: "CreatedEvent", Tag: "created", Version: "1"), (Name: "DeletedEvent", Tag: "deleted", Version: "2") }; - var result = new UnionMemberHoistBinder().Bind( + var result = UnionMemberHoistBinder.Bind( [Union("IExampleEvent", [.. arms.Select(static arm => Arm(arm.Name))])], [ .. arms.Select(static arm => Variant(arm.Name, arm.Tag, Property("durable", Named(arm.Name + "Durable"), isRequired: true))), @@ -44,7 +44,7 @@ public static UnionPlan Union(string name, params UnionVariantPlan[] variants) } public static void CheckNames(UnionHoistPlan plan, BindingErrorCollector errors) => - new OperationPlanBinder().CheckTypeNameCollisions(GeneratedTypeNameInventory.Create(new OwnershipCandidates + OperationPlanBinder.CheckTypeNameCollisions(GeneratedTypeNameInventory.Create(new OwnershipCandidates { Models = plan.Models, Unions = plan.Unions, @@ -145,7 +145,7 @@ public static EmitPlan HoistedEmitPlan(IReadOnlyList unions, IReadOnl ArgumentNullException.ThrowIfNull(curation); var errors = new BindingErrorCollector(); - var hoisted = new UnionMemberHoistBinder().Bind(unions, models, curation, errors); + var hoisted = UnionMemberHoistBinder.Bind(unions, models, curation, errors); CheckNames(hoisted, errors); errors.ThrowIfAny(); return new EmitPlan diff --git a/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs b/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs index 7c7f23dc..242fcb4a 100644 --- a/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs +++ b/tests/OpenCode.Sdk.Tools.Tests/ToolAppTests.cs @@ -213,7 +213,7 @@ public async Task RunAsync_Should_Mark_Pending_Operations_With_Their_Bindability await Assert.That(marker).Contains("- plugin.list [bindable]"); await Assert.That(marker).Contains("- session.list [refused: the success response must carry a JSON schema]"); await Assert.That(marker).Contains( - "- widget.tail [refused: WebSocket operations are not supported in M1; PUT operations must carry a request body]"); + "- widget.tail [refused: WebSocket operations are not supported; PUT operations must carry a request body]"); } [Test] @@ -246,7 +246,7 @@ public async Task RunAsync_Should_List_Declined_Operations_With_Their_Reason_And await Assert.That(marker).Contains( "Declined:\n- widget.tail [declined: The operation is WebSocket-marked and a bodyless PUT, " + "so it does not bind; maintainer 2026-08-30.] " - + "[refused: WebSocket operations are not supported in M1; PUT operations must carry a request body]\n"); + + "[refused: WebSocket operations are not supported; PUT operations must carry a request body]\n"); await Assert.That(marker).DoesNotContain("Pending:\n- widget.tail"); } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs index 7ef2103c..f2df40f7 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/CSharpNamePolicy.cs @@ -30,12 +30,8 @@ public static string ToPascalCase(string wireName) /// /// Pascal-cases a fragment that is appended to a non-empty stem — a JSON pointer segment, and - /// above all a promoted union branch's ordinal. A leading digit needs no identifier guard in - /// that position, and applying 's guard would instead place an - /// interior underscore inside the finished name. That spelling does not survive: the - /// post-generation format pass rewrites it (IDE1006, and Sonar S101 reports it with no fix), - /// which renames the declared type without renaming the file the emitter wrote it to, so the - /// generated tree fails MA0048 rather than merely looking unusual. + /// above all a promoted union branch's ordinal. A leading digit in that position needs no + /// Value prefix, unlike the whole identifier that produces. /// public static string ToPascalCaseFragment(string wireName) => PascalWords(wireName); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs index 4d7c9d0c..69a9e714 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/CurationValidator.cs @@ -3,11 +3,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; -internal sealed class CurationValidator +internal static class CurationValidator { - private readonly StringComparer _comparer = StringComparer.Ordinal; - - public void Validate(SpecDocument document, IReadOnlyList selected, ReachableSchemaSet reachable, + public static void Validate(SpecDocument document, IReadOnlyList selected, ReachableSchemaSet reachable, GenerationCuration curation, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(document); @@ -16,10 +14,10 @@ public void Validate(SpecDocument document, IReadOnlyList selecte ArgumentNullException.ThrowIfNull(curation); ArgumentNullException.ThrowIfNull(errors); - var selectedIds = selected.Select(static operation => operation.OperationId).ToHashSet(_comparer); - var selectedGroups = selected.Select(GetGroup).ToHashSet(_comparer); - var documentIds = document.Operations.Select(static operation => operation.OperationId).ToHashSet(_comparer); - var documentGroups = document.Operations.Select(GetGroup).ToHashSet(_comparer); + var selectedIds = selected.Select(static operation => operation.OperationId).ToHashSet(StringComparer.Ordinal); + var selectedGroups = selected.Select(GetGroup).ToHashSet(StringComparer.Ordinal); + var documentIds = document.Operations.Select(static operation => operation.OperationId).ToHashSet(StringComparer.Ordinal); + var documentGroups = document.Operations.Select(GetGroup).ToHashSet(StringComparer.Ordinal); ValidateGroups(selected, selectedGroups, documentGroups, curation, errors); ValidateOperationNames(selected, documentIds, curation, errors); ValidateParameterNames(selected, curation, errors); @@ -169,10 +167,10 @@ private static void ValidateOperationNames(IReadOnlyList selected /// working object, so every operation of a handle family that carries it must name it the same /// way, across every group the family merges. /// - private void ValidateParameterNames(IReadOnlyList selected, GenerationCuration curation, + private static void ValidateParameterNames(IReadOnlyList selected, GenerationCuration curation, BindingErrorCollector errors) { - var operations = selected.ToDictionary(static operation => operation.OperationId, _comparer); + var operations = selected.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal); foreach (var row in curation.ParameterNames) { var subject = row.OperationId + ":" + row.Parameter; diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/EffectStreamRootNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/EffectStreamRootNameResolver.cs new file mode 100644 index 00000000..c5bb6595 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/EffectStreamRootNameResolver.cs @@ -0,0 +1,44 @@ +using System.Collections.ObjectModel; +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; + +namespace OpenCode.Sdk.Tools.Generator.Binding; + +internal static class EffectStreamRootNameResolver +{ + /// + /// The extension gives its cause graph a shared semantic role independent of the operation + /// carrying it. Distinct schemas claiming the same name hit the ordinary schema-name wall + /// rather than silently merging. + /// + public static IReadOnlyDictionary Resolve(SpecDocument document, IReadOnlyList selected) + { + var names = new Dictionary(StringComparer.Ordinal); + foreach (var extension in selected + .SelectMany(static operation => operation.Responses) + .Select(static response => response.EffectStream) + .OfType()) + { + if (extension.CauseSchema is not ArrayNode { Item: RefNode item } + || !document.Schemas.TryGetValue(item.Target, out var itemSchema) + || itemSchema is not UnionNode { Classification: UnionClassification.Marked } union) + { + continue; + } + + names[item.Target] = EffectStreamTypeNamePolicy.CauseInterface; + foreach (var target in union.Branches.OfType().Select(static branch => branch.Target)) + { + if (!document.Schemas.TryGetValue(target, out var branchSchema) + || branchSchema is not ObjectNode objectNode + || objectNode.LiteralMarkers.FirstOrDefault(static marker => marker.PropertyName is ErrorMarkerPolicy.WireName) is not { } marker) + { + continue; + } + + names[target] = EffectStreamTypeNamePolicy.CauseVariant(marker.Value); + } + } + + return new ReadOnlyDictionary(names); + } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopeFacetBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopeFacetBinder.cs index 7e0094d0..a7be5a0c 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopeFacetBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopeFacetBinder.cs @@ -361,22 +361,9 @@ schema is RefNode reference return ListOf(Named(itemName)); } - // 'data' may also be a RefNode naming an ARRAY component directly — vcs.branches' exact - // shape, Vcs.BranchList = {"type":"array","items":{"type":"string"}} — rather than - // wrapping the array inline at this position. Resolve the reference through the ref - // graph and, only when the resolved target is itself an array, apply the same item - // logic as the arm above: a named item keeps its own identity, and every other item - // shape (here, a primitive string) falls through to the type machinery, which walks the - // very same reference (RefNode -> ArrayNode -> item) on its own. This does not widen the - // guard below: the guard exists to stop a RefNode resolving to a NOMINAL - // (object/enum/union) target with a failed name lookup from resurrecting a - // mechanically-derived name, and that refusal is unchanged — only a ref resolving to an - // ARRAY gains this path, keyed on shape (ref -> array), never on operation id. Resolve - // only follows RefNode chains — it does not unwrap a NullableNode, and its own visited - // set stops a ref alias cycle by handing back the cycling RefNode — so a nullable-wrapped - // array or a cycling ref falls through this arm unmatched and reaches the same guard, - // refusing exactly as before; intentional, since this dialect has no NullableNode- - // unwrapping step of its own yet. + // A reference to an array component binds as an array: named items keep their identity, + // and other items bind through the type machinery. Ref walks do not unwrap nullable + // nodes; a nullable target or alias cycle reaches the nominal-name refusal below. if (data.Schema is RefNode arrayReference && _context.Resolve(arrayReference) is ArrayNode resolvedArray) { if (resolvedArray.Item is RefNode resolvedItem @@ -389,12 +376,8 @@ schema is RefNode reference ?? _context.RefuseNull("success payload does not bind to a supported type plan"); } - // A RefNode or ArrayNode that reached here already failed the name lookups above (a - // target the resolver left unnamed — a collapsed structural union, or a schema the - // dialect excludes from naming — or a ref that does not resolve to an array); the - // dialect keeps refusing those exactly as before instead of letting the type machinery - // resurrect a mechanically-derived name. Every other shape (a dictionary, for one) - // delegates to the type machinery. + // A failed nominal lookup must not resurrect a mechanically-derived name through the + // type machinery. Other shapes, such as dictionaries, delegate to that machinery. if (data.Schema is RefNode or ArrayNode) { return _context.RefuseNull( @@ -447,9 +430,7 @@ schema is RefNode reference && _context.Document.Schemas.TryGetValue(reference.Target, out var target) && target is ObjectNode wrapper && wrapper.Properties.FirstOrDefault(static property => property.Name is "data") is { } data - // A literal inline array resolves to itself; a RefNode naming an array component (the - // vcs.branches shape) resolves through the ref graph to the same ArrayNode — one check - // classifies both. + // Inline arrays and references to array components share the same resolved shape. && _context.Resolve(data.Schema) is ArrayNode ? EnvelopeKind.DataLocationList : EnvelopeKind.DataLocation; diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopePayloadRootNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopePayloadRootNameResolver.cs new file mode 100644 index 00000000..24a3a180 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/EnvelopePayloadRootNameResolver.cs @@ -0,0 +1,103 @@ +using System.Collections.ObjectModel; +using OpenCode.Sdk.Tools.Generator.Binding.Models; +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; + +namespace OpenCode.Sdk.Tools.Generator.Binding; + +internal static class EnvelopePayloadRootNameResolver +{ + /// Names success payloads promoted out of envelope spine by operation identity, not their projection keys. + /// + /// A success payload that ingestion promoted into the graph carries no schema identity of + /// its own: it was promoted out of envelope spine — the operation's own inline root, or a + /// wrapper the dialect never names — and neither spelling belongs on the public surface. + /// The rule covers bare roots, data members and location data (the list item or single object). + /// Component payloads keep their identity; schemaNames rows override. Cursor-list items stay + /// nominal (ADR-0017), so that position has no promoted key to claim. + /// + public static IReadOnlyDictionary Resolve(SpecDocument document, + IReadOnlyList selected, BindingErrorCollector errors) + { + var names = new Dictionary(StringComparer.Ordinal); + foreach (var operation in selected) + { + if (operation.Responses.FirstOrDefault(static response => response.StatusCode is 200) is not + { IsSse: false, Schema: RefNode reference, } success) + { + continue; + } + + // Cursor-list items stay nominal, so that shape has no promoted key to claim; the + // remaining shapes never reach a payload at all. + var payloadKey = success.EnvelopeShape switch + { + SpecEnvelopeShape.Bare => reference.Target, + SpecEnvelopeShape.Data => ResolveDataMemberKey(document, reference.Target), + SpecEnvelopeShape.DataLocation => ResolveDataLocationPayloadKey(document, reference.Target), + // A single-key wrapper's value is nominal or a primitive shape the type + // machinery names on its own, so that shape never promotes a payload key here. + SpecEnvelopeShape.SingleKey or SpecEnvelopeShape.CursorData + or SpecEnvelopeShape.DataHasMore or SpecEnvelopeShape.None => null, + _ => null, + }; + if (payloadKey is null || !payloadKey.Contains('#', StringComparison.Ordinal)) + { + continue; + } + + // A bare root embeds the operation id and cannot be shared, but a component data + // wrapper can be: two operations claiming one payload under different names refuse + // rather than letting iteration order pick a winner. + var payloadName = OperationNamePolicy.PayloadTypeName(operation); + if (names.TryGetValue(payloadKey, out var existing) && !StringComparer.Ordinal.Equals(existing, payloadName)) + { + errors.Add( + BindingErrorCategory.Naming, + payloadKey, + $"envelope payload is claimed as both '{existing}' and '{payloadName}' by selected operations"); + continue; + } + + names[payloadKey] = payloadName; + } + + return new ReadOnlyDictionary(names); + } + + /// + /// Resolves the wrapper member the payload binder reads, matching the single-required-data + /// shape EnvelopeFacetBinder.BindDataPayload accepts; any other wrapper refuses there. + /// + private static string? ResolveDataMemberKey(SpecDocument document, string wrapperKey) => + document.Schemas.TryGetValue(wrapperKey, out var wrapper) + && wrapper is ObjectNode { Properties: [{ Name: "data", IsRequired: true, Schema: RefNode payload, }] } + ? payload.Target + : null; + + /// Resolves location data to the array's item key or the single value's key. + /// + /// The shape check is , the same call + /// EnvelopeFacetBinder.ResolveDataLocationWrapper makes, so what this claims and what + /// the binder reads cannot drift apart. The caller filters component + /// keys (no '#'), which also keeps references to named arrays out. + /// + private static string? ResolveDataLocationPayloadKey(SpecDocument document, string wrapperKey) + { + if (!document.Schemas.TryGetValue(wrapperKey, out var target) || target is not ObjectNode wrapper) + { + return null; + } + + if (!EnvelopeWrapperShape.IsDataLocation(wrapper, out var data) || data is null) + { + return null; + } + + return data.Schema switch + { + ArrayNode { Item: RefNode item } => item.Target, + RefNode payload => payload.Target, + _ => null, + }; + } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ErrorUnionBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ErrorUnionBinder.cs index c41e81b9..e088f22a 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/ErrorUnionBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ErrorUnionBinder.cs @@ -71,7 +71,7 @@ public ErrorUnionBindingResult Bind() var errors = _context.Errors.Snapshot(); _publishedErrors.Append(errors); - return new ErrorUnionBindingResult(new UnionBindingResult(null, plan, edges.AsReadOnly()), + return new ErrorUnionBindingResult(new UnionBindingResult(null, plan, plan is null ? [] : edges.AsReadOnly()), new ReadOnlyDictionary(admitted), new ReadOnlyDictionary(refused), errors); } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ExperimentalModelPolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ExperimentalModelPolicy.cs index 8324f5dd..359db157 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/ExperimentalModelPolicy.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ExperimentalModelPolicy.cs @@ -9,10 +9,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; /// internal static class ExperimentalModelPolicy { - public static IReadOnlyDictionary Collect(ReachableSchemaCollector reachableSchemas, ReachScope scope, + public static IReadOnlyDictionary Collect(ReachScope scope, OwnershipCandidates candidates, GeneratedTypeNameInventory inventory, BindingErrorCollector errors) { - ArgumentNullException.ThrowIfNull(reachableSchemas); ArgumentNullException.ThrowIfNull(scope); ArgumentNullException.ThrowIfNull(candidates); ArgumentNullException.ThrowIfNull(inventory); @@ -20,14 +19,14 @@ public static IReadOnlyDictionary Collect(ReachableSchemaCollect var diagnostics = scope.Clients.SelectMany(static client => client.Operations) .ToDictionary(static operation => operation.OperationId, static operation => operation.ExperimentalDiagnostic, StringComparer.Ordinal); - var stable = ModelReachCollector.CollectSubset(reachableSchemas, + var stable = ModelReachCollector.CollectSubset( scope with { Selected = [.. scope.Selected.Where(static operation => !ExperimentalRoutePolicy.IsExperimental(operation.Path))] }, candidates, errors); var reachedBy = new Dictionary>(StringComparer.Ordinal); foreach (var subset in scope.Selected.Where(operation => diagnostics.GetValueOrDefault(operation.OperationId) is not null) .GroupBy(operation => diagnostics[operation.OperationId]!, StringComparer.Ordinal)) { - var reached = ModelReachCollector.CollectSubset(reachableSchemas, scope with { Selected = [.. subset] }, candidates, errors); + var reached = ModelReachCollector.CollectSubset(scope with { Selected = [.. subset] }, candidates, errors); foreach (var name in reached.Where(name => inventory.Names.Contains(name) && !stable.Contains(name))) { if (!reachedBy.TryGetValue(name, out var ids)) diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/MarkedUnionBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/MarkedUnionBinder.cs index 8f560432..a541a25e 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/MarkedUnionBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/MarkedUnionBinder.cs @@ -3,11 +3,10 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; -internal sealed class MarkedUnionBinder(SchemaBindingContext context, UnionDiscriminatorSelector discriminators) +internal sealed class MarkedUnionBinder(SchemaBindingContext context) { private static readonly IReadOnlyList NoPrefixMarkers = []; private readonly SchemaBindingContext _context = context; - private readonly UnionDiscriminatorSelector _discriminators = discriminators; public UnionBindingResult Bind(string key, string name, UnionNode union) { @@ -24,7 +23,7 @@ public UnionBindingResult Bind(string key, string name, UnionNode union) return null; } - var discriminator = _discriminators.Select(resolved); + var discriminator = UnionDiscriminatorSelector.Select(resolved); if (discriminator is null) { _context.Errors.Add(BindingErrorCategory.Schema, key, "marked union branches share no discriminating marker property"); @@ -288,7 +287,7 @@ private static UnionLiteralVariants BindLiteralVariants(UnionBindingContext cont IsNestedUnion: false, reference.Target, _context.Inhabitation.IsInhabited(leaf))); } - var arms = leaves.Count is 0 ? null : _discriminators.Select(leaves)?.PrefixBranches; + var arms = leaves.Count is 0 ? null : UnionDiscriminatorSelector.Select(leaves)?.PrefixBranches; return arms is { Count: > 0 } ? arms[0].TypeName : null; } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ModelReachCollector.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ModelReachCollector.cs index 858ed48e..86f2511a 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/ModelReachCollector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ModelReachCollector.cs @@ -11,10 +11,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; /// internal static class ModelReachCollector { - public static IReadOnlyDictionary> Collect(ReachableSchemaCollector reachableSchemas, ReachScope scope, + public static IReadOnlyDictionary> Collect(ReachScope scope, OwnershipCandidates candidates, BindingErrorCollector errors) { - ArgumentNullException.ThrowIfNull(reachableSchemas); ArgumentNullException.ThrowIfNull(scope); ArgumentNullException.ThrowIfNull(candidates); ArgumentNullException.ThrowIfNull(errors); @@ -27,7 +26,7 @@ public static IReadOnlyDictionary> Collect(Reachab .Where(operation => familyOf.ContainsKey(operation.OperationId)) .GroupBy(operation => familyOf[operation.OperationId], StringComparer.Ordinal)) { - var reachable = reachableSchemas.Collect(scope.Document, [.. family], errors); + var reachable = ReachableSchemaCollector.Collect(scope.Document, [.. family], errors); foreach (var typeName in reachable.GraphKeys.Where(scope.TypeNames.ContainsKey).Select(key => scope.TypeNames[key])) { _ = Reached(reach, typeName).Add(family.Key); @@ -38,15 +37,14 @@ public static IReadOnlyDictionary> Collect(Reachab return reach.ToDictionary(static entry => entry.Key, static entry => (IReadOnlyList)[.. entry.Value], StringComparer.Ordinal); } - public static IReadOnlySet CollectSubset(ReachableSchemaCollector reachableSchemas, ReachScope scope, + public static IReadOnlySet CollectSubset(ReachScope scope, OwnershipCandidates candidates, BindingErrorCollector errors) { - ArgumentNullException.ThrowIfNull(reachableSchemas); ArgumentNullException.ThrowIfNull(scope); ArgumentNullException.ThrowIfNull(candidates); ArgumentNullException.ThrowIfNull(errors); - var reachable = reachableSchemas.Collect(scope.Document, scope.Selected, errors); + var reachable = ReachableSchemaCollector.Collect(scope.Document, scope.Selected, errors); var reach = reachable.GraphKeys.Where(scope.TypeNames.ContainsKey).Select(key => scope.TypeNames[key]) .Distinct(StringComparer.Ordinal) .ToDictionary(static name => name, static _ => new SortedSet(StringComparer.Ordinal) { "subset" }, StringComparer.Ordinal); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/RefChainResult.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/RefChainResult.cs new file mode 100644 index 00000000..9585e361 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/RefChainResult.cs @@ -0,0 +1,5 @@ +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; + +namespace OpenCode.Sdk.Tools.Generator.Binding.Models; + +internal sealed record RefChainResult(SchemaNode Node, string? LastKey); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/SchemaBindingContext.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/SchemaBindingContext.cs index 58bd8bc2..7d201f82 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/SchemaBindingContext.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/Models/SchemaBindingContext.cs @@ -10,4 +10,10 @@ internal sealed record SchemaBindingContext( IReadOnlyDictionary TypeNames, TypePlanBinder Types, SchemaInhabitationPolicy Inhabitation, - BindingErrorCollector Errors); + BindingErrorCollector Errors) +{ + public static SchemaBindingContext Create(SpecDocument document, ReachableSchemaSet reachable, + IReadOnlyDictionary typeNames, BindingErrorCollector errors) => + new(document, reachable, reachable.ResponseRootKeys.ToHashSet(StringComparer.Ordinal), reachable.StreamCauseKeys.ToHashSet(StringComparer.Ordinal), + typeNames, new TypePlanBinder(document.Schemas, typeNames, errors), new SchemaInhabitationPolicy(document.Schemas), errors); +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationFacetContext.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationFacetContext.cs index 24acc71e..e3dab378 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationFacetContext.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationFacetContext.cs @@ -6,7 +6,7 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; /// /// Read-only per-operation state shared by the facet binders: the ingested document, the /// operation under bind, its curation, the resolved type-name map, and the error batch. -/// It also owns the one ref-graph and the refusal helpers, so every +/// It also provides reference resolution and refusal helpers, so every /// facet attributes its refusals to the same operation identically. /// internal sealed class OperationFacetContext( @@ -71,21 +71,5 @@ public FacetResult Publish(FacetResult result, IReadOnlyList Resolve(Document, schema); - - /// Follows a reference chain through the document's components to the schema it ends at; a cycle or a missing target stops it. - public static SchemaNode Resolve(SpecDocument document, SchemaNode schema) - { - ArgumentNullException.ThrowIfNull(document); - - var visited = new HashSet(StringComparer.Ordinal); - var current = schema; - while (current is RefNode reference && visited.Add(reference.Target) - && document.Schemas.TryGetValue(reference.Target, out var target)) - { - current = target; - } - - return current; - } + public SchemaNode Resolve(SchemaNode schema) => RefChain.Resolve(schema, Document.Schemas).Node; } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs index a029edfa..40879eab 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationPlanBinder.cs @@ -4,13 +4,11 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; -internal sealed class OperationPlanBinder +internal static class OperationPlanBinder { private const string RootClientName = "OpenCodeClient"; - private readonly StringComparer _comparer = StringComparer.Ordinal; - - public IReadOnlyList Bind(SpecDocument document, IReadOnlyList selected, + public static IReadOnlyList Bind(SpecDocument document, IReadOnlyList selected, GenerationCuration curation, SchemaBindingResult schemas, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(document); @@ -22,7 +20,7 @@ public IReadOnlyList Bind(SpecDocument document, IReadOnlyList(selected.Count); - foreach (var operation in selected.OrderBy(static operation => operation.OperationId, _comparer)) + foreach (var operation in selected.OrderBy(static operation => operation.OperationId, StringComparer.Ordinal)) { var operationPlan = new SingleOperationBinder(new OperationFacetContext(document, operation, curation, typeNames, errors, typeBinder), schemas.ErrorUnion).Bind(); if (operationPlan is not null) @@ -41,12 +39,12 @@ public IReadOnlyList Bind(SpecDocument document, IReadOnlyList AssembleClients(List bound) + private static List AssembleClients(List bound) { var rootOperations = bound .Where(static operation => operation.Row.Placement is GroupPlacement.Root) .Select(static operation => operation.Plan) - .OrderBy(static plan => plan.MethodName, _comparer) + .OrderBy(static plan => plan.MethodName, StringComparer.Ordinal) .ToArray(); var subClients = new List(); var clients = new List(); @@ -55,8 +53,8 @@ private List AssembleClients(List bound) // curation validator has already demanded identical handle declarations. foreach (var group in bound .Where(static operation => operation.Row.Placement is GroupPlacement.Client) - .GroupBy(static operation => operation.Row.ClientName!, _comparer) - .OrderBy(static group => group.Key, _comparer)) + .GroupBy(static operation => operation.Row.ClientName!, StringComparer.Ordinal) + .OrderBy(static group => group.Key, StringComparer.Ordinal)) { var members = group.ToArray(); var row = members[0].Row; @@ -74,7 +72,7 @@ private List AssembleClients(List bound) Name = RootClientName, Namespace = GeneratedNamespace.Public, Role = ClientRole.Root, - SubClients = [.. subClients.OrderBy(static reference => reference.PropertyName, _comparer)], + SubClients = [.. subClients.OrderBy(static reference => reference.PropertyName, StringComparer.Ordinal)], Operations = rootOperations, }); @@ -95,16 +93,16 @@ private static string ClientNamespace(EmissionMode emission) => /// (ADR-0021): the public family name belongs to the hand-written door the root client /// keeps pointing at, so nothing generated may claim it. /// - private IEnumerable CreateGroupClients(IReadOnlyList group, GroupCuration row, string collectionName) + private static IEnumerable CreateGroupClients(IReadOnlyList group, GroupCuration row, string collectionName) { var handleOperations = group.Where(static operation => operation.IsHandleOperation).ToArray(); var collectionOperations = group .Where(static operation => !operation.IsHandleOperation) .Select(static operation => operation.Plan) - .OrderBy(static plan => plan.MethodName, _comparer) + .OrderBy(static plan => plan.MethodName, StringComparer.Ordinal) .ToArray(); var handleParameter = handleOperations - .OrderBy(static operation => operation.Plan.MethodName, _comparer) + .OrderBy(static operation => operation.Plan.MethodName, StringComparer.Ordinal) .SelectMany(static operation => operation.Plan.Parameters) .FirstOrDefault(static parameter => parameter.IsHandleParameter); var collectionTypeName = ClientTypeName(collectionName, row.Emission); @@ -144,7 +142,7 @@ private IEnumerable CreateGroupClients(IReadOnlyList [ .. handleOperations .Select(static operation => operation.Plan) - .OrderBy(static plan => plan.MethodName, _comparer), + .OrderBy(static plan => plan.MethodName, StringComparer.Ordinal), ], }; } @@ -153,7 +151,7 @@ .. handleOperations private static string ClientTypeName(string familyName, EmissionMode emission) => emission is EmissionMode.InternalRaw ? CSharpNamePolicy.ToRawClientName(familyName) : familyName; - private void CheckMemberCollisions(List clients, BindingErrorCollector errors) + private static void CheckMemberCollisions(List clients, BindingErrorCollector errors) { foreach (var client in clients) { @@ -169,22 +167,22 @@ private void CheckMemberCollisions(List clients, BindingErrorCollect } foreach (var member in members - .GroupBy(static member => member, _comparer) + .GroupBy(static member => member, StringComparer.Ordinal) .Where(static member => member.Skip(1).Any()) - .OrderBy(static member => member.Key, _comparer)) + .OrderBy(static member => member.Key, StringComparer.Ordinal)) { errors.Add(BindingErrorCategory.Naming, client.Name, $"multiple members map to C# name '{member.Key}'"); } } } - public IReadOnlySet CheckTypeNameCollisions(GeneratedTypeNameInventory inventory, IReadOnlyList clients, BindingErrorCollector errors) + public static IReadOnlySet CheckTypeNameCollisions(GeneratedTypeNameInventory inventory, IReadOnlyList clients, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(inventory); ArgumentNullException.ThrowIfNull(clients); ArgumentNullException.ThrowIfNull(errors); - var refusedHoists = new HashSet(_comparer); - var hoistedOwners = new HashSet(_comparer); + var refusedHoists = new HashSet(StringComparer.Ordinal); + var hoistedOwners = new HashSet(StringComparer.Ordinal); foreach (var claim in inventory.Claims.Where(claim => claim.Role is GeneratedTypeNameInventory.ClaimRole.Hoisted && (!hoistedOwners.Add(claim.Name) || inventory.OtherClaims(claim).Any(static other => other.Role is not GeneratedTypeNameInventory.ClaimRole.Hoisted)))) { @@ -197,7 +195,7 @@ public IReadOnlySet CheckTypeNameCollisions(GeneratedTypeNameInventory i } // Spine names claim first, followed by models individually so a model/spine duplicate refuses. - var owners = new HashSet(_comparer); + var owners = new HashSet(StringComparer.Ordinal); owners.UnionWith(ReservedNamePolicy.SpineTypeNames); // Derived kit/adapter names reserve their declarations; their owning declaration reports duplicates. owners.UnionWith(inventory.Claims.Where(static claim => claim.Role is GeneratedTypeNameInventory.ClaimRole.Derived).Select(static claim => claim.Name)); @@ -227,9 +225,9 @@ GeneratedTypeNameInventory.ClaimRole.Model or GeneratedTypeNameInventory.ClaimRo errors.Add(BindingErrorCategory.Naming, claim.Subject, problem); } - var derivedOwners = new HashSet(ReservedNamePolicy.SpineTypeNames, _comparer); + var derivedOwners = new HashSet(ReservedNamePolicy.SpineTypeNames, StringComparer.Ordinal); var responseNames = inventory.Claims.Where(static claim => claim.Role is GeneratedTypeNameInventory.ClaimRole.Response) - .ToDictionary(static claim => claim.Subject, static claim => claim.Name, _comparer); + .ToDictionary(static claim => claim.Subject, static claim => claim.Name, StringComparer.Ordinal); foreach (var claim in inventory.Claims.Where(claim => claim.Role is GeneratedTypeNameInventory.ClaimRole.Derived && !derivedOwners.Add(claim.Name))) { // Equal response declarations already refuse with their response-name diagnostic. @@ -242,8 +240,8 @@ GeneratedTypeNameInventory.ClaimRole.Model or GeneratedTypeNameInventory.ClaimRo errors.Add(BindingErrorCategory.Naming, claim.Subject, $"type name '{claim.Name}' collides with another generated type"); } - var routeMembers = new HashSet(_comparer); - var operations = clients.SelectMany(static client => client.Operations).OrderBy(static operation => operation.OperationId, _comparer).ToArray(); + var routeMembers = new HashSet(StringComparer.Ordinal); + var operations = clients.SelectMany(static client => client.Operations).OrderBy(static operation => operation.OperationId, StringComparer.Ordinal).ToArray(); foreach (var operation in operations.Where(operation => !routeMembers.Add($"{operation.RouteContainerName}.{operation.RouteMemberName}"))) { errors.Add(BindingErrorCategory.Naming, operation.OperationId, diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationWireShapeWall.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationWireShapeWall.cs index 6f953279..7dc8b06b 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationWireShapeWall.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/OperationWireShapeWall.cs @@ -30,7 +30,7 @@ public bool Check() if (operation.IsWebSocket) { - _context.Refuse("WebSocket operations are not supported in M1"); + _context.Refuse("WebSocket operations are not supported"); } // A GET body has no defined semantics on any origin server and stays refused. DELETE is diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/PendingOperationBindabilityProbe.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/PendingOperationBindabilityProbe.cs index d1bdaed7..50a6c99d 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/PendingOperationBindabilityProbe.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/PendingOperationBindabilityProbe.cs @@ -7,8 +7,7 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; /// /// Marks each pending operation with the bindability the real selection-path binder finds, so /// wall-free drift among pending operations surfaces as a committed diff instead of accumulating -/// silently — the interim telltale bridging until the operation inventory/assurance-ledger lane -/// standardizes pending-operation tracking. Declined operations are probed the same way and for +/// silently. Declined operations are probed the same way and for /// the same reason: a decline asserts a standing wall, and only this probe can confirm it still /// stands. /// diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumRootNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumRootNameResolver.cs new file mode 100644 index 00000000..2ff42efe --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumRootNameResolver.cs @@ -0,0 +1,36 @@ +using System.Collections.ObjectModel; +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; + +namespace OpenCode.Sdk.Tools.Generator.Binding; + +internal static class QueryEnumRootNameResolver +{ + /// Names promoted inline query enums by {RequestTypeName}{PropertyName}; component enums keep their identity. + /// + /// An inline query enum that ingestion promoted into the graph carries no schema identity + /// of its own — its key is the operation's parameter pointer — so it is named from the + /// request record it is a member of, mechanically {RequestTypeName}{PropertyName}. + /// Each promoted key embeds one operation id and parameter pointer, so two selected + /// operations cannot claim the same key. A reasoned schemaNames row still overrides. + /// + public static IReadOnlyDictionary Resolve(SpecDocument document, IReadOnlyList selected) + { + var names = new Dictionary(StringComparer.Ordinal); + foreach (var operation in selected) + { + foreach (var parameter in operation.Parameters.Where(static parameter => + parameter.Location is SpecParameterLocation.Query)) + { + var key = QueryEnumShapePolicy.ResolveModelKey(parameter.Schema, document.Schemas); + if (key is null || !key.Contains('#', StringComparison.Ordinal)) + { + continue; + } + + names[key] = $"{OperationNamePolicy.RequestTypeName(operation)}{CSharpNamePolicy.ToPascalCase(parameter.Name)}"; + } + } + + return new ReadOnlyDictionary(names); + } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumShapePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumShapePolicy.cs index f515c7df..1d83beae 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumShapePolicy.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryEnumShapePolicy.cs @@ -54,15 +54,7 @@ internal static class QueryEnumShapePolicy // Ingestion promotes every inline enum into the graph, so the enum a parameter binds // always arrives behind a reference; a key is what the model and its name hang on. - string? key = null; - var visited = new HashSet(StringComparer.Ordinal); - while (candidate is RefNode reference && visited.Add(reference.Target) - && graph.TryGetValue(reference.Target, out var target)) - { - key = reference.Target; - candidate = target; - } - - return candidate is EnumNode { Format: null } node && ResolveSpineKind(node) is null ? key : null; + var resolved = candidate is null ? null : RefChain.Resolve(candidate, graph); + return resolved?.Node is EnumNode { Format: null } node && ResolveSpineKind(node) is null ? resolved.LastKey : null; } } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryParameterCurationValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryParameterCurationValidator.cs index 329740bf..8e38a6bb 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryParameterCurationValidator.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/QueryParameterCurationValidator.cs @@ -20,15 +20,16 @@ public static void Validate(SpecDocument document, IReadOnlyList ArgumentNullException.ThrowIfNull(curation); ArgumentNullException.ThrowIfNull(errors); - var documentIds = document.Operations.Select(static operation => operation.OperationId).ToHashSet(StringComparer.Ordinal); - var operations = selected.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal); + var context = new QueryParameterValidationContext(document, + document.Operations.Select(static operation => operation.OperationId).ToHashSet(StringComparer.Ordinal), + selected.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal), errors); var typed = new HashSet(StringComparer.Ordinal); foreach (var row in curation.QueryParameterTypes) { var subject = row.OperationId + ":" + row.Parameter; _ = typed.Add(subject); - ValidateSubject(document, documentIds, operations, row.OperationId, row.Parameter, subject, errors); + ValidateSubject(context, row.OperationId, row.Parameter, subject); ValidateTypedRow(row, subject, errors); } @@ -41,7 +42,7 @@ public static void Validate(SpecDocument document, IReadOnlyList "the query parameter is classified twice: a queryParameterTypes row types it, so its stringQueryParameters row must go"); } - ValidateSubject(document, documentIds, operations, row.OperationId, row.Parameter, subject, errors); + ValidateSubject(context, row.OperationId, row.Parameter, subject); } } @@ -60,13 +61,12 @@ private static void ValidateTypedRow(QueryParameterTypeCuration row, string subj } } - private static void ValidateSubject(SpecDocument document, HashSet documentIds, - Dictionary operations, string operationId, string parameterName, string subject, - BindingErrorCollector errors) + private static void ValidateSubject(QueryParameterValidationContext context, string operationId, string parameterName, string subject) { - if (!operations.TryGetValue(operationId, out var operation)) + var errors = context.Errors; + if (!context.Operations.TryGetValue(operationId, out var operation)) { - errors.Add(BindingErrorCategory.Curation, subject, documentIds.Contains(operationId) + errors.Add(BindingErrorCategory.Curation, subject, context.DocumentIds.Contains(operationId) ? "curated operation is not selected by the current profile" : "the operation no longer exists in the document; remove this row"); return; @@ -80,7 +80,7 @@ private static void ValidateSubject(SpecDocument document, HashSet docum return; } - switch (ResolveValue(document, parameter.Schema)) + switch (ResolveValue(context.Document, parameter.Schema)) { case PrimitiveNode { Kind: PrimitiveKind.String, Format: null }: return; @@ -98,7 +98,10 @@ private static void ValidateSubject(SpecDocument document, HashSet docum /// Follows references and one nullable wrapper to the value a query parameter carries, as the binder does. private static SchemaNode ResolveValue(SpecDocument document, SchemaNode schema) { - var resolved = OperationFacetContext.Resolve(document, schema); - return resolved is NullableNode { Format: null } nullable ? OperationFacetContext.Resolve(document, nullable.Inner) : resolved; + var resolved = RefChain.Resolve(schema, document.Schemas).Node; + return resolved is NullableNode { Format: null } nullable ? RefChain.Resolve(nullable.Inner, document.Schemas).Node : resolved; } + + private sealed record QueryParameterValidationContext(SpecDocument Document, IReadOnlySet DocumentIds, + IReadOnlyDictionary Operations, BindingErrorCollector Errors); } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs index 138c3e06..6c09fe45 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/ReachableSchemaCollector.cs @@ -3,17 +3,15 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; -internal sealed class ReachableSchemaCollector +internal static class ReachableSchemaCollector { - private readonly StringComparer _comparer = StringComparer.Ordinal; - - public ReachableSchemaSet Collect(SpecDocument document, IReadOnlyList operations, BindingErrorCollector errors) + public static ReachableSchemaSet Collect(SpecDocument document, IReadOnlyList operations, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(document); ArgumentNullException.ThrowIfNull(operations); ArgumentNullException.ThrowIfNull(errors); - var traversal = new ReachabilityTraversal(document.Schemas, errors, _comparer); + var traversal = new ReachabilityTraversal(document.Schemas, errors); foreach (var operation in operations) { foreach (var parameter in operation.Parameters) @@ -52,14 +50,13 @@ public ReachableSchemaSet Collect(SpecDocument document, IReadOnlyList graph, BindingErrorCollector errors, StringComparer comparer) + private sealed class ReachabilityTraversal(IReadOnlyDictionary graph, BindingErrorCollector errors) { - private readonly StringComparer _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer)); private readonly BindingErrorCollector _errors = errors ?? throw new ArgumentNullException(nameof(errors)); private readonly IReadOnlyDictionary _graph = graph ?? throw new ArgumentNullException(nameof(graph)); - private readonly HashSet _keys = new(comparer); - private readonly HashSet _responseRoots = new(comparer); - private readonly HashSet _streamCauseKeys = new(comparer); + private readonly HashSet _keys = new(StringComparer.Ordinal); + private readonly HashSet _responseRoots = new(StringComparer.Ordinal); + private readonly HashSet _streamCauseKeys = new(StringComparer.Ordinal); public void VisitResponse(string operationId, SpecResponse response) { @@ -192,9 +189,9 @@ private void Visit(SchemaNode schema, bool isStreamCause) public ReachableSchemaSet Snapshot() => new() { - GraphKeys = [.. _keys.Order(_comparer)], - ResponseRootKeys = [.. _responseRoots.Order(_comparer)], - StreamCauseKeys = [.. _streamCauseKeys.Order(_comparer)], + GraphKeys = [.. _keys.Order(StringComparer.Ordinal)], + ResponseRootKeys = [.. _responseRoots.Order(StringComparer.Ordinal)], + StreamCauseKeys = [.. _streamCauseKeys.Order(StringComparer.Ordinal)], }; private void VisitReference(string target, bool isStreamCause) diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/RefChain.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/RefChain.cs new file mode 100644 index 00000000..8200bae1 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/RefChain.cs @@ -0,0 +1,25 @@ +using OpenCode.Sdk.Tools.Generator.Binding.Models; +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; + +namespace OpenCode.Sdk.Tools.Generator.Binding; + +/// Follows references only, retaining the last successfully followed key even when a chain cannot finish. +internal static class RefChain +{ + public static RefChainResult Resolve(SchemaNode node, IReadOnlyDictionary graph) + { + ArgumentNullException.ThrowIfNull(node); + ArgumentNullException.ThrowIfNull(graph); + + var visited = new HashSet(StringComparer.Ordinal); + string? lastKey = null; + while (node is RefNode reference && visited.Add(reference.Target) + && graph.TryGetValue(reference.Target, out var target)) + { + lastKey = reference.Target; + node = target; + } + + return new RefChainResult(node, lastKey); + } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/RequestBodyRootNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/RequestBodyRootNameResolver.cs new file mode 100644 index 00000000..cbf7ff01 --- /dev/null +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/RequestBodyRootNameResolver.cs @@ -0,0 +1,44 @@ +using System.Collections.ObjectModel; +using OpenCode.Sdk.Tools.Generator.Binding.Models; +using OpenCode.Sdk.Tools.Generator.Ingestion.Models; + +namespace OpenCode.Sdk.Tools.Generator.Binding; + +internal static class RequestBodyRootNameResolver +{ + /// + /// Every selected operation's nominal body root — inline or component-referenced — is + /// named from its operation identity, the mechanical {Subject}{Verb}Request rule + /// (public names never surface raw operation ids or dotted component spellings). + /// + /// + /// Pending operations cannot rename a component the selected closure shares. Two selected + /// operations claiming one root under different names refuse. Only the root reference is + /// claimed; nested component dependencies stay shared. + /// + public static IReadOnlyDictionary Resolve(IReadOnlyList selected, BindingErrorCollector errors) + { + var names = new Dictionary(StringComparer.Ordinal); + foreach (var operation in selected) + { + if (operation.RequestBody?.Schema is not RefNode reference) + { + continue; + } + + var requestName = OperationNamePolicy.RequestTypeName(operation); + if (names.TryGetValue(reference.Target, out var existing) && !StringComparer.Ordinal.Equals(existing, requestName)) + { + errors.Add( + BindingErrorCategory.Naming, + reference.Target, + $"request body root is claimed as both '{existing}' and '{requestName}' by selected operations"); + continue; + } + + names[reference.Target] = requestName; + } + + return new ReadOnlyDictionary(names); + } +} diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasApplier.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasApplier.cs index 3970b636..f7eb984a 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasApplier.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasApplier.cs @@ -9,9 +9,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; /// duplicate. Container kinds without a rewrite rule pass through unchanged; a reference they /// might carry then dangles and fails reachability loudly. /// -internal sealed class SchemaAliasApplier +internal static class SchemaAliasApplier { - public SpecDocument Apply(SpecDocument document, StabilizeDuplicateCollapse collapse, IReadOnlyList aliases) + public static SpecDocument Apply(SpecDocument document, StabilizeDuplicateCollapse collapse, IReadOnlyList aliases) { ArgumentNullException.ThrowIfNull(document); @@ -58,7 +58,7 @@ public static IReadOnlyDictionary Compose(StabilizeDuplicateColl return map; } - private SpecOperation RewriteOperation(SpecOperation operation, Fold map) => + private static SpecOperation RewriteOperation(SpecOperation operation, Fold map) => operation with { Parameters = @@ -84,7 +84,7 @@ .. operation.Responses.Select(response => response with ], }; - private SpecEffectStreamContract? RewriteEffectStream(SpecEffectStreamContract? contract, Fold map) + private static SpecEffectStreamContract? RewriteEffectStream(SpecEffectStreamContract? contract, Fold map) { if (contract is null) { @@ -100,7 +100,7 @@ .. operation.Responses.Select(response => response with }; } - private SchemaNode Rewrite(SchemaNode node, Fold map) => node switch + private static SchemaNode Rewrite(SchemaNode node, Fold map) => node switch { RefNode reference when map.Targets.TryGetValue(reference.Target, out var target) => reference with { @@ -141,7 +141,7 @@ .. target.Properties.Select(property => property with }; /// Collapsed branches can duplicate a reference; the duplicates fold away mechanically. - private UnionNode RewriteUnion(UnionNode union, Fold map) + private static UnionNode RewriteUnion(UnionNode union, Fold map) { var branches = new List(union.Branches.Count); var seen = new HashSet(StringComparer.Ordinal); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasValidator.cs index 461100fd..4e7897ce 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasValidator.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaAliasValidator.cs @@ -12,11 +12,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; /// needs the collapse, which is why it is its own validator rather than a sixth check inside /// . /// -internal sealed class SchemaAliasValidator +internal static class SchemaAliasValidator { - private readonly StringComparer _comparer = StringComparer.Ordinal; - - public void Validate(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation, + public static void Validate(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation, StabilizeDuplicateCollapse collapse, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(document); @@ -25,8 +23,8 @@ public void Validate(SpecDocument document, ReachableSchemaSet reachable, Genera ArgumentNullException.ThrowIfNull(collapse); ArgumentNullException.ThrowIfNull(errors); - var reachableKeys = reachable.GraphKeys.ToHashSet(_comparer); - var sources = curation.SchemaAliases.Select(static row => row.Schema).ToHashSet(_comparer); + var reachableKeys = reachable.GraphKeys.ToHashSet(StringComparer.Ordinal); + var sources = curation.SchemaAliases.Select(static row => row.Schema).ToHashSet(StringComparer.Ordinal); // Identity is judged on the graph as it will be bound, so one alias can be what makes // a second pair identical — the mechanical collapse included. // A duplicated source is refused by the row contract; Compose tolerates it. diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs index fbbe6e90..b4a6008b 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaNameResolver.cs @@ -4,11 +4,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; -internal sealed class SchemaNameResolver +internal static class SchemaNameResolver { - private readonly StringComparer _comparer = StringComparer.Ordinal; - - public IReadOnlyDictionary Resolve(SpecDocument document, ReachableSchemaSet reachable, + public static IReadOnlyDictionary Resolve(SpecDocument document, ReachableSchemaSet reachable, IReadOnlyList selected, GenerationCuration curation, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(document); @@ -17,18 +15,18 @@ public IReadOnlyDictionary Resolve(SpecDocument document, Reacha ArgumentNullException.ThrowIfNull(curation); ArgumentNullException.ThrowIfNull(errors); - var responseRoots = reachable.ResponseRootKeys.ToHashSet(_comparer); - var requestRoots = ResolveRequestBodyRootNames(selected, errors); - var queryEnumRoots = ResolveQueryEnumRootNames(document, selected); - var payloadRoots = ResolveEnvelopePayloadRootNames(document, selected, errors); - var effectStreamTypes = ResolveEffectStreamTypeNames(document, selected); + var responseRoots = reachable.ResponseRootKeys.ToHashSet(StringComparer.Ordinal); + var requestRoots = RequestBodyRootNameResolver.Resolve(selected, errors); + var queryEnumRoots = QueryEnumRootNameResolver.Resolve(document, selected); + var payloadRoots = EnvelopePayloadRootNameResolver.Resolve(document, selected, errors); + var effectStreamTypes = EffectStreamRootNameResolver.Resolve(document, selected); var artifacts = new ProjectionArtifactNamePolicy(document.Schemas.Keys); var curatedNames = curation .SchemaNames .DistinctBy(static row => row.Schema, StringComparer.Ordinal) .ToDictionary(static row => row.Schema, static row => row.DotNetName, StringComparer.Ordinal); - var result = new Dictionary(_comparer); - var owners = new Dictionary(_comparer); + var result = new Dictionary(StringComparer.Ordinal); + var owners = new Dictionary(StringComparer.Ordinal); foreach (var key in reachable.GraphKeys) { if (responseRoots.Contains(key) || !document.Schemas.TryGetValue(key, out var schema) @@ -88,45 +86,6 @@ public IReadOnlyDictionary Resolve(SpecDocument document, Reacha return new ReadOnlyDictionary(result); } - /// - /// The extension gives its cause graph a shared semantic role independent of the operation - /// carrying it. The pin currently exposes one such contract; a second selected contract - /// claiming the same names would hit the ordinary owner collision below rather than silently - /// merging distinct schemas. - /// - private static Dictionary ResolveEffectStreamTypeNames(SpecDocument document, - IReadOnlyList selected) - { - var names = new Dictionary(StringComparer.Ordinal); - foreach (var extension in selected - .SelectMany(static operation => operation.Responses) - .Select(static response => response.EffectStream) - .OfType()) - { - if (extension.CauseSchema is not ArrayNode { Item: RefNode item } - || !document.Schemas.TryGetValue(item.Target, out var itemSchema) - || itemSchema is not UnionNode { Classification: UnionClassification.Marked } union) - { - continue; - } - - names[item.Target] = EffectStreamTypeNamePolicy.CauseInterface; - foreach (var target in union.Branches.OfType().Select(static branch => branch.Target)) - { - if (!document.Schemas.TryGetValue(target, out var branchSchema) - || branchSchema is not ObjectNode objectNode - || objectNode.LiteralMarkers.FirstOrDefault(static marker => marker.PropertyName is ErrorMarkerPolicy.WireName) is not { } marker) - { - continue; - } - - names[target] = EffectStreamTypeNamePolicy.CauseVariant(marker.Value); - } - } - - return names; - } - /// /// A union that carries no choice is not a type of its own — it binds to what its branches /// already are (), so it never claims a C# name. @@ -138,171 +97,7 @@ private static Dictionary ResolveEffectStreamTypeNames(SpecDocum _ => false, }; - /// - /// Every selected operation's nominal body root — inline or component-referenced — is - /// named from its operation identity, the mechanical {Subject}{Verb}Request rule - /// (public names never surface raw operation ids or dotted component spellings). - /// Ownership is scoped to the selection so a pending operation can never rename a - /// component the selected closure shares; two selected operations claiming one root - /// under different names refuse. Nested component dependencies stay shared: only the - /// root reference is claimed. - /// - private Dictionary ResolveRequestBodyRootNames(IReadOnlyList selected, - BindingErrorCollector errors) - { - var names = new Dictionary(_comparer); - foreach (var operation in selected) - { - if (operation.RequestBody?.Schema is not RefNode reference) - { - continue; - } - - var requestName = OperationNamePolicy.RequestTypeName(operation); - if (names.TryGetValue(reference.Target, out var existing) && !_comparer.Equals(existing, requestName)) - { - errors.Add( - BindingErrorCategory.Naming, - reference.Target, - $"request body root is claimed as both '{existing}' and '{requestName}' by selected operations"); - continue; - } - - names[reference.Target] = requestName; - } - - return names; - } - - /// - /// An inline query enum that ingestion promoted into the graph carries no schema identity - /// of its own — its key is the operation's parameter pointer — so it is named from the - /// request record it is a member of, mechanically {RequestTypeName}{PropertyName}. - /// A component-referenced enum keeps its component identity and is skipped here. Each - /// promoted key embeds one operation id and one parameter pointer, so no two selected - /// operations can claim the same key; a reasoned schemaNames row still overrides. - /// - private Dictionary ResolveQueryEnumRootNames(SpecDocument document, IReadOnlyList selected) - { - var names = new Dictionary(_comparer); - foreach (var operation in selected) - { - foreach (var parameter in operation.Parameters.Where(static parameter => - parameter.Location is SpecParameterLocation.Query)) - { - var key = QueryEnumShapePolicy.ResolveModelKey(parameter.Schema, document.Schemas); - if (key is null || !key.Contains('#', StringComparison.Ordinal)) - { - continue; - } - - names[key] = $"{OperationNamePolicy.RequestTypeName(operation)}{CSharpNamePolicy.ToPascalCase(parameter.Name)}"; - } - } - - return names; - } - - /// - /// A success payload that ingestion promoted into the graph carries no schema identity of - /// its own: it was promoted out of envelope spine — the operation's own inline root, or a - /// wrapper the dialect never names — and neither spelling belongs on the public surface. - /// Such a payload is named from the operation instead, mechanically - /// (), uniformly for the bare body root, for - /// a data wrapper's data member, and for a location wrapper's data — the list's - /// item, or the single object itself. A component-referenced payload keeps its component - /// identity, and a reasoned schemaNames row still overrides. Cursor-list items stay - /// nominal (ADR-0017), so that position never carries a promoted key for this to claim. - /// - private Dictionary ResolveEnvelopePayloadRootNames(SpecDocument document, - IReadOnlyList selected, BindingErrorCollector errors) - { - var names = new Dictionary(_comparer); - foreach (var operation in selected) - { - if (operation.Responses.FirstOrDefault(static response => response.StatusCode is 200) is not - { IsSse: false, Schema: RefNode reference, } success) - { - continue; - } - - // Cursor-list items stay nominal, so that shape has no promoted key to claim; the - // remaining shapes never reach a payload at all. - var payloadKey = success.EnvelopeShape switch - { - SpecEnvelopeShape.Bare => reference.Target, - SpecEnvelopeShape.Data => ResolveDataMemberKey(document, reference.Target), - SpecEnvelopeShape.DataLocation => ResolveDataLocationPayloadKey(document, reference.Target), - // A single-key wrapper's value is nominal or a primitive shape the type - // machinery names on its own, so that shape never promotes a payload key here. - SpecEnvelopeShape.SingleKey or SpecEnvelopeShape.CursorData - or SpecEnvelopeShape.DataHasMore or SpecEnvelopeShape.None => null, - _ => null, - }; - if (payloadKey is null || !payloadKey.Contains('#', StringComparison.Ordinal)) - { - continue; - } - - // A bare root embeds the operation id and cannot be shared, but a component data - // wrapper can be: two operations claiming one payload under different names refuse - // rather than letting iteration order pick a winner. - var payloadName = OperationNamePolicy.PayloadTypeName(operation); - if (names.TryGetValue(payloadKey, out var existing) && !_comparer.Equals(existing, payloadName)) - { - errors.Add( - BindingErrorCategory.Naming, - payloadKey, - $"envelope payload is claimed as both '{existing}' and '{payloadName}' by selected operations"); - continue; - } - - names[payloadKey] = payloadName; - } - - return names; - } - - /// - /// Resolves the wrapper member the payload binder reads, matching the single-required-data - /// shape EnvelopeFacetBinder.BindDataPayload accepts; any other wrapper refuses there. - /// - private static string? ResolveDataMemberKey(SpecDocument document, string wrapperKey) => - document.Schemas.TryGetValue(wrapperKey, out var wrapper) - && wrapper is ObjectNode { Properties: [{ Name: "data", IsRequired: true, Schema: RefNode payload, }] } - ? payload.Target - : null; - - /// - /// Resolves the graph key of a location wrapper's data payload — the array's item for - /// a list, the member itself for a single value. The shape check is - /// , the same call - /// EnvelopeFacetBinder.ResolveDataLocationWrapper makes, so what this claims and what - /// the binder reads cannot drift apart. A real top-level component payload is filtered back - /// out by this method's caller (its key carries no '#'), which is also what keeps the - /// ref-to-named-array shape out. - /// - private static string? ResolveDataLocationPayloadKey(SpecDocument document, string wrapperKey) - { - if (!document.Schemas.TryGetValue(wrapperKey, out var target) || target is not ObjectNode wrapper) - { - return null; - } - - if (!EnvelopeWrapperShape.IsDataLocation(wrapper, out var data) || data is null) - { - return null; - } - - return data.Schema switch - { - ArrayNode { Item: RefNode item } => item.Target, - RefNode payload => payload.Target, - _ => null, - }; - } - - private string ResolveUnionName(string key, UnionNode union, IReadOnlyDictionary graph, + private static string ResolveUnionName(string key, UnionNode union, IReadOnlyDictionary graph, ProjectionArtifactNamePolicy artifacts) { if (!key.Contains('#', StringComparison.Ordinal)) @@ -337,7 +132,7 @@ private string ResolveUnionName(string key, UnionNode union, IReadOnlyDictionary var owner = CSharpNamePolicy.ToPascalCase(GetRoot(key)); var common = string.Concat(commonWords.Select(CSharpNamePolicy.ToPascalCase)); - return common.Length > 0 && !_comparer.Equals(common, owner) + return common.Length > 0 && !StringComparer.Ordinal.Equals(common, owner) ? common : ResolveDefault(key, artifacts); } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs index 4dcfadf0..c048dce1 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SchemaPlanBinder.cs @@ -3,41 +3,27 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; -internal sealed class SchemaPlanBinder( - StructuralUnionPlanBinder structuralUnions, - UnionMembershipValidator unionMemberships, - UnionDiscriminatorSelector discriminators, - UnionMemberHoistBinder memberHoists) +internal sealed class SchemaPlanBinder(SchemaBindingContext context) { - private readonly StringComparer _comparer = StringComparer.Ordinal; - private readonly StructuralUnionPlanBinder _structuralUnions = structuralUnions - ?? throw new ArgumentNullException(nameof(structuralUnions)); - private readonly UnionMembershipValidator _unionMemberships = unionMemberships - ?? throw new ArgumentNullException(nameof(unionMemberships)); - private readonly UnionDiscriminatorSelector _discriminators = discriminators - ?? throw new ArgumentNullException(nameof(discriminators)); - private readonly UnionMemberHoistBinder _memberHoists = memberHoists ?? throw new ArgumentNullException(nameof(memberHoists)); + private readonly SchemaBindingContext _context = context; - public SchemaBindingResult Bind(SpecDocument document, ReachableSchemaSet reachable, GenerationCuration curation, - IReadOnlyDictionary typeNames, BindingErrorCollector errors) + public SchemaBindingResult Bind(GenerationCuration curation) { - ArgumentNullException.ThrowIfNull(document); - ArgumentNullException.ThrowIfNull(reachable); ArgumentNullException.ThrowIfNull(curation); - ArgumentNullException.ThrowIfNull(typeNames); - ArgumentNullException.ThrowIfNull(errors); - var responseRoots = reachable.ResponseRootKeys.ToHashSet(_comparer); - var requestKeys = reachable.RequestReachableKeys.ToHashSet(_comparer); - var streamCauseKeys = reachable.StreamCauseKeys.ToHashSet(_comparer); - var inhabitation = new SchemaInhabitationPolicy(document.Schemas); - var typeBinder = new TypePlanBinder(document.Schemas, typeNames, errors); - var context = new SchemaBindingContext(document, reachable, responseRoots, streamCauseKeys, typeNames, typeBinder, inhabitation, errors); - var decisions = BindExplicitUnions(context, responseRoots); - var errorUnion = new ErrorUnionBinder(context).Bind(); + var document = _context.Document; + var reachable = _context.Reachable; + var typeNames = _context.TypeNames; + var errors = _context.Errors; + var responseRoots = _context.ResponseRootKeys; + var requestKeys = reachable.RequestReachableKeys.ToHashSet(StringComparer.Ordinal); + var streamCauseKeys = _context.StreamCauseKeys; + var inhabitation = _context.Inhabitation; + var decisions = BindExplicitUnions(); + var errorUnion = new ErrorUnionBinder(_context).Bind(); decisions.Add(errorUnion.Union); var graph = UnionInheritanceGraph.Create(decisions, streamCauseKeys, errors); - var objects = new ObjectModelBinder(context); + var objects = new ObjectModelBinder(_context); var models = new List(); foreach (var key in reachable.GraphKeys) { @@ -66,7 +52,7 @@ public SchemaBindingResult Bind(SpecDocument document, ReachableSchemaSet reacha break; case UnionNode { Classification: UnionClassification.Structural } structural when UnstructuredUnionPolicy.Collapse(structural, document.Schemas) is null: - var structuralPlan = _structuralUnions.Bind(key, name, structural, document.Schemas, typeBinder, errors); + var structuralPlan = StructuralUnionPlanBinder.Bind(_context, key, name, structural); if (structuralPlan is not null) { models.Add(structuralPlan); @@ -76,12 +62,12 @@ when UnstructuredUnionPolicy.Collapse(structural, document.Schemas) is null: } } - var orderedModels = models.OrderBy(static model => model.Name, _comparer).ToArray(); - var orderedUnions = graph.Unions.OrderBy(static union => union.Name, _comparer).ToArray(); - _unionMemberships.Validate([.. orderedModels.OfType()], orderedUnions, errors); + var orderedModels = models.OrderBy(static model => model.Name, StringComparer.Ordinal).ToArray(); + var orderedUnions = graph.Unions.OrderBy(static union => union.Name, StringComparer.Ordinal).ToArray(); + UnionMembershipValidator.Validate([.. orderedModels.OfType()], orderedUnions, errors); // Hoisting adds members after membership validation. Its interfaces never enter the // registry because no converter or source-generated metadata materializes them. - var hoisted = _memberHoists.Bind(orderedUnions, orderedModels, curation, errors); + var hoisted = UnionMemberHoistBinder.Bind(orderedUnions, orderedModels, curation, errors); orderedModels = [.. hoisted.Models]; orderedUnions = [.. hoisted.Unions]; return new SchemaBindingResult @@ -99,16 +85,16 @@ when UnstructuredUnionPolicy.Collapse(structural, document.Schemas) is null: }; } - private List BindExplicitUnions(SchemaBindingContext context, HashSet responseRoots) + private List BindExplicitUnions() { - var markedBinder = new MarkedUnionBinder(context, _discriminators); + var markedBinder = new MarkedUnionBinder(_context); var decisions = new List(); - foreach (var key in context.Reachable.GraphKeys) + foreach (var key in _context.Reachable.GraphKeys) { - if (!responseRoots.Contains(key) - && context.Document.Schemas.TryGetValue(key, out var schema) + if (!_context.ResponseRootKeys.Contains(key) + && _context.Document.Schemas.TryGetValue(key, out var schema) && schema is UnionNode { Classification: UnionClassification.Marked } union - && context.TypeNames.TryGetValue(key, out var name)) + && _context.TypeNames.TryGetValue(key, out var name)) { decisions.Add(markedBinder.Bind(key, name, union)); } @@ -118,7 +104,7 @@ private List BindExplicitUnions(SchemaBindingContext context } /// The materializable types the serializer registry must carry, never hoisted interfaces. - private string[] ComposeRegistryNames(IReadOnlyList models, IReadOnlyList unions) => + private static string[] ComposeRegistryNames(IReadOnlyList models, IReadOnlyList unions) => [ .. models .Select(static model => model.Name) @@ -128,8 +114,8 @@ .. models .Where(static arm => arm.Type.IsCollection) .Select(static arm => TypeReferenceNamePolicy.Format(arm.Type))) .Concat(unions.SelectMany(static union => new[] { union.Name, union.UnknownTypeName, })) - .Distinct(_comparer) - .Order(_comparer), + .Distinct(StringComparer.Ordinal) + .Order(StringComparer.Ordinal), ]; /// Each value's member name comes from . diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs index ea8756d1..2c53bbad 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/SpecBinder.cs @@ -5,25 +5,8 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; -internal sealed class SpecBinder( - ReachableSchemaCollector reachableSchemas, - StabilizeDuplicatePolicy stabilizeDuplicates, - CurationValidator curationValidator, - SchemaAliasValidator schemaAliasValidator, - SchemaAliasApplier schemaAliases, - SchemaNameResolver schemaNames, - SchemaPlanBinder schemaPlans, - OperationPlanBinder operationPlans) : ISpecBinder +internal sealed class SpecBinder : ISpecBinder { - private readonly CurationValidator _curationValidator = curationValidator ?? throw new ArgumentNullException(nameof(curationValidator)); - private readonly OperationPlanBinder _operationPlans = operationPlans ?? throw new ArgumentNullException(nameof(operationPlans)); - private readonly ReachableSchemaCollector _reachableSchemas = reachableSchemas ?? throw new ArgumentNullException(nameof(reachableSchemas)); - private readonly SchemaAliasApplier _schemaAliases = schemaAliases ?? throw new ArgumentNullException(nameof(schemaAliases)); - private readonly SchemaAliasValidator _schemaAliasValidator = schemaAliasValidator ?? throw new ArgumentNullException(nameof(schemaAliasValidator)); - private readonly SchemaNameResolver _schemaNames = schemaNames ?? throw new ArgumentNullException(nameof(schemaNames)); - private readonly SchemaPlanBinder _schemaPlans = schemaPlans ?? throw new ArgumentNullException(nameof(schemaPlans)); - private readonly StabilizeDuplicatePolicy _stabilizeDuplicates = stabilizeDuplicates ?? throw new ArgumentNullException(nameof(stabilizeDuplicates)); - public EmitPlan Bind(SpecDocument document, OperationSelection selection, GenerationCuration curation) { ArgumentNullException.ThrowIfNull(document); @@ -33,35 +16,36 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera var errors = new BindingErrorCollector(); var operationsById = document.Operations.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal); var selected = SelectOperations(selection, operationsById, errors); - var reachable = _reachableSchemas.Collect(document, selected, errors); + var reachable = ReachableSchemaCollector.Collect(document, selected, errors); CurationRowContract.ValidateBeforeAliases(curation, errors); // Upstream stabilize's per-site duplicates collapse mechanically, ahead of curation: // curated rows are then judged against the graph as it will be bound, and a row that // repeats a mechanical fold is refused as redundant instead of shadowing it. - var collapse = _stabilizeDuplicates.Resolve(document, reachable, curation.SchemaNames, errors); - _curationValidator.Validate(document, selected, reachable, curation, errors); - _schemaAliasValidator.Validate(document, reachable, curation, collapse, errors); + var collapse = StabilizeDuplicatePolicy.Resolve(document, reachable, curation.SchemaNames, errors); + CurationValidator.Validate(document, selected, reachable, curation, errors); + SchemaAliasValidator.Validate(document, reachable, curation, collapse, errors); // Aliases are validated against the raw document above; the collapse happens before // names resolve so the rest of the pipeline never sees the duplicates. Reachability // recollects on the transformed document because an alias target's promoted inline // children only become reachable through the rewritten references. - var aliased = _schemaAliases.Apply(document, collapse, curation.SchemaAliases); + var aliased = SchemaAliasApplier.Apply(document, collapse, curation.SchemaAliases); if (!ReferenceEquals(aliased, document)) { document = aliased; var aliasedById = document.Operations.ToDictionary(static operation => operation.OperationId, StringComparer.Ordinal); selected = Array.AsReadOnly([.. selected.Select(operation => aliasedById[operation.OperationId])]); - reachable = _reachableSchemas.Collect(document, selected, errors); + reachable = ReachableSchemaCollector.Collect(document, selected, errors); } CurationValidator.ValidateCollapsedSchemas(document, reachable, curation, collapse, errors); // Type names are resolved exactly once per bind; schema and operation binding // consume the same map so neither can reinterpret a schema under another name. - var typeNames = _schemaNames.Resolve(document, reachable, selected, curation, errors); - var schemaResult = _schemaPlans.Bind(document, reachable, curation, typeNames, errors); - var clients = _operationPlans.Bind(document, selected, curation, schemaResult, errors); + var typeNames = SchemaNameResolver.Resolve(document, reachable, selected, curation, errors); + var schemaContext = SchemaBindingContext.Create(document, reachable, typeNames, errors); + var schemaResult = new SchemaPlanBinder(schemaContext).Bind(curation); + var clients = OperationPlanBinder.Bind(document, selected, curation, schemaResult, errors); var candidates = new OwnershipCandidates { Models = schemaResult.Models, @@ -69,22 +53,18 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera HoistedInterfaces = schemaResult.HoistedInterfaces, }; var inventory = GeneratedTypeNameInventory.Create(candidates, clients, schemaResult.HoistedClaims, typeNames); - var refusedHoists = _operationPlans.CheckTypeNameCollisions(inventory, clients, errors); + var refusedHoists = OperationPlanBinder.CheckTypeNameCollisions(inventory, clients, errors); var pending = PendingOperations(document, selected, curation); - var models = AttachRequestQueryProperties(schemaResult.Models, clients, errors); - models = BindSessionScopedEvents(document, typeNames, schemaResult with { Models = models }, clients, - curation, SchemaAliasApplier.Compose(collapse, curation.SchemaAliases), errors); - - var masked = SecretMemberPolicy.MaskUnknownPayloads(SecretMemberPolicy.Apply(models, curation, errors), schemaResult.Unions); + var masked = BindModelFacets(schemaContext, schemaResult, clients, curation, collapse); CheckDtoNameCollisions(inventory, errors); candidates = candidates with { Models = masked.Models, Unions = masked.Unions, HoistedInterfaces = [.. candidates.HoistedInterfaces.Where(plan => !refusedHoists.Contains(plan.Name))] }; InternalNameCollisionCheck.Check(inventory, errors); // An unplaced model never refuses a bind: it is the generate command's wall, so the // pending-operation probe still judges bindability under its synthetic curation. var placement = ModelOwnershipPolicy.Place(candidates, clients, curation, errors); - var reach = ModelReachCollector.Collect(_reachableSchemas, new ReachScope(document, selected, clients, typeNames), candidates, errors); - var experimental = ExperimentalModelPolicy.Collect(_reachableSchemas, new ReachScope(document, selected, clients, typeNames), candidates, inventory, errors); + var reach = ModelReachCollector.Collect(new ReachScope(document, selected, clients, typeNames), candidates, errors); + var experimental = ExperimentalModelPolicy.Collect(new ReachScope(document, selected, clients, typeNames), candidates, inventory, errors); errors.ThrowIfAny(); var plan = new EmitPlan { @@ -106,6 +86,32 @@ public EmitPlan Bind(SpecDocument document, OperationSelection selection, Genera return plan; } + /// Attaches operation-owned model properties and session-event roles before masking secret payloads. + private static UnknownPayloadMaskResult BindModelFacets(SchemaBindingContext context, SchemaBindingResult schemas, + IReadOnlyList clients, GenerationCuration curation, StabilizeDuplicateCollapse collapse) + { + var models = AttachRequestQueryProperties(schemas.Models, clients, context.Errors); + if (curation.SessionScopedEvent is { } row) + { + // The row's identifier is read through the same alias map the schemas were bound with, + // so a stabilized duplicate of the identifier schema is the identifier too. + var aliases = SchemaAliasApplier.Compose(collapse, curation.SchemaAliases); + var identifier = !string.IsNullOrWhiteSpace(row.Identifier) && aliases.TryGetValue(row.Identifier, out var target) + ? target + : row.Identifier; + models = SessionScopedEventBinder.Bind(new SessionScopedEventBindingContext + { + Document = context.Document, + TypeNames = context.TypeNames, + Schemas = schemas with { Models = models }, + Clients = clients, + Row = row with { Identifier = identifier }, + }, context.Errors); + } + + return SecretMemberPolicy.MaskUnknownPayloads(SecretMemberPolicy.Apply(models, curation, context.Errors), schemas.Unions); + } + /// Declined operations retain their reviewed reasons in ordinal identity order. private static IReadOnlyList DeclinedOperations(GenerationCuration curation) => [ @@ -148,33 +154,6 @@ .. document /// A merged operation's query properties ride its body model; the model plan gains /// them here because schema binding cannot see operation placement. /// - /// - /// Marks the session-scoped event records when the curation names the rule. The row's identifier - /// is read through the same alias map the schemas were bound with, so a stabilized duplicate of - /// the identifier schema is the identifier too. - /// - private static IReadOnlyList BindSessionScopedEvents(SpecDocument document, - IReadOnlyDictionary typeNames, SchemaBindingResult schemas, IReadOnlyList clients, - GenerationCuration curation, IReadOnlyDictionary aliases, BindingErrorCollector errors) - { - if (curation.SessionScopedEvent is not { } row) - { - return schemas.Models; - } - - var identifier = !string.IsNullOrWhiteSpace(row.Identifier) && aliases.TryGetValue(row.Identifier, out var target) - ? target - : row.Identifier; - return SessionScopedEventBinder.Bind(new SessionScopedEventBindingContext - { - Document = document, - TypeNames = typeNames, - Schemas = schemas, - Clients = clients, - Row = row with { Identifier = identifier }, - }, errors); - } - private static IReadOnlyList AttachRequestQueryProperties(IReadOnlyList models, IReadOnlyList clients, BindingErrorCollector errors) { diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/StabilizeDuplicatePolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/StabilizeDuplicatePolicy.cs index 1f30c075..9321d81b 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/StabilizeDuplicatePolicy.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/StabilizeDuplicatePolicy.cs @@ -25,12 +25,10 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; /// ordinary curation and naming checks validate those rows after this policy resolves aliases. A /// row naming a candidate that folds is refused as redundant. /// -internal sealed class StabilizeDuplicatePolicy +internal static class StabilizeDuplicatePolicy { - private readonly StringComparer _comparer = StringComparer.Ordinal; - /// Resolves aliases and reports non-equivalent candidates without explicit names. - public StabilizeDuplicateCollapse Resolve(SpecDocument document, ReachableSchemaSet reachable, + public static StabilizeDuplicateCollapse Resolve(SpecDocument document, ReachableSchemaSet reachable, IReadOnlyList schemaNames, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(document); @@ -44,8 +42,8 @@ public StabilizeDuplicateCollapse Resolve(SpecDocument document, ReachableSchema return StabilizeDuplicateCollapse.Empty; } - var explicitlyNamed = schemaNames.Select(static row => row.Schema).ToHashSet(_comparer); - var responseRoots = reachable.ResponseRootKeys.ToHashSet(_comparer); + var explicitlyNamed = schemaNames.Select(static row => row.Schema).ToHashSet(StringComparer.Ordinal); + var responseRoots = reachable.ResponseRootKeys.ToHashSet(StringComparer.Ordinal); foreach (var (schema, baseKey) in ReduceToFixpoint(document, candidates)) { // A reason-bearing name can preserve a distinct shape. The ordinary curation @@ -67,7 +65,7 @@ public StabilizeDuplicateCollapse Resolve(SpecDocument document, ReachableSchema // refresh that made the pair equivalent is the moment to retire it, so it is refused // rather than carried along silently (the schemaAliases validator treats a redundant // alias row the same way). - foreach (var (schema, baseKey) in candidates.OrderBy(static pair => pair.Key, _comparer)) + foreach (var (schema, baseKey) in candidates.OrderBy(static pair => pair.Key, StringComparer.Ordinal)) { if (explicitlyNamed.Contains(schema)) { @@ -82,9 +80,9 @@ public StabilizeDuplicateCollapse Resolve(SpecDocument document, ReachableSchema } /// Every reachable component spelled as a stabilize duplicate of a base the graph declares. - private Dictionary CollectCandidates(SpecDocument document, ReachableSchemaSet reachable) + private static Dictionary CollectCandidates(SpecDocument document, ReachableSchemaSet reachable) { - var candidates = new Dictionary(_comparer); + var candidates = new Dictionary(StringComparer.Ordinal); foreach (var key in reachable.GraphKeys) { if (TryGetStabilizeBase(key, out var baseKey) @@ -103,7 +101,7 @@ private Dictionary CollectCandidates(SpecDocument document, Reac /// Drops every candidate that is not deeply equal to its base under the surviving map and /// repeats until nothing drops, so a refusal cascades into the pairs that leaned on it. /// - private IReadOnlyList> ReduceToFixpoint(SpecDocument document, + private static IReadOnlyList> ReduceToFixpoint(SpecDocument document, Dictionary candidates) { var refused = new List>(); @@ -111,7 +109,7 @@ private IReadOnlyList> ReduceToFixpoint(SpecDocumen do { dropped = false; - foreach (var candidate in candidates.OrderBy(static pair => pair.Key, _comparer).ToArray()) + foreach (var candidate in candidates.OrderBy(static pair => pair.Key, StringComparer.Ordinal).ToArray()) { if (IsSpellingOfBase(document, candidate, candidates)) { @@ -125,7 +123,7 @@ private IReadOnlyList> ReduceToFixpoint(SpecDocumen } while (dropped); - return [.. refused.OrderBy(static pair => pair.Key, _comparer)]; + return [.. refused.OrderBy(static pair => pair.Key, StringComparer.Ordinal)]; } private static bool IsSpellingOfBase(SpecDocument document, KeyValuePair candidate, diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/StructuralUnionPlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/StructuralUnionPlanBinder.cs index 3ad16d9c..84e19c5f 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/StructuralUnionPlanBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/StructuralUnionPlanBinder.cs @@ -4,35 +4,30 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; -internal sealed class StructuralUnionPlanBinder +internal static class StructuralUnionPlanBinder { - private readonly StringComparer _comparer = StringComparer.Ordinal; - - public StructuralUnionModelPlan? Bind(string key, string name, UnionNode union, - IReadOnlyDictionary graph, TypePlanBinder typeBinder, BindingErrorCollector errors) + public static StructuralUnionModelPlan? Bind(SchemaBindingContext context, string key, string name, UnionNode union) { + ArgumentNullException.ThrowIfNull(context); ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentException.ThrowIfNullOrWhiteSpace(name); ArgumentNullException.ThrowIfNull(union); - ArgumentNullException.ThrowIfNull(graph); - ArgumentNullException.ThrowIfNull(typeBinder); - ArgumentNullException.ThrowIfNull(errors); - - var scope = new ArmScope(key, typeBinder, errors); - var arms = new List(union.Branches.Count); - var claimedTokens = new HashSet(); + var graph = context.Document.Schemas; + var errors = context.Errors; + var scope = new ArmScope(key, context.Types, errors); + var arms = scope.Arms; var objectArms = new List(); var inhabitableBranchCount = 0; foreach (var (branch, index) in union.Branches.Select(static (branch, index) => (branch, index))) { - var resolved = Resolve(branch, graph, []); + var resolved = RefChain.Resolve(branch, graph).Node; if (resolved is NeverNode) { continue; } inhabitableBranchCount++; - var arm = BindArm(scope, branch, resolved, index, arms, claimedTokens, sharesObjectToken: objectArms.Count > 0); + var arm = BindArm(scope, branch, resolved, index, sharesObjectToken: objectArms.Count > 0); if (arm is null) { continue; @@ -44,7 +39,7 @@ internal sealed class StructuralUnionPlanBinder } arms.Add(arm); - claimedTokens.UnionWith(arm.Tokens); + scope.ClaimedTokens.UnionWith(arm.Tokens); } if (arms.Count < 2 || arms.Count != inhabitableBranchCount) @@ -73,8 +68,8 @@ internal sealed class StructuralUnionPlanBinder /// a dictionary, a free-form object, a marked union - takes every object, so a neighbour /// there is ambiguous and stays refused. /// - private StructuralUnionArmPlan? BindArm(ArmScope scope, SchemaNode branch, SchemaNode resolved, int index, - IReadOnlyList arms, HashSet claimedTokens, bool sharesObjectToken) + private static StructuralUnionArmPlan? BindArm(ArmScope scope, SchemaNode branch, SchemaNode resolved, int index, + bool sharesObjectToken) { var armNameSubject = index.ToString(System.Globalization.CultureInfo.InvariantCulture); if (!TryGetTokens(resolved, out var tokens)) @@ -87,13 +82,13 @@ internal sealed class StructuralUnionPlanBinder var firstMatch = resolved is ObjectNode && sharesObjectToken; var effective = firstMatch ? branch - : ResolveEffectiveBranch(branch, resolved, tokens, claimedTokens, scope.Key, index, scope.Errors); + : ResolveEffectiveBranch(scope, branch, resolved, tokens, index); if (effective is null) { return null; } - JsonTokenType[] effectiveTokens = firstMatch ? [.. tokens] : [.. tokens.Where(token => !claimedTokens.Contains(token))]; + JsonTokenType[] effectiveTokens = firstMatch ? [.. tokens] : [.. tokens.Where(token => !scope.ClaimedTokens.Contains(token))]; if (!ValidateSpecialNumberArm(resolved, effectiveTokens, scope.Key, scope.Errors)) { return null; @@ -106,7 +101,7 @@ internal sealed class StructuralUnionPlanBinder } var armName = ArmName(type); - return ValidateArmName(armName, arms, scope.Key, scope.Errors) + return ValidateArmName(armName, scope.Arms, scope.Key, scope.Errors) ? new StructuralUnionArmPlan { Name = armName, Type = type, Tokens = effectiveTokens } : null; } @@ -176,7 +171,7 @@ private static bool TryClaimObjectArms(List arms, IReadO var valid = true; foreach (var property in objectArm.Node.Properties) { - switch (Resolve(property.Schema, graph, [])) + switch (RefChain.Resolve(property.Schema, graph).Node) { case LiteralNode { Kind: LiteralKind.Number }: errors.Add(BindingErrorCategory.Schema, key, string.Create( @@ -216,7 +211,11 @@ private static bool Subsumes(StructuralObjectClaimPlan earlier, StructuralObject private sealed record ObjectArm(int ArmIndex, int BranchIndex, ObjectNode Node); - private sealed record ArmScope(string Key, TypePlanBinder TypeBinder, BindingErrorCollector Errors); + private sealed record ArmScope(string Key, TypePlanBinder TypeBinder, BindingErrorCollector Errors) + { + public List Arms { get; } = []; + public HashSet ClaimedTokens { get; } = []; + } private static StructuralUnionModelPlan CreatePlan(string name, UnionNode union, IReadOnlyList arms) => @@ -258,7 +257,7 @@ private static bool ValidateNotBinaryArm(TypeReferencePlan type, string armNameS return false; } - private bool ValidateArmName(string armName, IReadOnlyList arms, string key, + private static bool ValidateArmName(string armName, IReadOnlyList arms, string key, BindingErrorCollector errors) { if (armName is "Kind" or "Unknown") @@ -268,7 +267,7 @@ private bool ValidateArmName(string armName, IReadOnlyList _comparer.Equals(arm.Name, armName))) + if (!arms.Any(arm => StringComparer.Ordinal.Equals(arm.Name, armName))) { return true; } @@ -278,17 +277,16 @@ private bool ValidateArmName(string armName, IReadOnlyList tokens, HashSet claimedTokens, string key, int index, - BindingErrorCollector errors) + private static SchemaNode? ResolveEffectiveBranch(ArmScope scope, SchemaNode original, SchemaNode resolved, + IReadOnlyList tokens, int index) { - var overlap = tokens.Where(claimedTokens.Contains).ToArray(); + var overlap = tokens.Where(scope.ClaimedTokens.Contains).ToArray(); if (overlap.Length is 0) { return original; } - var remaining = tokens.Where(token => !claimedTokens.Contains(token)).ToArray(); + var remaining = tokens.Where(token => !scope.ClaimedTokens.Contains(token)).ToArray(); if (resolved is SpecialNumberNode && overlap is [JsonTokenType.String] && remaining is [JsonTokenType.Number]) @@ -302,22 +300,11 @@ private bool ValidateArmName(string armName, IReadOnlyList token.ToString())); - errors.Add(BindingErrorCategory.Schema, key, + scope.Errors.Add(BindingErrorCategory.Schema, scope.Key, $"structural union branch {index.ToString(System.Globalization.CultureInfo.InvariantCulture)} overlaps earlier branch token kind(s): {tokenNames}"); return null; } - private static SchemaNode Resolve(SchemaNode node, IReadOnlyDictionary graph, HashSet visited) - { - if (node is not RefNode reference || !visited.Add(reference.Target) - || !graph.TryGetValue(reference.Target, out var target)) - { - return node; - } - - return Resolve(target, graph, visited); - } - private static bool TryGetTokens(SchemaNode branch, out IReadOnlyList tokens) { tokens = branch switch diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/TypePlanBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/TypePlanBinder.cs index a85f17b2..8222391b 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/TypePlanBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/TypePlanBinder.cs @@ -45,16 +45,16 @@ internal sealed class TypePlanBinder( NeverNode => Refuse(subject, "never schemas cannot materialize a .NET value"), SpecialNumberNode => SpecialNumber(), NullableNode nullable => BindNullable(nullable, subject, aliases), - JsonStringNode => Refuse(subject, "JSON-encoded strings are not supported by the M1 emitter"), + JsonStringNode => Refuse(subject, "JSON-encoded strings are not supported by the emitter"), EncodedStringNode { ContentEncoding: "base64" } => Binary(), EncodedStringNode encoded => Refuse( subject, $"content encoding '{encoded.ContentEncoding}' is not supported by the emitter; only base64 materializes as bytes"), - TupleNode => Refuse(subject, "tuple schemas are not supported by the M1 emitter"), + TupleNode => Refuse(subject, "tuple schemas are not supported by the emitter"), UnionNode { Classification: UnionClassification.Structural } union when UnstructuredUnionPolicy.Collapse(union, _graph) is { } collapsed => BindCore(collapsed, subject, aliases), ObjectNode or EnumNode or UnionNode => Refuse(subject, "inline nominal schema was not promoted into the graph"), - _ => Refuse(subject, $"schema node '{schema.GetType().Name}' is not supported by the M1 emitter"), + _ => Refuse(subject, $"schema node '{schema.GetType().Name}' is not supported by the emitter"), }; private TypeReferencePlan? BindReference(RefNode reference, string subject, HashSet aliases) diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionDiscriminatorSelector.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionDiscriminatorSelector.cs index a79061ff..7f77a25b 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionDiscriminatorSelector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionDiscriminatorSelector.cs @@ -14,12 +14,10 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; /// branch that also carries an unrelated prefix marker — a templated identifier, say — stays /// an ordinary variant, because that marker discriminates nothing. /// -internal sealed class UnionDiscriminatorSelector +internal static class UnionDiscriminatorSelector { - private readonly StringComparer _comparer = StringComparer.Ordinal; - /// Returns the discriminator every branch answers, or null when no candidate qualifies. - public UnionDiscriminator? Select(IReadOnlyList branches) + public static UnionDiscriminator? Select(IReadOnlyList branches) { ArgumentNullException.ThrowIfNull(branches); @@ -49,15 +47,15 @@ internal sealed class UnionDiscriminatorSelector /// tags the property neither way disqualifies the candidate outright. Several prefix carriers /// still qualify, so the arity refusal can name them. /// - private UnionDiscriminator? Qualify(IReadOnlyList branches, string property) + private static UnionDiscriminator? Qualify(IReadOnlyList branches, string property) { var literalBranches = new List(branches.Count); var prefixBranches = new List(); - var values = new HashSet(_comparer); + var values = new HashSet(StringComparer.Ordinal); LiteralMarker? marker = null; foreach (var branch in branches) { - var literal = branch.Markers.FirstOrDefault(candidate => _comparer.Equals(candidate.PropertyName, property)); + var literal = branch.Markers.FirstOrDefault(candidate => StringComparer.Ordinal.Equals(candidate.PropertyName, property)); if (literal is null) { if (branch.PrefixMarkers.Count is 0) diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMemberHoistBinder.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMemberHoistBinder.cs index b79ad570..c4cdce27 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMemberHoistBinder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMemberHoistBinder.cs @@ -14,11 +14,9 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; /// outer promise instead of hiding it. A union with fewer than two members shares nothing across /// arms and is left alone. /// -internal sealed class UnionMemberHoistBinder +internal static class UnionMemberHoistBinder { - private readonly StringComparer _comparer = StringComparer.Ordinal; - - public UnionHoistPlan Bind(IReadOnlyList unions, IReadOnlyList models, + public static UnionHoistPlan Bind(IReadOnlyList unions, IReadOnlyList models, GenerationCuration curation, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(unions); @@ -27,7 +25,7 @@ public UnionHoistPlan Bind(IReadOnlyList unions, IReadOnlyList>(_comparer); + var hoisted = new Dictionary>(StringComparer.Ordinal); foreach (var union in OrderByChainDepth(unions, context.Unions)) { hoisted[union.Name] = HoistUnion(union, context, hoisted); @@ -43,17 +41,17 @@ public UnionHoistPlan Bind(IReadOnlyList unions, IReadOnlyList unions, IReadOnlyList models, + private static HoistContext CreateContext(IReadOnlyList unions, IReadOnlyList models, GenerationCuration curation, BindingErrorCollector errors) { - var objectModels = models.OfType().ToDictionary(static model => model.Name, _comparer); + var objectModels = models.OfType().ToDictionary(static model => model.Name, StringComparer.Ordinal); var implementations = new HoistedImplementationAccumulator(); // One policy instance answers both the union interface and every carrier interface, so the // two can never disagree about which record property satisfies a declared member. var satisfaction = new HoistedMemberSatisfactionPolicy( - models.OfType().Select(static model => model.Name).ToHashSet(_comparer)); + models.OfType().Select(static model => model.Name).ToHashSet(StringComparer.Ordinal)); return new HoistContext( - unions.ToDictionary(static union => union.Name, _comparer), + unions.ToDictionary(static union => union.Name, StringComparer.Ordinal), objectModels, new HoistedMemberIdentityPolicy(objectModels), satisfaction, @@ -62,7 +60,7 @@ private HoistContext CreateContext(IReadOnlyList unions, IReadOnlyLis errors); } - private ReadOnlyCollection HoistUnion(UnionPlan union, HoistContext context, + private static ReadOnlyCollection HoistUnion(UnionPlan union, HoistContext context, IReadOnlyDictionary> hoisted) { var result = new List(); @@ -188,10 +186,10 @@ private static void RecordImplementations(UnionPlan union, List } } - private HashSet InheritedNames(UnionPlan union, IReadOnlyDictionary unions, + private static HashSet InheritedNames(UnionPlan union, IReadOnlyDictionary unions, IReadOnlyDictionary> hoisted) { - var result = new HashSet(_comparer) { union.MarkerName }; + var result = new HashSet(StringComparer.Ordinal) { union.MarkerName }; for (var current = union.BaseTypeName; current is not null && unions.TryGetValue(current, out var outer); current = outer.BaseTypeName) { diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMembershipValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMembershipValidator.cs index 76b8af22..ba26576a 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMembershipValidator.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnionMembershipValidator.cs @@ -3,18 +3,16 @@ namespace OpenCode.Sdk.Tools.Generator.Binding; -internal sealed class UnionMembershipValidator +internal static class UnionMembershipValidator { - private readonly StringComparer _comparer = StringComparer.Ordinal; - - public void Validate(IReadOnlyList models, IReadOnlyList unions, + public static void Validate(IReadOnlyList models, IReadOnlyList unions, BindingErrorCollector errors) { ArgumentNullException.ThrowIfNull(models); ArgumentNullException.ThrowIfNull(unions); ArgumentNullException.ThrowIfNull(errors); - var unionsByName = unions.ToDictionary(static union => union.Name, _comparer); + var unionsByName = unions.ToDictionary(static union => union.Name, StringComparer.Ordinal); foreach (var model in models) { ValidateModel(model, unionsByName, errors); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnstructuredUnionPolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnstructuredUnionPolicy.cs index c5ec1860..c2166508 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/UnstructuredUnionPolicy.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/UnstructuredUnionPolicy.cs @@ -15,7 +15,7 @@ internal static class UnstructuredUnionPolicy { ArgumentNullException.ThrowIfNull(union); - var kinds = union.Branches.Select(branch => PrimitiveKindOf(branch, graph, [])).ToArray(); + var kinds = union.Branches.Select(branch => PrimitiveKindOf(branch, graph)).ToArray(); if (kinds.Length > 1 && kinds[0] is { } first && Array.TrueForAll(kinds, kind => kind == first)) { return new PrimitiveNode @@ -31,12 +31,11 @@ internal static class UnstructuredUnionPolicy : null; } - private static PrimitiveKind? PrimitiveKindOf(SchemaNode branch, IReadOnlyDictionary? graph, HashSet visited) + private static PrimitiveKind? PrimitiveKindOf(SchemaNode branch, IReadOnlyDictionary? graph) { - if (branch is RefNode reference && graph is not null && visited.Add(reference.Target) - && graph.TryGetValue(reference.Target, out var target)) + if (graph is not null) { - return PrimitiveKindOf(target, graph, visited); + branch = RefChain.Resolve(branch, graph).Node; } return branch switch diff --git a/tools/OpenCode.Sdk.Tools/Generator/Binding/WireDocumentationPolicy.cs b/tools/OpenCode.Sdk.Tools/Generator/Binding/WireDocumentationPolicy.cs index de641178..2812edca 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Binding/WireDocumentationPolicy.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Binding/WireDocumentationPolicy.cs @@ -31,7 +31,7 @@ public static EmitPlan Apply(EmitPlan plan) continue; } - // An event record used anywhere besides its unions has a second role and keeps today's text. + // An event record used besides its unions keeps the fallback summary because its wire role is not unique. if (uses.GetValueOrDefault(model.Name) is 1) { Add(candidates, model.Name, $"The '{tag}' event."); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EffectStreamProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EffectStreamProjector.cs index 55692d64..3c4470a2 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EffectStreamProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EffectStreamProjector.cs @@ -8,13 +8,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; /// Projects the typed semantics carried by an x-effect-stream extension. internal sealed class EffectStreamProjector { - private readonly GraphKeyBuilder _keys; private readonly SchemaProjector _schemas; - public EffectStreamProjector(SchemaProjector schemas, GraphKeyBuilder keys) + public EffectStreamProjector(SchemaProjector schemas) { _schemas = schemas ?? throw new ArgumentNullException(nameof(schemas)); - _keys = keys ?? throw new ArgumentNullException(nameof(keys)); } public SpecEffectStreamContract? Project(JsonNode? source, string root, string mediaPointer, ProjectionState state) @@ -28,7 +26,7 @@ public EffectStreamProjector(SchemaProjector schemas, GraphKeyBuilder keys) return null; } - var pointer = _keys.Append(mediaPointer, "x-effect-stream"); + var pointer = GraphKeyBuilder.Append(mediaPointer, "x-effect-stream"); var location = string.Concat(root, pointer); if (source is not JsonObject extension) { @@ -52,7 +50,7 @@ public EffectStreamProjector(SchemaProjector schemas, GraphKeyBuilder keys) return null; } - var schemaPointer = _keys.Append(pointer, name); + var schemaPointer = GraphKeyBuilder.Append(pointer, name); var location = string.Concat(root, schemaPointer); if (raw is null) { @@ -85,7 +83,7 @@ public EffectStreamProjector(SchemaProjector schemas, GraphKeyBuilder keys) return _schemas.Project(parsed, root, schemaPointer, state); } - private string? ReadString(JsonObject extension, string name, string root, string pointer, IngestionErrorCollector errors) + private static string? ReadString(JsonObject extension, string name, string root, string pointer, IngestionErrorCollector errors) { if (!extension.TryGetPropertyValue(name, out var raw)) { @@ -97,7 +95,7 @@ public EffectStreamProjector(SchemaProjector schemas, GraphKeyBuilder keys) return text; } - errors.Add(string.Concat(root, _keys.Append(pointer, name)), $"'{name}' must contain a string"); + errors.Add(string.Concat(root, GraphKeyBuilder.Append(pointer, name)), $"'{name}' must contain a string"); return null; } } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs index 15bf310e..2274b2ab 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/EnumSchemaProjector.cs @@ -4,9 +4,9 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; -internal sealed class EnumSchemaProjector +internal static class EnumSchemaProjector { - public EnumNode? Project(OpenApiSchema schema, string location, IngestionErrorCollector errors) + public static EnumNode? Project(OpenApiSchema schema, string location, IngestionErrorCollector errors) { ArgumentNullException.ThrowIfNull(schema); ArgumentException.ThrowIfNullOrWhiteSpace(location); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs index 00eaea0a..7ba8b40c 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ErrorStyleClassifier.cs @@ -7,11 +7,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; /// Any other shape classifies as no error, and the binder refuses it wherever an error response /// references it. /// -internal sealed class ErrorStyleClassifier +internal static class ErrorStyleClassifier { public const string WireName = "_tag"; - public ErrorStyle Classify(IReadOnlyList properties) + public static ErrorStyle Classify(IReadOnlyList properties) { ArgumentNullException.ThrowIfNull(properties); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs index 1a50b67d..8a71151f 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/GraphKeyBuilder.cs @@ -3,16 +3,16 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; -internal sealed class GraphKeyBuilder +internal static class GraphKeyBuilder { - public string Root(string wireNameOrOpId) + public static string Root(string wireNameOrOpId) { ArgumentException.ThrowIfNullOrWhiteSpace(wireNameOrOpId); return wireNameOrOpId; } - public string Append(string parentPointer, string segment) + public static string Append(string parentPointer, string segment) { ArgumentNullException.ThrowIfNull(parentPointer); ArgumentNullException.ThrowIfNull(segment); @@ -24,7 +24,7 @@ public string Append(string parentPointer, string segment) return $"{parentPointer}/{escaped}"; } - public string UnionBranch(string parentPointer, string keyword, int index, LiteralMarker? marker) + public static string UnionBranch(string parentPointer, string keyword, int index, LiteralMarker? marker) { ArgumentNullException.ThrowIfNull(parentPointer); ArgumentException.ThrowIfNullOrWhiteSpace(keyword); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/JsonStringSchemaProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/JsonStringSchemaProjector.cs index cc420aa6..3ccec486 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/JsonStringSchemaProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/JsonStringSchemaProjector.cs @@ -5,13 +5,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; internal sealed class JsonStringSchemaProjector { - private readonly GraphKeyBuilder _keys; private readonly SchemaProjector _schemaProjector; - public JsonStringSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder keys) + public JsonStringSchemaProjector(SchemaProjector schemaProjector) { _schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector)); - _keys = keys ?? throw new ArgumentNullException(nameof(keys)); } public JsonStringNode? Project(OpenApiSchema schema, string root, string pointer, string location, ProjectionState state) @@ -24,7 +22,7 @@ public JsonStringSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilde if (!string.Equals(schema.ContentMediaType, "application/json", StringComparison.Ordinal)) { - state.Errors.Add(string.Concat(root, _keys.Append(pointer, "contentMediaType")), "contentMediaType must be 'application/json'"); + state.Errors.Add(string.Concat(root, GraphKeyBuilder.Append(pointer, "contentMediaType")), "contentMediaType must be 'application/json'"); return null; } @@ -34,7 +32,7 @@ public JsonStringSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilde return null; } - var inner = _schemaProjector.Project(schema.ContentSchema, root, _keys.Append(pointer, "contentSchema"), state); + var inner = _schemaProjector.Project(schema.ContentSchema, root, GraphKeyBuilder.Append(pointer, "contentSchema"), state); return inner is null ? null : new JsonStringNode diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs index 9f8f3da3..a11c43cd 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/LiteralClassifier.cs @@ -4,21 +4,15 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; -internal sealed class LiteralClassifier +internal static class LiteralClassifier { - public LiteralMarker? FindFirstMarker(IOpenApiSchema schema) + public static LiteralMarker? FindFirstMarker(IOpenApiSchema schema) { ArgumentNullException.ThrowIfNull(schema); - var candidate = schema is OpenApiSchemaReference reference ? reference.Target : schema; - if (candidate is not OpenApiSchema { Properties: not null, Required: not null } objectSchema) + foreach (var property in RequiredProperties(schema)) { - return null; - } - - foreach (var property in objectSchema.Properties) - { - if (!objectSchema.Required.Contains(property.Key) || property.Value is not OpenApiSchema propertySchema) + if (property.Value is not OpenApiSchema propertySchema) { continue; } @@ -33,20 +27,13 @@ internal sealed class LiteralClassifier return null; } - public PrefixMarker? FindFirstPrefixMarker(IOpenApiSchema schema) + public static PrefixMarker? FindFirstPrefixMarker(IOpenApiSchema schema) { ArgumentNullException.ThrowIfNull(schema); - var candidate = schema is OpenApiSchemaReference reference ? reference.Target : schema; - if (candidate is not OpenApiSchema { Properties: not null, Required: not null } objectSchema) - { - return null; - } - - foreach (var property in objectSchema.Properties) + foreach (var property in RequiredProperties(schema)) { - if (!objectSchema.Required.Contains(property.Key) - || property.Value is not OpenApiSchema { Type: JsonSchemaType.String, Enum: null, Const: null, Format: null, Pattern: { } pattern } + if (property.Value is not OpenApiSchema { Type: JsonSchemaType.String, Enum: null, Const: null, Format: null, Pattern: { } pattern } || TemplateLiteralPrefixPolicy.TryDecodePrefix(pattern) is not { } prefix) { continue; @@ -58,7 +45,15 @@ internal sealed class LiteralClassifier return null; } - public LiteralNode? Project(OpenApiSchema schema, string location, IngestionErrorCollector errors) + private static IEnumerable> RequiredProperties(IOpenApiSchema schema) + { + var candidate = schema is OpenApiSchemaReference reference ? reference.Target : schema; + return candidate is OpenApiSchema { Properties: not null, Required: not null } objectSchema + ? objectSchema.Properties.Where(property => objectSchema.Required.Contains(property.Key)) + : []; + } + + public static LiteralNode? Project(OpenApiSchema schema, string location, IngestionErrorCollector errors) { ArgumentNullException.ThrowIfNull(schema); ArgumentException.ThrowIfNullOrWhiteSpace(location); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/MediaTypeProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/MediaTypeProjector.cs index 2c3bdac0..48b8770a 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/MediaTypeProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/MediaTypeProjector.cs @@ -5,6 +5,14 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; internal static class MediaTypeProjector { + internal static MediaTypeProjection? ProjectMediaEntry(KeyValuePair mediaEntry, string root, string pointer, + ProjectionState state, out string mediaPointer) + { + mediaPointer = GraphKeyBuilder.Append(GraphKeyBuilder.Append(pointer, "content"), mediaEntry.Key); + var mediaLocation = string.Concat(root, mediaPointer); + return Project(mediaEntry.Key, mediaEntry.Value, mediaLocation, state.Errors); + } + public static MediaTypeProjection? Project(string raw, IOpenApiMediaType media, string location, IngestionErrorCollector errors) { ArgumentException.ThrowIfNullOrWhiteSpace(raw); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ObjectSchemaProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ObjectSchemaProjector.cs index 4dfc91d9..300031f4 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ObjectSchemaProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ObjectSchemaProjector.cs @@ -5,15 +5,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; internal sealed class ObjectSchemaProjector { - private readonly ErrorStyleClassifier _errorStyleClassifier; - private readonly GraphKeyBuilder _keys; private readonly SchemaProjector _schemaProjector; - public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder keys, ErrorStyleClassifier errorStyleClassifier) + public ObjectSchemaProjector(SchemaProjector schemaProjector) { _schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector)); - _keys = keys ?? throw new ArgumentNullException(nameof(keys)); - _errorStyleClassifier = errorStyleClassifier ?? throw new ArgumentNullException(nameof(errorStyleClassifier)); } public SchemaNode? Project(OpenApiSchema schema, string root, string pointer, string location, ProjectionState state) @@ -36,7 +32,7 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke if (schema.AdditionalProperties is not null) { - return ProjectDictionary(schema, schema.AdditionalProperties, root, _keys.Append(pointer, "additionalProperties"), state); + return ProjectDictionary(schema, schema.AdditionalProperties, root, GraphKeyBuilder.Append(pointer, "additionalProperties"), state); } return new FreeFormObjectNode @@ -69,7 +65,7 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke } var pattern = patterns.Single(); - var valuePointer = _keys.Append(_keys.Append(pointer, "patternProperties"), pattern.Key); + var valuePointer = GraphKeyBuilder.Append(GraphKeyBuilder.Append(pointer, "patternProperties"), pattern.Key); return ProjectDictionary(schema, pattern.Value, root, valuePointer, state); } @@ -93,7 +89,7 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke var missingRequired = schema.Required?.Where(name => !properties.ContainsKey(name)).ToArray() ?? []; if (missingRequired.Length > 0) { - var requiredLocation = string.Concat(root, _keys.Append(pointer, "required")); + var requiredLocation = string.Concat(root, GraphKeyBuilder.Append(pointer, "required")); foreach (var missing in missingRequired) { state.Errors.Add(requiredLocation, $"required property '{missing}' is not declared in properties"); @@ -105,7 +101,7 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke var projectedProperties = new List(properties.Count); foreach (var property in properties) { - var projected = _schemaProjector.Project(property.Value, root, _keys.Append(_keys.Append(pointer, "properties"), property.Key), state); + var projected = _schemaProjector.Project(property.Value, root, GraphKeyBuilder.Append(GraphKeyBuilder.Append(pointer, "properties"), property.Key), state); if (projected is null) { continue; @@ -157,7 +153,7 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke AdditionalPropertiesSchema = additionalPropertiesSchema, LiteralMarkers = markers, PrefixMarkers = prefixMarkers, - ErrorStyle = _errorStyleClassifier.Classify(projectedProperties), + ErrorStyle = ErrorStyleClassifier.Classify(projectedProperties), }; } @@ -166,16 +162,16 @@ public ObjectSchemaProjector(SchemaProjector schemaProjector, GraphKeyBuilder ke /// StructWithRest encoding (); the pointer names where the /// document really says it, so a promoted value keeps an honest graph key. /// - private (IOpenApiSchema? Schema, string? Pointer) ResolveAdditionalProperties(OpenApiSchema schema, string pointer) + private static (IOpenApiSchema? Schema, string? Pointer) ResolveAdditionalProperties(OpenApiSchema schema, string pointer) { if (schema.AdditionalProperties is { } own) { - return (own, _keys.Append(pointer, "additionalProperties")); + return (own, GraphKeyBuilder.Append(pointer, "additionalProperties")); } if (RestWrapperPolicy.TryGetRest(schema) is { } rest) { - return (rest, _keys.Append(_keys.Append(_keys.Append(pointer, "allOf"), "0"), "additionalProperties")); + return (rest, GraphKeyBuilder.Append(GraphKeyBuilder.Append(GraphKeyBuilder.Append(pointer, "allOf"), "0"), "additionalProperties")); } return (null, null); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs index c76b012f..d4a546ae 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/OperationProjector.cs @@ -7,20 +7,17 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; internal sealed class OperationProjector { - private readonly GraphKeyBuilder _keys; private readonly ParameterProjector _parameters; - private readonly PathTemplateValidator _pathTemplates = new(); private readonly RequestBodyProjector _requestBodies; private readonly ResponseProjector _responses; - public OperationProjector(SchemaProjector schemaProjector, GraphKeyBuilder keys) + public OperationProjector(SchemaProjector schemaProjector) { ArgumentNullException.ThrowIfNull(schemaProjector); - _keys = keys ?? throw new ArgumentNullException(nameof(keys)); - _parameters = new ParameterProjector(schemaProjector, keys); - _requestBodies = new RequestBodyProjector(schemaProjector, keys); - _responses = new ResponseProjector(schemaProjector, keys, new EnvelopeClassifier()); + _parameters = new ParameterProjector(schemaProjector); + _requestBodies = new RequestBodyProjector(schemaProjector); + _responses = new ResponseProjector(schemaProjector, new EnvelopeClassifier()); } public IReadOnlyList Project(LoadedSpec loaded, ProjectionState state, @@ -122,9 +119,9 @@ private void ProjectOperation(string path, KeyValuePair Project(OpenApiOperation operation, JsonObject rawOperation, string root, ProjectionState state) @@ -38,7 +36,7 @@ public IReadOnlyList Project(OpenApiOperation operation, JsonObje var identities = new HashSet<(string Name, ParameterLocation Location)>(); for (var index = 0; index < parameters.Count; index++) { - var pointer = _keys.Append(_keys.Append(string.Empty, "parameters"), index.ToString(System.Globalization.CultureInfo.InvariantCulture)); + var pointer = GraphKeyBuilder.Append(GraphKeyBuilder.Append(string.Empty, "parameters"), index.ToString(System.Globalization.CultureInfo.InvariantCulture)); var location = string.Concat(root, pointer); if (rawParameters[index] is not JsonObject rawParameter) { @@ -70,7 +68,7 @@ public IReadOnlyList Project(OpenApiOperation operation, JsonObje continue; } - var schema = _schemaProjector.Project(parameter.Schema, root, _keys.Append(pointer, "schema"), state); + var schema = _schemaProjector.Project(parameter.Schema, root, GraphKeyBuilder.Append(pointer, "schema"), state); if (schema is null || state.Errors.Count != errorCount) { continue; diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PathTemplateValidator.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PathTemplateValidator.cs index a0ab5a71..c2186dd7 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PathTemplateValidator.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PathTemplateValidator.cs @@ -2,11 +2,9 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; -internal sealed class PathTemplateValidator +internal static class PathTemplateValidator { - private readonly StringComparer _comparer = StringComparer.Ordinal; - - public void Check(string path, IReadOnlyList parameters, string location, IngestionErrorCollector errors) + public static void Check(string path, IReadOnlyList parameters, string location, IngestionErrorCollector errors) { ArgumentException.ThrowIfNullOrWhiteSpace(path); ArgumentNullException.ThrowIfNull(parameters); @@ -17,14 +15,14 @@ public void Check(string path, IReadOnlyList parameters, string l var declared = parameters .Where(static parameter => parameter.Location is SpecParameterLocation.Path) .Select(static parameter => parameter.Name) - .ToHashSet(_comparer); + .ToHashSet(StringComparer.Ordinal); - foreach (var token in tokens.Where(token => !declared.Contains(token)).Order(_comparer)) + foreach (var token in tokens.Where(token => !declared.Contains(token)).Order(StringComparer.Ordinal)) { errors.Add(location, $"path token '{token}' is not declared as a path parameter"); } - foreach (var name in declared.Where(name => !tokens.Contains(name)).Order(_comparer)) + foreach (var name in declared.Where(name => !tokens.Contains(name)).Order(StringComparer.Ordinal)) { errors.Add(location, $"path parameter '{name}' is absent from the path template"); } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PrefixItemsAdapter.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PrefixItemsAdapter.cs index 94165285..33496c04 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PrefixItemsAdapter.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/PrefixItemsAdapter.cs @@ -7,13 +7,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; internal sealed class PrefixItemsAdapter { - private readonly GraphKeyBuilder _keys; private readonly SchemaProjector _schemaProjector; - public PrefixItemsAdapter(SchemaProjector schemaProjector, GraphKeyBuilder keys) + public PrefixItemsAdapter(SchemaProjector schemaProjector) { _schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector)); - _keys = keys ?? throw new ArgumentNullException(nameof(keys)); } public SchemaNode? Project(OpenApiSchema schema, string root, string pointer, string location, ProjectionState state) @@ -45,7 +43,7 @@ public PrefixItemsAdapter(SchemaProjector schemaProjector, GraphKeyBuilder keys) var items = new List(prefixItems.Count); for (var index = 0; index < prefixItems.Count; index++) { - var itemPointer = _keys.Append(_keys.Append(pointer, "prefixItems"), index.ToString(System.Globalization.CultureInfo.InvariantCulture)); + var itemPointer = GraphKeyBuilder.Append(GraphKeyBuilder.Append(pointer, "prefixItems"), index.ToString(System.Globalization.CultureInfo.InvariantCulture)); var itemLocation = string.Concat(root, itemPointer); if (prefixItems[index] is not { } rawItem) { @@ -115,24 +113,16 @@ public PrefixItemsAdapter(SchemaProjector schemaProjector, GraphKeyBuilder keys) return null; } - var item = _schemaProjector.Project(schema.Items, root, _keys.Append(pointer, "items"), state); - return item is null - ? null - : new ArrayNode - { - Description = schema.Description, - Format = schema.Format, - Item = item, - }; + return _schemaProjector.ProjectArrayItems(schema, itemsReference, root, pointer, state); } - private bool ValidateArity(OpenApiSchema schema, int arity, string root, string pointer, IngestionErrorCollector errors) + private static bool ValidateArity(OpenApiSchema schema, int arity, string root, string pointer, IngestionErrorCollector errors) { var isValid = true; var arityText = arity.ToString(System.Globalization.CultureInfo.InvariantCulture); if (schema.MinItems != arity) { - errors.Add(string.Concat(root, _keys.Append(pointer, "minItems")), $"minItems must equal tuple arity {arityText}"); + errors.Add(string.Concat(root, GraphKeyBuilder.Append(pointer, "minItems")), $"minItems must equal tuple arity {arityText}"); isValid = false; } @@ -141,7 +131,7 @@ private bool ValidateArity(OpenApiSchema schema, int arity, string root, string return isValid; } - errors.Add(string.Concat(root, _keys.Append(pointer, "maxItems")), $"maxItems must equal tuple arity {arityText}"); + errors.Add(string.Concat(root, GraphKeyBuilder.Append(pointer, "maxItems")), $"maxItems must equal tuple arity {arityText}"); return false; } diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/RequestBodyProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/RequestBodyProjector.cs index 58649020..832cad5d 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/RequestBodyProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/RequestBodyProjector.cs @@ -5,13 +5,11 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; internal sealed class RequestBodyProjector { - private readonly GraphKeyBuilder _keys; private readonly SchemaProjector _schemaProjector; - public RequestBodyProjector(SchemaProjector schemaProjector, GraphKeyBuilder keys) + public RequestBodyProjector(SchemaProjector schemaProjector) { _schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector)); - _keys = keys ?? throw new ArgumentNullException(nameof(keys)); } public SpecRequestBody? Project(IOpenApiRequestBody? requestBody, string root, ProjectionState state) @@ -24,7 +22,7 @@ public RequestBodyProjector(SchemaProjector schemaProjector, GraphKeyBuilder key return null; } - var pointer = _keys.Append(string.Empty, "requestBody"); + var pointer = GraphKeyBuilder.Append(string.Empty, "requestBody"); var location = string.Concat(root, pointer); var errorCount = state.Errors.Count; if (requestBody is not OpenApiRequestBody) @@ -41,9 +39,7 @@ public RequestBodyProjector(SchemaProjector schemaProjector, GraphKeyBuilder key } var mediaEntry = content.Single(); - var mediaPointer = _keys.Append(_keys.Append(pointer, "content"), mediaEntry.Key); - var mediaLocation = string.Concat(root, mediaPointer); - var media = MediaTypeProjector.Project(mediaEntry.Key, mediaEntry.Value, mediaLocation, state.Errors); + var media = MediaTypeProjector.ProjectMediaEntry(mediaEntry, root, pointer, state, out var mediaPointer); if (media is null) { return null; @@ -51,11 +47,11 @@ public RequestBodyProjector(SchemaProjector schemaProjector, GraphKeyBuilder key if (mediaEntry.Value.Schema is null) { - state.Errors.Add(mediaLocation, "request body media schema is required"); + state.Errors.Add(string.Concat(root, mediaPointer), "request body media schema is required"); return null; } - var schema = _schemaProjector.Project(mediaEntry.Value.Schema, root, _keys.Append(mediaPointer, "schema"), state); + var schema = _schemaProjector.Project(mediaEntry.Value.Schema, root, GraphKeyBuilder.Append(mediaPointer, "schema"), state); return schema is null || state.Errors.Count != errorCount ? null : new SpecRequestBody diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ResponseProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ResponseProjector.cs index 6dc507c0..ad74bd70 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ResponseProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/ResponseProjector.cs @@ -8,15 +8,13 @@ internal sealed class ResponseProjector { private readonly EnvelopeClassifier _envelopes; private readonly EffectStreamProjector _effectStreams; - private readonly GraphKeyBuilder _keys; private readonly SchemaProjector _schemaProjector; - public ResponseProjector(SchemaProjector schemaProjector, GraphKeyBuilder keys, EnvelopeClassifier envelopes) + public ResponseProjector(SchemaProjector schemaProjector, EnvelopeClassifier envelopes) { _schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector)); - _keys = keys ?? throw new ArgumentNullException(nameof(keys)); _envelopes = envelopes ?? throw new ArgumentNullException(nameof(envelopes)); - _effectStreams = new EffectStreamProjector(_schemaProjector, _keys); + _effectStreams = new EffectStreamProjector(_schemaProjector); } public IReadOnlyList Project(OpenApiResponses? responses, string root, ProjectionState state) @@ -33,7 +31,7 @@ public IReadOnlyList Project(OpenApiResponses? responses, string r var statusCodes = new HashSet(); foreach (var (key, response) in responses) { - var keyLocation = string.Concat(root, _keys.Append(_keys.Append(string.Empty, "responses"), key)); + var keyLocation = string.Concat(root, GraphKeyBuilder.Append(GraphKeyBuilder.Append(string.Empty, "responses"), key)); if (!int.TryParse(key, NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode)) { state.Errors.Add(keyLocation, $"response key '{key}' must be an integer status code"); @@ -65,7 +63,7 @@ public IReadOnlyList Project(OpenApiResponses? responses, string r private SpecResponse? ProjectResponse(int statusCode, IOpenApiResponse response, string root, ProjectionState state) { var status = statusCode.ToString(CultureInfo.InvariantCulture); - var pointer = _keys.Append(_keys.Append(string.Empty, "responses"), status); + var pointer = GraphKeyBuilder.Append(GraphKeyBuilder.Append(string.Empty, "responses"), status); var location = string.Concat(root, pointer); var errorCount = state.Errors.Count; if (response is not OpenApiResponse) @@ -98,9 +96,7 @@ public IReadOnlyList Project(OpenApiResponses? responses, string r } var mediaEntry = content.Single(); - var mediaPointer = _keys.Append(_keys.Append(pointer, "content"), mediaEntry.Key); - var mediaLocation = string.Concat(root, mediaPointer); - var media = MediaTypeProjector.Project(mediaEntry.Key, mediaEntry.Value, mediaLocation, state.Errors); + var media = MediaTypeProjector.ProjectMediaEntry(mediaEntry, root, pointer, state, out var mediaPointer); if (media is null) { return null; @@ -109,7 +105,7 @@ public IReadOnlyList Project(OpenApiResponses? responses, string r SchemaNode? schema = null; if (mediaEntry.Value.Schema is not null) { - schema = _schemaProjector.Project(mediaEntry.Value.Schema, root, _keys.Append(mediaPointer, "schema"), state); + schema = _schemaProjector.Project(mediaEntry.Value.Schema, root, GraphKeyBuilder.Append(mediaPointer, "schema"), state); } var effectStream = _effectStreams.Project(media.EffectStream, root, mediaPointer, state); diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs index 07176e67..fcd4a876 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/SchemaProjector.cs @@ -6,25 +6,19 @@ namespace OpenCode.Sdk.Tools.Generator.Ingestion.Projection; internal sealed class SchemaProjector { - private readonly GraphKeyBuilder _keys; - private readonly EnumSchemaProjector _enumProjector; private readonly JsonStringSchemaProjector _jsonStringProjector; - private readonly LiteralClassifier _literalClassifier; private readonly ObjectSchemaProjector _objectProjector; private readonly PrefixItemsAdapter _prefixItemsAdapter; private readonly UnionNormalizer _unionNormalizer; private readonly SchemaShapeClassifier _shapeClassifier; - public SchemaProjector(GraphKeyBuilder keys) + public SchemaProjector() { - _keys = keys ?? throw new ArgumentNullException(nameof(keys)); _shapeClassifier = new SchemaShapeClassifier(); - _enumProjector = new EnumSchemaProjector(); - _literalClassifier = new LiteralClassifier(); - _jsonStringProjector = new JsonStringSchemaProjector(this, _keys); - _objectProjector = new ObjectSchemaProjector(this, _keys, new ErrorStyleClassifier()); - _unionNormalizer = new UnionNormalizer(this, _keys, _shapeClassifier, _literalClassifier); - _prefixItemsAdapter = new PrefixItemsAdapter(this, _keys); + _jsonStringProjector = new JsonStringSchemaProjector(this); + _objectProjector = new ObjectSchemaProjector(this); + _unionNormalizer = new UnionNormalizer(this, _shapeClassifier); + _prefixItemsAdapter = new PrefixItemsAdapter(this); } public SchemaNode? Project(IOpenApiSchema schema, string root, string pointer, ProjectionState state) @@ -78,8 +72,8 @@ public SchemaProjector(GraphKeyBuilder keys) (CoreSchemaShape.Primitive, JsonSchemaType.Boolean) => CreatePrimitive(schema, PrimitiveKind.Boolean), (CoreSchemaShape.Array, _) => ProjectArray(schema, root, pointer, state), (CoreSchemaShape.Object, _) => _objectProjector.Project(schema, root, pointer, location, state), - (CoreSchemaShape.Enum, _) => _enumProjector.Project(schema, location, state.Errors), - (CoreSchemaShape.Literal, _) => _literalClassifier.Project(schema, location, state.Errors), + (CoreSchemaShape.Enum, _) => EnumSchemaProjector.Project(schema, location, state.Errors), + (CoreSchemaShape.Literal, _) => LiteralClassifier.Project(schema, location, state.Errors), (CoreSchemaShape.Union, _) => _unionNormalizer.Project(schema, root, pointer, location, state), (CoreSchemaShape.Null, _) => RefuseStandaloneNull(location, state), (CoreSchemaShape.Tuple, _) => _prefixItemsAdapter.Project(schema, root, pointer, location, state), @@ -120,7 +114,12 @@ private static SchemaNode ProjectNegation(OpenApiSchema schema) => }; } - var item = Project(schema.Items, root, _keys.Append(pointer, "items"), state); + return ProjectArrayItems(schema, schema.Items, root, pointer, state); + } + + internal ArrayNode? ProjectArrayItems(OpenApiSchema schema, IOpenApiSchema items, string root, string pointer, ProjectionState state) + { + var item = Project(items, root, GraphKeyBuilder.Append(pointer, "items"), state); return item is null ? null : new ArrayNode diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs index 53118720..16c0bb35 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/Projection/UnionNormalizer.cs @@ -14,18 +14,13 @@ internal sealed class UnionNormalizer "-Infinity", }; - private readonly GraphKeyBuilder _keys; - private readonly LiteralClassifier _literalClassifier; private readonly SchemaProjector _schemaProjector; private readonly SchemaShapeClassifier _shapeClassifier; - public UnionNormalizer(SchemaProjector schemaProjector, GraphKeyBuilder keys, SchemaShapeClassifier shapeClassifier, - LiteralClassifier literalClassifier) + public UnionNormalizer(SchemaProjector schemaProjector, SchemaShapeClassifier shapeClassifier) { _schemaProjector = schemaProjector ?? throw new ArgumentNullException(nameof(schemaProjector)); - _keys = keys ?? throw new ArgumentNullException(nameof(keys)); _shapeClassifier = shapeClassifier ?? throw new ArgumentNullException(nameof(shapeClassifier)); - _literalClassifier = literalClassifier ?? throw new ArgumentNullException(nameof(literalClassifier)); } public SchemaNode? Project(OpenApiSchema schema, string root, string pointer, string location, ProjectionState state) @@ -107,7 +102,7 @@ public UnionNormalizer(SchemaProjector schemaProjector, GraphKeyBuilder keys, Sc }; } - private bool ValidateSpecialNumberBranches(IList branches, string root, string pointer, string keyword, ProjectionState state) + private static bool ValidateSpecialNumberBranches(IList branches, string root, string pointer, string keyword, ProjectionState state) { var errorCount = state.Errors.Count; for (var index = 0; index < branches.Count; index++) @@ -117,7 +112,7 @@ private bool ValidateSpecialNumberBranches(IList branches, strin continue; } - var branchPointer = _keys.UnionBranch(pointer, keyword, index, marker: null); + var branchPointer = GraphKeyBuilder.UnionBranch(pointer, keyword, index, marker: null); SchemaWallPolicy.Check(branch, string.Concat(root, branchPointer), state.Errors); if (branch.AnyOf is not { Count: > 0 } nested) { @@ -131,7 +126,7 @@ private bool ValidateSpecialNumberBranches(IList branches, strin continue; } - var innerPointer = _keys.UnionBranch(branchPointer, "anyOf", innerIndex, marker: null); + var innerPointer = GraphKeyBuilder.UnionBranch(branchPointer, "anyOf", innerIndex, marker: null); SchemaWallPolicy.Check(inner, string.Concat(root, innerPointer), state.Errors); } } @@ -147,10 +142,10 @@ private bool ValidateSpecialNumberBranches(IList branches, strin for (var index = 0; index < sourceBranches.Count; index++) { var source = sourceBranches[index]; - var marker = _literalClassifier.FindFirstMarker(source); - var prefixMarker = marker is null ? _literalClassifier.FindFirstPrefixMarker(source) : null; - var isMarkedUnionReference = marker is null && IsMarkedUnionReference(source); - var branchPointer = _keys.UnionBranch(pointer, keyword, index, marker); + var marker = LiteralClassifier.FindFirstMarker(source); + var prefixMarker = marker is null ? LiteralClassifier.FindFirstPrefixMarker(source) : null; + var isMarkedUnionReference = marker is null && ReferencesMarkedUnion(source); + var branchPointer = GraphKeyBuilder.UnionBranch(pointer, keyword, index, marker); var branchLocation = string.Concat(root, branchPointer); if (source is OpenApiSchema { Type: JsonSchemaType.Null } concrete) { @@ -187,11 +182,11 @@ private bool ValidateSpecialNumberBranches(IList branches, strin /// of the referenced union is itself a marked object. The Binder validates that the /// nested variants contribute one uniform outer tag; this records only markedness. /// - private bool IsMarkedUnionReference(IOpenApiSchema source) => + private static bool ReferencesMarkedUnion(IOpenApiSchema source) => source is OpenApiSchemaReference { Target: OpenApiSchema target } && (IsPureAnyOfBranch(target) || IsPureOneOfBranch(target)) && (target.AnyOf is { Count: > 1 } ? target.AnyOf : target.OneOf) is { Count: > 1 } nested - && nested.All(branch => _literalClassifier.FindFirstMarker(branch) is not null); + && nested.All(static branch => LiteralClassifier.FindFirstMarker(branch) is not null); /// The oneOf twin of ; a nested marked union may use either keyword exclusively. private static bool IsPureOneOfBranch(OpenApiSchema schema) => diff --git a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs index 515b573d..aec4ef45 100644 --- a/tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs +++ b/tools/OpenCode.Sdk.Tools/Generator/Ingestion/SpecIngestion.cs @@ -31,17 +31,16 @@ public async Task IngestAsync(string specPath, IReadOnlyDictionary var errors = new IngestionErrorCollector(); var loaded = await _reader.LoadAsync(specPath, errors, cancellationToken).ConfigureAwait(false); - var keys = new GraphKeyBuilder(); - var schemaProjector = new SchemaProjector(keys); + var schemaProjector = new SchemaProjector(); var state = new ProjectionState(errors, loaded.Document); - var operationProjector = new OperationProjector(schemaProjector, keys); + var operationProjector = new OperationProjector(schemaProjector); var operations = operationProjector.Project(loaded, state, operationIdentities); if (loaded.Document.Components?.Schemas is { } namedSchemas) { foreach (var (name, schema) in namedSchemas) { - _ = schemaProjector.Project(schema, keys.Root(name), string.Empty, state); + _ = schemaProjector.Project(schema, GraphKeyBuilder.Root(name), string.Empty, state); } } diff --git a/tools/OpenCode.Sdk.Tools/ToolApp.cs b/tools/OpenCode.Sdk.Tools/ToolApp.cs index a6d77d59..049a06da 100644 --- a/tools/OpenCode.Sdk.Tools/ToolApp.cs +++ b/tools/OpenCode.Sdk.Tools/ToolApp.cs @@ -39,18 +39,6 @@ internal static ServiceCollection CreateServices(Action? ove services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/tools/jscpd/tests.baseline.json b/tools/jscpd/tests.baseline.json index af466b8a..86ca1a8e 100644 --- a/tools/jscpd/tests.baseline.json +++ b/tools/jscpd/tests.baseline.json @@ -402,7 +402,6 @@ "93b9870af2c85e3c": 1, "93c7ffbef646d5bd": 1, "943a5cceaaa0085d": 1, - "94b8d278f96ea4ca": 1, "94c3b038871234a4": 1, "950d14b87d7527d0": 1, "9676160daa1042e5": 1, @@ -473,7 +472,6 @@ "b11e207acaec82c8": 1, "b17bbc10f934e336": 1, "b26ec09aa9c7d788": 1, - "b4bc3f0719b7f773": 1, "b4f1d187378630d2": 1, "b516fc87f57ff552": 1, "b550ded1432f76f0": 1, @@ -571,6 +569,7 @@ "e4bc846ba130e160": 1, "e4c2843bfae3d310": 1, "e539609f53bb27d3": 1, + "e59cfed574cd3816": 1, "e6460fb624a61900": 1, "e73051f32d6330a4": 1, "e7e34567eac6357c": 1, diff --git a/tools/jscpd/tools.baseline.json b/tools/jscpd/tools.baseline.json index 83de5ea6..51449c89 100644 --- a/tools/jscpd/tools.baseline.json +++ b/tools/jscpd/tools.baseline.json @@ -6,7 +6,6 @@ "08efb9ba7f389078": 1, "09178bc14755a5ba": 1, "0c39eb63b12c36f1": 1, - "1ae69713b409cdba": 1, "1b90fd3dac3d53d3": 1, "1c3a98975a10f3e5": 1, "1e519d77b533f929": 1, @@ -31,6 +30,7 @@ "674d96257eb7a166": 1, "6b8f7d70c8efaf60": 1, "6f10da095e7bc46f": 1, + "73357f998d7f20ac": 1, "745feb6ba3d09950": 1, "7ab125456f2f419a": 1, "841b71829b6dc2e9": 1, @@ -38,7 +38,6 @@ "8d45596fbec558f6": 1, "8f2d1ca14e547f58": 1, "91b61792ecefb203": 1, - "9758b5580ab6cf7f": 1, "a2149d8e66c69ca6": 1, "a3a0feec942b1660": 1, "a3cf47c6f8c22f4b": 1, @@ -52,8 +51,7 @@ "b6a0f64aff54043f": 1, "bac17552a4121dfc": 1, "bbc5f9f6a972d84d": 1, - "bc70ef6e41874dc1": 1, - "c0ebf126475a0728": 1, + "bcccdc53396e05d6": 1, "c10ae1c8944b85b5": 1, "c79da7da04fbfbb0": 1, "d440bcf891c20c44": 1, @@ -64,7 +62,6 @@ "eecbb27cc2bdc7c3": 1, "f58baafe927d049c": 1, "f61574ba59a41a6b": 1, - "fec407757b33ea72": 1, "fef60269564f795a": 1 } }