fix: resolve recursive $ref in AsyncAPI-extracted fragments - #455
Open
jonaslagoni wants to merge 1 commit into
Open
fix: resolve recursive $ref in AsyncAPI-extracted fragments#455jonaslagoni wants to merge 1 commit into
jonaslagoni wants to merge 1 commit into
Conversation
@asyncapi/parser leaves a circular $ref as a literal `#/components/schemas/X` pointer. The AsyncAPI extraction sites re-root a single payload/headers/parameters schema as a standalone draft-07 document with no `components` section, so the pointer dangled and Modelina's dereference step failed with "Could not dereference $ref in input". Each surviving pointer is now resolved against the fragment itself: to the local JSON pointer of the copy already inlined there (`#` for a single-message root, `#/oneOf/<i>` for a union member, `#/properties/<id>` for a v2 parameter), or, when the component is not inlined, into a `definitions` block with `$id` stamped and transitive targets collected. Both halves are needed — hoisting an inlined component emits a duplicate class, and omitting `$id` names a hoisted model after its use site. Fragments with no surviving pointer are returned unchanged by identity, so documents that generate today are unaffected. Closes #448 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Closes #448 (split out of #447).
The problem
An AsyncAPI document containing a recursive schema fails generation outright:
Three design points collide:
@asyncapi/parserinlines every non-circular$ref, but deliberately leaves a cycle as a literal{"$ref": "#/components/schemas/Node"}.componentssection the pointer aims at is not carried along.#/components/schemas/Nodeagainst something that has nocomponentskey, and throws.Nothing needs to change in Modelina —
circular: trueis already set in its dereference options. The pointer target is genuinely absent from the fragment.The fix
A new pure helper,
src/codegen/inputs/asyncapi/refs.ts, applied at each extraction site immediately after the fragment is assembled. Every surviving#/components/schemas/Xis resolved one of two ways:Xis already inlined in this fragment → rewrite the pointer to that copy's local JSON pointer (#for a single-message root,#/oneOf/<i>for a union member,#/properties/<id>for a v2 parameter).Xis not inlined anywhere → rewrite to#/definitions/X, convert the component, stamp$id: X, hoist it intodefinitions, and recurse so transitive targets come along.Both halves are load-bearing, and this was determined empirically rather than by preference:
definitionsonlyNodeandNodeChildrenItem#Node { children?: Node[] }Abafter the use sitedefinitions+$idA,Bdefinitions#/oneOf/NDuplicate class names in one output file are broken code, not cosmetic noise — hence the two-way split.
The walk is copy-on-write. The extraction sites rebuild only the top level of a fragment; nested nodes stay shared with the parser's document tree, so an in-place rewrite would corrupt every other extraction site and
filter.ts. There is a dedicated regression test for this.Sites covered
payloads.tssingle message{<root x-parser-schema-id> → '#'}payloads.tsoneOfunion{<member id> → '#/oneOf/<i>'}headers.tssingle message / channel unionparameters.ts(AsyncAPI v2){<param schema id> → '#/properties/<id>'}The plan left headers and parameters as open questions. Both were checked against the parsed model and both reproduce the failure, so both are wired rather than documented as non-reproducing. v3 Parameter Objects carry no
schema, so only v2 documents can reach that path.Not a breaking change
The transform is gated on the presence of a surviving
#/components/schemas/pointer; without one the fragment is returned by identity. Documents that hit this bug currently produce no output at all, so there is no working output to break.Verified rather than asserted:
git diff --numstatover every snapshot file shows 0 deleted lines — only new snapshots from the new fixtures, no pre-existing snapshot modified.Verification
npm testnpm run test:blackboxnpm run runtime:typescript(recursive spec)npm run prepare:prchildren?: NodeMessage[]on a single modelCoverage added across all three tiers:
refs.spec.ts(local-pointer rewrite,definitionshoisting with$id, transitive collection, cycle termination, unknown component left alone, copy-on-write, no-pointer gate), plus recursive describe blocks inpayloads.spec.ts,headers.spec.tsand a newparameters.spec.ts. The integration tests assert exact model counts and no duplicate names, since the duplicate-class failure mode shows up only as an extra model with a repeated name.recursive-asyncapi.ymlcarrying self-recursion, mutual recursion and a recursive union member in one document, so the output has to type-check.GraphNode ↔ GraphEdgecycle round-trip throughmarshal()/unmarshal()with structure intact. No broker needed.Docs:
docs/inputs/asyncapi.mdgains a recursive/self-referencing schema section showing real generator output.No Zod schema changed, so
schemas/regeneration is not triggered by this work.🤖 Generated with Claude Code