Skip to content

fix: resolve dollar-ref chains and disambiguate component sections - #131

Open
MaxMichel2 wants to merge 3 commits into
refactor/mock-http-client-call-internalfrom
fix/openapi-ref-resolution
Open

MaxMichel2 wants to merge 3 commits into
refactor/mock-http-client-call-internalfrom
fix/openapi-ref-resolution

Conversation

@MaxMichel2

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #126 (PR chain: #123 → #124 → #127 → #128 → #129 → #126 → this) — merge in order.

OpenApiParser.resolveRef keyed resolved $ref targets by a fragment's trailing name segment only, ignoring which components.<section> it actually named, and followed a $ref chain exactly one level deep (a resolved entry's own $ref, if any, was not followed further). Real specs' components sections cross-reference and chain more than this parser could handle — this is a prerequisite for #82/#84 (schema synthesis), which will need to resolve $ref chains through components.schemas the same way.

What changed

  • OpenApiParser.resolveRef (private, inside ParseContext): now takes an explicit section parameter ("parameters"/"responses"/"examples"/"headers") and validates a fragment's declared section against it — a $ref naming an unexpected section is rejected with a clear error instead of being silently resolved against whatever map the call site happened to expect, so a same-named entry in a different section can never be conflated with the one actually referenced.
  • Converted from a single lookup into a loop that keeps resolving as long as a resolved entry itself declares a $ref (via a new refOf parameter), until a non-ref entry is reached — a $ref chain is now followed to completion instead of stopping after one hop.
  • Cycle guard: a visited: MutableSet<Pair<OpenApiDocument, String>> tracks every (document, fragment) hop; revisiting one throws a clear IllegalStateException instead of looping forever.
  • Updated all four call sites (resolveParameter, and the response/example/header refs inside resolveResponseIndex/resolveHeaders) and the class-level KDoc's "Scope decisions" section to match.
  • docs/modules/networkmock-core.md's "$ref resolution" section updated — it documented the now-fixed "one level deep" limitation.

Public API

None — the openapi package stays internal (see #73's pure-seam requirement). No api.txt diff.

Tests

Added to MockConfigRepositoryTest.kt, following the existing $ref test fixtures' style:

  • `dollar-ref naming the wrong components section is rejected even if a same-named entry exists there` — a response $ref pointing at components/parameters/... instead of components/responses/..., with a same-named entry in both sections, now fails clearly instead of resolving against whichever map the call site expected.
  • `local dollar-ref chain of two hops resolves to the final non-ref entry` — A → B → literal content.
  • `cyclic dollar-ref chain fails clearly instead of hanging` — A → B → A.

Verification

.\gradlew.bat detektFull -Pandroidx.baselineprofile.skipgeneration
.\gradlew.bat testAndroidHostTest -Pandroidx.baselineprofile.skipgeneration
.\gradlew.bat :konsist:test -Pandroidx.baselineprofile.skipgeneration

All green, including a full repo-wide testAndroidHostTest run to confirm no fallout outside the networkmock modules.

🤖 Generated with GitHub Copilot

@MaxMichel2 MaxMichel2 changed the title 🐛 fix: resolve dollar-ref chains and disambiguate component sections fix: resolve dollar-ref chains and disambiguate component sections Sep 23, 2026
@MaxMichel2
MaxMichel2 added this pull request to stack #130 September 23, 2026 12:39
OpenApiParser.resolveRef previously keyed resolved components by a ref's
trailing name segment only, ignoring which components.<section> it named,
and followed a chain exactly one level deep.

This fixes both: a ref's fragment must now declare the section its call site
expects (parameters/responses/examples/headers), so a same-named entry in a
different section can never be silently conflated with the one actually
referenced; and ref chains are followed until a non-ref entry is reached,
guarded by a visited-set of (document, fragment) pairs that fails clearly on
a cycle instead of hanging.

@Momik-jpg Momik-jpg left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff at 4666236. The new section check still only inspects the final two fragment segments. For example, a response ref #/not-components/responses/UserOk (or #/responses/UserOk) passes actualSection == "responses" and resolves components.responses["UserOk"], even though the pointer does not identify that location.

Could you validate the complete supported fragment shape components/<expected-section>/<name> before the lookup, and add a regression case using an invalid prefix with an existing same-named response? The current wrong-section test does not exercise this path.

CI checks are green at this head; this finding is from source inspection, not a local runtime reproduction.

The previous section-disambiguation check only inspected the final two
fragment segments. Fragments like "#/not-components/responses/UserOk" or
"#/responses/UserOk" would pass the section check and incorrectly resolve
to components.responses["UserOk"] despite not identifying that location.

Now require the fragment to be exactly "components/<section>/<name>" (per
the OpenAPI Reference Object convention), validating the structure before
any section/name extraction. Update KDoc to reflect the enforced shape.

Add two regression tests exercising malformed fragments that slip through
the old check but fail on the new one.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@MaxMichel2

Copy link
Copy Markdown
Collaborator Author

Hey @Momik-jpg!

Thanks for taking the time to identify this. Indeed, the specification was not 100% respected... I've pushed a new commit that should hopefully fix this.

If not, what we'll do is merge the pending PRs and that should allow you to contribute a fix that fits your needs better :D

@Momik-jpg

Copy link
Copy Markdown

Thanks! I reviewed ead88d9: it fixes both malformed-fragment cases, and CI is green.

I found another issue with external reference chains: loadExternalDocument always uses the original spec’s baseDir.

For example, specs/api.json references ./shared/a.json#/components/responses/A. If A then references ./b.json#/components/responses/B, the parser loads specs/b.json instead of specs/shared/b.json.

Could you track each document’s source path, resolve each hop relative to that document, and add a regression test crossing directories?

This finding is from source inspection; I haven’t reproduced it locally.

OpenApiParser.ParseContext held a single baseDir (the root spec's), so every
external $ref was loaded relative to the root spec even when it appeared in
another file: specs/shared/a.json -> ./b.json loaded specs/b.json instead of
specs/shared/b.json.

Track each document's source path (SourceDocument) and resolve every hop
relative to the document containing the $ref. resolveRef now also returns the
document the final entry was found in, so nested local $refs (headers,
examples) resolve against that document's components, and a relative
externalValue resolves against its containing document rather than the root.
The cycle guard now keys on (path, fragment) instead of structurally comparing
whole documents.

Add regression tests crossing directories (sibling and parent hops, with decoy
files at the old root-relative location), nested local header refs,
externalValue in an external document, and a cross-document cycle.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants