Reachable fragment bundling and deep rebasing for unselected $ref nodes - #5
Merged
Conversation
…entrypoint, without loading unrelated refs from other components * Change so an unselected map is preserved but no longer terminates traversal: its sibling values are walked
$ref Nodes$ref nodes
Owner
Author
|
This PR helps accomplish this xinz/oasis#3 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposal to JSONSchex: Reachable fragment bundling and deep rebasing for unselected
$refnodesjsonschex0.8.2or laterSummary
Oasis uses two JSONSchex APIs to maintain a strict ownership boundary:
JSONSchex.Ref.resolve_selected/2resolves structural OpenAPI Reference Objects while leaving Schema Object$refs available for JSON Schema processing.JSONSchex.bundle_fragment/2turns one schema entrypoint inside an OpenAPI document into a standalone schema for generated code.JSONSchex
0.8.1fixed the original external-resource base loss for a direct unselected nested$ref. This proposal addressed two related graph-traversal gaps:bundle_fragment/2scanned the entire merged containing document for external refs, so unreachable refs could trigger unrelated loading failures.resolve_selected/2rebased an unselected node's direct$ref, but stopped walking that node, so refs inside valid$refsiblings and deeper descendants kept the wrong resource base.Both fixes were implemented in JSONSchex because they require reference-graph reachability and resource-base semantics. Oasis does not need to duplicate that logic.
Implementation Outcome
The public function signatures and existing options remain unchanged.
JSONSchex.bundle_fragment/2The final implementation:
$ref/$dynamicRefedges$idresources, and external fragments$idmetadata in opaque containing-document data from overriding authoritative schema resources$defsentries and chooses collision-safe internaljsonschex_*mount keysinvalid_defsandambiguous_anchorerrors instead of raising, overwriting caller data, or choosing duplicate fallback anchors by map iteration orderThe bundle may still retain complete containing or external resource documents. Reachability controls loader invocation and failure behavior; it does not guarantee byte-minimal output.
JSONSchex.Ref.resolve_selected/2The final implementation:
$refnode:selectfor descendant$refnodes beneath an unselected ref map:base_uri, canonical root$id, ancestor$idvalues along pointer paths, and nested resource boundariesSelectors with side effects should account for callback invocations at descendant paths that JSONSchex
0.8.1skipped.Shared internal support
The implementation introduced:
JSONSchex.ResourceContextfor decoded JSON Pointer traversal, array indexes, containing-resource discovery, and inherited/effective base calculationJSONSchex.SchemaTraversalfor centralized active, metadata, and scope schema-child traversalJSONSchex.Compiler.Fragment.Bundle,JSONSchex.Ref, andJSONSchex.ScopeScannernow share these mechanics instead of maintaining independent pointer walkers and schema-keyword lists.Problem 1: Fragment Bundling Loaded Unreachable External Refs
JSONSchex 0.8.1 behavior
Given a containing OpenAPI-like document:
and this call:
#/schemais self-contained and cannot reachcomponents.schemas.Unused. Nevertheless, JSONSchex0.8.1merges the full containing document with the entry schema and recursively collects external refs from the merged map. It therefore tries to load/api/missing.yaml.Generation fails because of an unrelated, unreachable schema.
Why this is incorrect for a fragment API
The purpose of a fragment API is to compile or bundle the schema graph reachable from one entrypoint while retaining the containing document as reference context.
Containing-document context does not imply that every
$ref-shaped value in the document is reachable. This distinction is especially important for OpenAPI documents, which may contain:$ref-shaped mapsAn unreachable reference should not require a loader and should not make the selected entrypoint fail.
Implemented behavior
bundle_fragment/2now discovers and loads resources by traversing the schema graph reachable from:entry.It continues to support:
$idresource boundariesIt does not load an external resource merely because an unrelated subtree of the containing document contains a
$ref.Problem 2: Unselected
$refSiblings Were Not TraversedJSONSchex 0.8.1 behavior
Consider an OpenAPI Parameter Object selected from an external document. Its Schema Object contains a direct
$refand a valid Draft 2020-12 sibling:When the Parameter Object is selected and inlined from
/api/common.yaml, the Schema Object itself remains unselected. JSONSchex0.8.1produces behavior equivalent to:The direct
$refis correctly rebased, but the sibling descendant is not. Once this object is inlined into/api/openapi.yaml, theallOfref resolves against the wrong resource.JSONSchex 0.8.1 root cause
The selected-ref walker treated any map containing
$refas a terminal reference node:$refand return immediatelyFor JSON Schema Draft 2019-09 and 2020-12,
$refsiblings are meaningful. Returning immediately skips valid sibling schemas and any refs nested beneath them.Implemented behavior
When a
$refnode is unselected, JSONSchex now:$ref; do not resolve it.$refwhen the node originated in an external selected target.This is implemented as generic recursive reference preservation, not as an Oasis-specific list of JSON Schema keywords such as
allOf,properties, or$defs.Why These Fixes Belong in JSONSchex
Oasis knows which OpenAPI Reference Object locations it needs before generation, but it should not implement:
$idand resource-base propagationImplementing either workaround in Oasis would make it maintain a partial JSON Schema engine and would violate the intended boundary:
Implemented Semantics
JSONSchex.bundle_fragment/2Without changing the public API, bundling now operates from the selected entrypoint:
:entryin the containing document.The output does not need to be minimal byte-for-byte, but loader invocation and failure behavior must be reachability-based.
JSONSchex.Ref.resolve_selected/2For an unselected map containing
$ref, JSONSchex now:The selection callback continues to control dereferencing, while origin preservation applies independently to all unselected refs inside a selected external target.
Implemented Test Coverage
The original five acceptance examples below were integrated into the project test suite using in-memory loaders. The final suite also covers local-to-external chains, inactive definitions and opaque metadata, all supported schema-bearing keywords, external fragments, recursive and dynamic refs, dynamic-scope overrides, nested
$idresources, loader aliases, canonical IDs, generated-definition collisions, ambiguous anchors, selector callback paths, and shared resource-context traversal.Original acceptance examples
Test 1: Unreachable external refs are not loaded
Test 2: Reachable external refs are still loaded
Test 3: Rebase direct and sibling-descendant refs
Test 4: Deep rebasing does not eagerly resolve refs
Test 5: Loader wrapper
:base_uriremains authoritative for deep rebasingAcceptance Criteria
Reachable fragment bundling
bundle_fragment/2loads only external resources reachable from:entry.:base_uriremains authoritative.invalid_defsandambiguous_anchorerrors.Deep unselected-ref rebasing
$refof an unselected node is rebased when necessary.:base_uriremains authoritative for all rebased descendants.$idresource boundaries remain authoritative.Final Differences and Deliberate Trade-offs
$defsmount names are internal implementation details. They use thejsonschex_*prefix and receive deterministic numeric suffixes when caller-owned keys collide. Callers must not construct refs to these generated paths; resource identity remains based on$idand anchors.ambiguous_anchorwithout invoking the loader.Validation Result
Oasis Follow-up
After JSONSchex releases these fixes, Oasis should:
$ref-sibling cases.Oasis.Spec.OpenAPIRefResolverlimited to OpenAPI selection policy.