From a7ba65c1a69b524cc746fbfee9eeee9b5e63d04c Mon Sep 17 00:00:00 2001 From: ivmorski Date: Fri, 9 Oct 2026 15:49:51 +0200 Subject: [PATCH] Keep parsing past a ref that cannot be followed --- internal/provider/libopenapi/converter.go | 4 +- internal/provider/libopenapi/provider.go | 16 ++--- internal/provider/libopenapi/provider_test.go | 66 +++++++++++++++++-- 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/internal/provider/libopenapi/converter.go b/internal/provider/libopenapi/converter.go index 78e57451..eadb603c 100644 --- a/internal/provider/libopenapi/converter.go +++ b/internal/provider/libopenapi/converter.go @@ -660,9 +660,11 @@ func (c *converter) rebuild(p *base.SchemaProxy, ptr string) *base.Schema { lp := p.GoLow() ctx, node, idx := lp.GetContext(), lp.GetValueNode(), lp.GetIndex() if isRef, _, _ := utils.IsNodeRefValue(node); isRef { - // A spec with a $ref libopenapi cannot find has no model, so the target is there. node, idx, _, ctx = lowmodel.LocateRefNodeWithContext(ctx, node, idx) } + if node == nil { + return nil + } kept := keywords{c: c, node: utils.NodeAlias(node), ptr: ptr}.buildable() ls := &lowbase.Schema{} diff --git a/internal/provider/libopenapi/provider.go b/internal/provider/libopenapi/provider.go index a9c1d2cb..df3d3b4e 100644 --- a/internal/provider/libopenapi/provider.go +++ b/internal/provider/libopenapi/provider.go @@ -17,6 +17,8 @@ import ( "github.com/pb33f/libopenapi" "github.com/pb33f/libopenapi/datamodel" + v3 "github.com/pb33f/libopenapi/datamodel/high/v3" + lowv3 "github.com/pb33f/libopenapi/datamodel/low/v3" "github.com/mockzilla/mockzilla-codegen/internal/diag" "github.com/mockzilla/mockzilla-codegen/internal/provider" @@ -66,7 +68,8 @@ func (p *Provider) Parse(ctx context.Context, data []byte, opts provider.ParseOp return nil, nil, err } - d, err := libopenapi.NewDocumentWithConfiguration(data, p.config()) + cfg := p.config() + d, err := libopenapi.NewDocumentWithConfiguration(data, cfg) if err != nil { return nil, nil, fmt.Errorf("%w: %s: %w", provider.ErrParse, syntaxAt(opts.File, data, err), err) } @@ -75,14 +78,11 @@ func (p *Provider) Parse(ctx context.Context, data []byte, opts provider.ParseOp return nil, nil, fmt.Errorf("%w: %s", err, opts.File) } - model, buildErr := d.BuildV3Model() - if model == nil { - return nil, nil, fmt.Errorf("%w: %s: %w", provider.ErrParse, opts.File, buildErr) - } - + // BuildV3Model drops the whole model for one ref it cannot follow, like one inside an example value. + low, buildErr := lowv3.CreateDocumentFromConfig(d.GetSpecInfo(), cfg) c := newConverter(version, opts) - c.buildIssues(buildErr, model.Index.GetCircularReferences()) - doc = c.document(&model.Model) + c.buildIssues(buildErr, low.Index.GetCircularReferences()) + doc = c.document(v3.NewDocument(low)) return doc, c.diags.List(), nil } diff --git a/internal/provider/libopenapi/provider_test.go b/internal/provider/libopenapi/provider_test.go index 352ab813..2634fe14 100644 --- a/internal/provider/libopenapi/provider_test.go +++ b/internal/provider/libopenapi/provider_test.go @@ -83,11 +83,6 @@ func TestParseErrors(t *testing.T) { }, {name: "Not an OpenAPI document", src: "a: b\n", wantErr: provider.ErrParse}, {name: "Swagger 2.0", src: "swagger: '2.0'\ninfo: {title: t, version: '1'}\npaths: {}\n", wantErr: provider.ErrUnsupportedVersion}, - { - name: "Ref to nothing", - src: minimalSpec + "components:\n schemas:\n A: {$ref: '#/components/schemas/Missing'}\n", - wantErr: provider.ErrParse, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -100,6 +95,67 @@ func TestParseErrors(t *testing.T) { } } +func TestParseUnresolvedRefs(t *testing.T) { + t.Parallel() + + file := diag.Origin{File: "spec.yaml"} + tests := []struct { + name string + src string + wantDiags []diag.Diagnostic + }{ + { + name: "Ref inside an example value", + src: "openapi: 3.1.0\ninfo: {title: t, version: '1'}\n" + + "paths:\n /a:\n get:\n responses:\n '200':\n description: ok\n" + + " content:\n application/json:\n" + + " examples:\n many: {$ref: '#/components/examples/Many'}\n" + + "components:\n examples:\n One:\n value: {id: 1}\n" + + " Many:\n value: {$ref: '#/components/examples/One/value'}\n", + wantDiags: []diag.Diagnostic{{ + Severity: diag.Warning, + Code: diag.CodeBuildIssue, + Origin: file, + Message: "cannot resolve reference `#/components/examples/One/value`, it's missing: $.components.examples['One'].value [18:15]", + }}, + }, + { + name: "Ref to nothing", + src: minimalSpec + "components:\n schemas:\n A: {$ref: '#/components/schemas/Missing'}\n", + wantDiags: []diag.Diagnostic{ + { + Severity: diag.Warning, + Code: diag.CodeBuildIssue, + Origin: file, + Message: "cannot resolve reference `#/components/schemas/Missing`, it's missing: $.components.schemas['Missing'] [6:9]", + }, + { + Severity: diag.Warning, + Code: diag.CodeBuildIssue, + Origin: file, + Message: "component `#/components/schemas/Missing` does not exist in the specification", + }, + { + Severity: diag.Error, + Code: diag.CodeSchemaBuild, + Pointer: "/components/schemas/Missing", + Origin: diag.Origin{File: "spec.yaml", Line: 6, Col: 8}, + Message: "schema cannot be built: build schema failed: reference cannot be found: '#/components/schemas/Missing'", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + _, diags, err := New().Parse(context.Background(), []byte(tt.src), provider.ParseOptions{File: "spec.yaml"}) + require.NoError(t, err) + assert.Equal(t, tt.wantDiags, diags) + }) + } +} + func TestSyntaxAt(t *testing.T) { t.Parallel()