Skip to content

feat(frontend): resolve external $ref via a pluggable Loader - #10

Merged
tdakkota merged 1 commit into
mainfrom
feat/external-ref-loader
Jul 20, 2026
Merged

feat(frontend): resolve external $ref via a pluggable Loader#10
tdakkota merged 1 commit into
mainfrom
feat/external-ref-loader

Conversation

@tdakkota

Copy link
Copy Markdown
Contributor

Summary

Closes #3. Only in-document $refs resolved before; external/remote document refs degraded to unresolved-ref diagnostics. This adds an optional Loader on Options so cross-document schemas resolve.

type Loader func(ctx context.Context, uri *url.URL) ([]byte, error)

type Options struct {
    // ...
    Loader Loader // nil ⇒ external refs stay unresolved (unchanged behavior)
}

The Loader receives the resolved absolute target URI (fragment removed) and returns raw bytes — the frontend does its own libopenapi parsing, staying decoupled from libopenapi's rolodex (which stripRefs deliberately bypasses). Callers supply the transport (filesystem, HTTP, in-memory). Per issue #3 the signature follows google/jsonschema-go's ResolveOptions.Loader, with context.Context added for cancellation.

How it works

Reference resolution becomes a fixed-point worklist (resolve.go):

  1. resolvePass resolves every ref that can be resolved now and collects the distinct external target documents that are absent from the registry and not yet attempted. A present resource with a missing fragment is a genuine dangling ref, not a fetch candidate.
  2. Each missing document is fetched via the Loader, parsed, and convertSchema-d into the same convState/Registry, seeding its retrieval URI as a resource (a declared root $id additionally registers itself). Its own refs join the worklist.
  3. Repeat until nothing new loads, then finalizeUnresolved records any still-dangling refs (folding in loader errors), and analyzeSCCs runs once — so transitive loads and cross-document cycles classify guarded/unguarded correctly.

Each document is fetched at most once (loaded set), which also bounds the worklist and breaks document cycles.

API threading

  • frontend.Load(ctx, data, baseURI) kept unchanged (all existing callers/tests untouched); new LoadWithLoader(ctx, data, baseURI, loader) carries the loader. Compile uses the latter; FromLibOpenAPI passes nil.

Base-URI policy

Seed the retrieval URI as the loaded document's base; a declared root $id additionally registers its own resource. Refs addressing a document by an $id that differs from its fetch URL are a known limitation (not reconciled here).

Tests

internal/frontend/external_test.go (in-memory loader): pointer-fragment + whole-resource external refs, transitive load (root→a→b), cross-document cycle (terminates + classifies Guarded), nil-loader graceful degrade, loader-error diagnostic, load-once-per-document caching, missing-fragment-not-refetched. Plus end-to-end Compile tests with/without Options.Loader in schemacompiler_test.go.

Verification

  • go build ./..., go test ./... (incl. conformance), go test -race ./internal/frontend/ . all green
  • golangci-lint run ./... → 0 issues

🤖 Generated with Claude Code

Add Options.Loader func(ctx, *url.URL) ([]byte, error) so cross-document
$refs resolve. Reference resolution becomes a fixed-point worklist that
fetches each unknown target document on demand, integrates its nodes into
the shared registry, and only runs SCC analysis once every document is
loaded — so transitive and cross-document recursion classify correctly.
A nil Loader preserves the single-document graceful-degrade behavior.

Closes #3

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 76.85950% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.42%. Comparing base (cdebc6a) to head (571b304).

Files with missing lines Patch % Lines
internal/frontend/loader.go 60.00% 16 Missing and 8 partials ⚠️
internal/frontend/resolve.go 92.15% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #10      +/-   ##
==========================================
- Coverage   75.54%   75.42%   -0.12%     
==========================================
  Files          42       42              
  Lines        3051     3142      +91     
==========================================
+ Hits         2305     2370      +65     
- Misses        613      629      +16     
- Partials      133      143      +10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tdakkota
tdakkota merged commit 4fd0868 into main Jul 20, 2026
15 checks passed
@tdakkota
tdakkota deleted the feat/external-ref-loader branch July 20, 2026 14:42
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.

frontend: support external/remote $ref via a Loader

1 participant