feat: resolve $dynamicRef against the dynamic scope#504
Conversation
…ferencing locallyDereferenced() / dereferenced(in:) now resolve $dynamicRef against the dynamic scope (outermost $dynamicAnchor wins, per JSON Schema 2020-12). Non-recursive targets are inlined; recursive or unresolvable dynamic refs throw, consistent with static $ref cycles and the Dereferenced... invariant (a DereferencedJSONSchema must not contain references). Implements the 'dynamic-scope-aware dereferencing' item from mattpolzin#359. Edits the same migration-guide section as mattpolzin#503 (external deref); reconcile on merge. Part of mattpolzin#359.
cbba88a to
26b74d9
Compare
mattpolzin
left a comment
There was a problem hiding this comment.
This looks mostly ready to go. Thanks! A few nits with comments and migration guide stuff but just one request for refactoring.
| /// `dynamicScope` maps a `$dynamicAnchor` name to the **outermost** schema | ||
| /// resource bearing that anchor on the current resolution path (first-wins | ||
| /// insertion). A `$dynamicRef` is resolved against this scope per JSON | ||
| /// Schema 2020-12 dynamic-scope rules: the outermost matching anchor wins. | ||
| /// Non-recursive targets are inlined; recursive or unresolvable dynamic | ||
| /// references throw (a `DereferencedJSONSchema` must not contain references). |
There was a problem hiding this comment.
| /// `dynamicScope` maps a `$dynamicAnchor` name to the **outermost** schema | |
| /// resource bearing that anchor on the current resolution path (first-wins | |
| /// insertion). A `$dynamicRef` is resolved against this scope per JSON | |
| /// Schema 2020-12 dynamic-scope rules: the outermost matching anchor wins. | |
| /// Non-recursive targets are inlined; recursive or unresolvable dynamic | |
| /// references throw (a `DereferencedJSONSchema` must not contain references). | |
| /// `dynamicScope` maps a `$dynamicAnchor` name to the **outermost** schema | |
| /// resource bearing that anchor on the current resolution path per JSON | |
| /// Schema 2020-12 dynamic-scope rules. | |
| /// Non-recursive targets are inlined; recursive or unresolvable dynamic | |
| /// references throw. |
This comment is technically accurate but a bit repetitive and or verbose for my taste.
| case .reference(let reference, let context): | ||
| var dereferenced = try reference | ||
| ._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil) | ||
| // Thread the dynamic scope across the `$ref` boundary (outermost-wins). |
There was a problem hiding this comment.
| // Thread the dynamic scope across the `$ref` boundary (outermost-wins). |
I prefer to add comments when they will tell the next reader more about what's going on. While this comment does describe the following lines, it doesn't (to me) say anything that the code itself doesn't also say.
On the other hand, if the code did something that had been misunderstood initially then a comment explaining why the code does what it does instead of what the code does would be generally appreciated.
There was a problem hiding this comment.
I changed the comment to only state the non-obvious things.
| // Recursive dynamic reference: cannot inline without retaining a | ||
| // reference, so fail -- consistent with static `$ref` cycles. |
There was a problem hiding this comment.
| // Recursive dynamic reference: cannot inline without retaining a | |
| // reference, so fail -- consistent with static `$ref` cycles. |
See other comments for my thoughts on code comments.
There was a problem hiding this comment.
I changed the comment to only state the non-obvious things.
| `locallyDereferenced()` and `JSONSchema.dereferenced(in:)` now resolve | ||
| `$dynamicRef` against the dynamic scope (the outermost in-scope `$dynamicAnchor` | ||
| wins, per JSON Schema 2020-12). Non-recursive targets are inlined. Recursive | ||
| or unresolvable `$dynamicRef`s **throw** — a `DereferencedJSONSchema` must not | ||
| contain references, so a dynamic ref that cannot be fully inlined fails the | ||
| same way a recursive static `$ref` does (`ReferenceCycleError`). |
There was a problem hiding this comment.
| `locallyDereferenced()` and `JSONSchema.dereferenced(in:)` now resolve | |
| `$dynamicRef` against the dynamic scope (the outermost in-scope `$dynamicAnchor` | |
| wins, per JSON Schema 2020-12). Non-recursive targets are inlined. Recursive | |
| or unresolvable `$dynamicRef`s **throw** — a `DereferencedJSONSchema` must not | |
| contain references, so a dynamic ref that cannot be fully inlined fails the | |
| same way a recursive static `$ref` does (`ReferenceCycleError`). | |
| `locallyDereferenced()` and `JSONSchema.dereferenced(in:)` now resolve | |
| `$dynamicRef` against the dynamic scope. |
The migration guide should describe what needs to be changed or considered to adopt a new OpenAPIKit version. It should not describe pre-existing behavior or how the OpenAPI or JSON Schema specifications work.
There was a problem hiding this comment.
Simplified the migration-guide wording to keep it adoption-focused.
| throw OpenAPI.Components.ReferenceCycleError(ref: reference.absoluteString) | ||
| } | ||
| var dereferenced = try components | ||
| .lookup(reference) |
There was a problem hiding this comment.
Actually, the way this skips over the code that dereferences a JSONReference but reproduces all of that code here feels off. I think it mostly makes the LocallyDereferenceable implementation in JSONReference irrelevant which may be ok but for the fact that it is still there and still public interface.
For now, I'd like to refactor things so that there's an additional helper next to JSONReference's LocallyDereferenceable helper allowing that code to be used in both ways instead of copied into this location. Does that make sense? I can describe this more completely if helpful.
There was a problem hiding this comment.
Makes sense. I refactored JSONReference._dereferenced(in:following:) through an internal continuation helper so cycle detection and lookup stay in one place, and JSONSchema can thread dynamic scope across the $ref boundary without duplicating that logic.
Extract JSONReference._dereferenced(in:following:then:) so JSONSchema's .dynamicReference path can thread dynamic scope through the shared cycle- detection + lookup logic instead of duplicating it. Addresses the one non-nit request from review of mattpolzin#504. Also applies comment/migration-guide nits from the same review.
|
@mattpolzin Thanks for the review. I addressed the refactor request by sharing the |
Part of #359.
Implements the "dynamic-scope-aware dereferencing" item from the #359 task list.
locallyDereferenced()/JSONSchema.dereferenced(in:)now resolve$dynamicRefagainst the dynamic scope (the outermost in-scope$dynamicAnchorwins, per JSON Schema 2020-12 §7.7), instead of throwing on every$dynamicRef.Behavior
$dynamicRef(e.g. the JSON Schema "generics" pattern —$dynamicRef "#T"→ a leaf$defsentry): the target is inlined.$dynamicRef: throws (ReferenceCycleError/GenericError), consistent with how recursive static$refalready fails and with theDereferenced...invariant that aDereferencedJSONSchemacontains no references.$dynamicAnchor(and$defs-declared anchors, first-wins/outermost-wins) and threaded through subschemas and across$refboundaries.Notes
swift-openapi-generator) are unaffected —.dynamicReferenceonJSONSchemais unchanged; onlylocallyDereferenced()behavior changes (strictly an improvement: previously it threw on all$dynamicRef; now only recursive/unresolvable ones).$dynamicRef#503 (external deref); trivial doc reconcile on merge.Testing
$ref,$ref→$dynamicRefname propagation, optional$refproperty..referencerefactor is behavior-preserving).llvm-cov-verified: 0 uncovered regions in the scope-aware_dereferenced+ theArrayContext/ObjectContextthreading).This PR was drafted with assistance from AI tooling. The submitter has reviewed and validated the contents prior to submission.