Skip to content

Commit 9ec47b3

Browse files
feat: add file path to origin location tracking
Pass file path through the origin chain (yaml3 → yaml → kin-openapi) so that each Location includes which file it came from. This enables source location tracking for external $ref resolution. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 72b5d51 commit 9ec47b3

File tree

9 files changed

+114
-15
lines changed

9 files changed

+114
-15
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ require (
66
github.com/go-openapi/jsonpointer v0.21.0
77
github.com/gorilla/mux v1.8.0
88
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
9-
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037
10-
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90
9+
github.com/oasdiff/yaml v0.0.0-20260217201108-4f1c3d02ddd4
10+
github.com/oasdiff/yaml3 v0.0.0-20260217201013-8a620da6102a
1111
github.com/perimeterx/marshmallow v1.1.5
1212
github.com/stretchr/testify v1.9.0
1313
)

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
1818
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
1919
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
2020
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
21-
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
22-
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
23-
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
24-
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
21+
github.com/oasdiff/yaml v0.0.0-20260217201108-4f1c3d02ddd4 h1:WM2g1YIFwlbz3xcGO/RtaT0LBykfe0Cd3seWa4jZELM=
22+
github.com/oasdiff/yaml v0.0.0-20260217201108-4f1c3d02ddd4/go.mod h1:ZTJR/EwUBsFK7J01Ybq7z/XCz8ioB4TMdr72QTeuqoY=
23+
github.com/oasdiff/yaml3 v0.0.0-20260217201013-8a620da6102a h1:so9gdCU1AyG+EFCCp6ORLut4MBi/wIh4J3jGrEjNZnI=
24+
github.com/oasdiff/yaml3 v0.0.0-20260217201013-8a620da6102a/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
2525
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
2626
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
2727
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

openapi3/internalize_refs.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"strings"
77
)
88

9-
// RefNameResolver maps a component to an name that is used as it's internalized name.
9+
// RefNameResolver maps a component to a name that is used as it's internalized name.
1010
//
1111
// The function should avoid name collisions (i.e. be a injective mapping).
1212
// It must only contain characters valid for fixed field names: [IdentifierRegExp].

openapi3/loader.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, el
108108
if err != nil {
109109
return nil, err
110110
}
111-
if err := unmarshal(data, element, IncludeOrigin); err != nil {
111+
if err := unmarshal(data, element, IncludeOrigin, resolvedPath); err != nil {
112112
return nil, err
113113
}
114114

@@ -144,7 +144,7 @@ func (loader *Loader) LoadFromIoReader(reader io.Reader) (*T, error) {
144144
func (loader *Loader) LoadFromData(data []byte) (*T, error) {
145145
loader.resetVisitedPathItemRefs()
146146
doc := &T{}
147-
if err := unmarshal(data, doc, IncludeOrigin); err != nil {
147+
if err := unmarshal(data, doc, IncludeOrigin, nil); err != nil {
148148
return nil, err
149149
}
150150
if err := loader.ResolveRefsIn(doc, nil); err != nil {
@@ -173,7 +173,7 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR
173173
doc := &T{}
174174
loader.visitedDocuments[uri] = doc
175175

176-
if err := unmarshal(data, doc, IncludeOrigin); err != nil {
176+
if err := unmarshal(data, doc, IncludeOrigin, location); err != nil {
177177
return nil, err
178178
}
179179

@@ -427,7 +427,7 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv
427427
if err2 != nil {
428428
return nil, nil, err
429429
}
430-
if err2 = unmarshal(data, &cursor, IncludeOrigin); err2 != nil {
430+
if err2 = unmarshal(data, &cursor, IncludeOrigin, path); err2 != nil {
431431
return nil, nil, err
432432
}
433433
if cursor, err2 = drill(cursor); err2 != nil || cursor == nil {

openapi3/marsh.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package openapi3
33
import (
44
"encoding/json"
55
"fmt"
6+
"net/url"
67
"strings"
78

89
"github.com/oasdiff/yaml"
@@ -16,7 +17,7 @@ func unmarshalError(jsonUnmarshalErr error) error {
1617
return jsonUnmarshalErr
1718
}
1819

19-
func unmarshal(data []byte, v any, includeOrigin bool) error {
20+
func unmarshal(data []byte, v any, includeOrigin bool, location *url.URL) error {
2021
var jsonErr, yamlErr error
2122

2223
// See https://github.com/getkin/kin-openapi/issues/680
@@ -25,7 +26,11 @@ func unmarshal(data []byte, v any, includeOrigin bool) error {
2526
}
2627

2728
// UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys
28-
if yamlErr = yaml.UnmarshalWithOrigin(data, v, includeOrigin); yamlErr == nil {
29+
var file string
30+
if location != nil {
31+
file = location.Path
32+
}
33+
if yamlErr = yaml.UnmarshalWithOrigin(data, v, includeOrigin, file); yamlErr == nil {
2934
return nil
3035
}
3136

openapi3/origin.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Origin struct {
1212

1313
// Location is a struct that contains the location of a field.
1414
type Location struct {
15-
Line int `json:"line,omitempty" yaml:"line,omitempty"`
16-
Column int `json:"column,omitempty" yaml:"column,omitempty"`
15+
File string `json:"file,omitempty" yaml:"file,omitempty"`
16+
Line int `json:"line,omitempty" yaml:"line,omitempty"`
17+
Column int `json:"column,omitempty" yaml:"column,omitempty"`
1718
}

openapi3/origin_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,23 @@ func TestOrigin_Info(t *testing.T) {
2626
require.NotNil(t, doc.Info.Origin)
2727
require.Equal(t,
2828
&Location{
29+
File: "testdata/origin/simple.yaml",
2930
Line: 2,
3031
Column: 1,
3132
},
3233
doc.Info.Origin.Key)
3334

3435
require.Equal(t,
3536
Location{
37+
File: "testdata/origin/simple.yaml",
3638
Line: 3,
3739
Column: 3,
3840
},
3941
doc.Info.Origin.Fields["title"])
4042

4143
require.Equal(t,
4244
Location{
45+
File: "testdata/origin/simple.yaml",
4346
Line: 4,
4447
Column: 3,
4548
},
@@ -61,6 +64,7 @@ func TestOrigin_Paths(t *testing.T) {
6164
require.NotNil(t, doc.Paths.Origin)
6265
require.Equal(t,
6366
&Location{
67+
File: "testdata/origin/simple.yaml",
6468
Line: 5,
6569
Column: 1,
6670
},
@@ -71,6 +75,7 @@ func TestOrigin_Paths(t *testing.T) {
7175
require.NotNil(t, base.Origin)
7276
require.Equal(t,
7377
&Location{
78+
File: "testdata/origin/simple.yaml",
7479
Line: 13,
7580
Column: 3,
7681
},
@@ -79,6 +84,7 @@ func TestOrigin_Paths(t *testing.T) {
7984
require.NotNil(t, base.Get.Origin)
8085
require.Equal(t,
8186
&Location{
87+
File: "testdata/origin/simple.yaml",
8288
Line: 14,
8389
Column: 5,
8490
},
@@ -101,6 +107,7 @@ func TestOrigin_RequestBody(t *testing.T) {
101107
require.NotNil(t, base.Origin)
102108
require.Equal(t,
103109
&Location{
110+
File: "testdata/origin/request_body.yaml",
104111
Line: 8,
105112
Column: 7,
106113
},
@@ -109,6 +116,7 @@ func TestOrigin_RequestBody(t *testing.T) {
109116
require.NotNil(t, base.Content["application/json"].Origin)
110117
require.Equal(t,
111118
&Location{
119+
File: "testdata/origin/request_body.yaml",
112120
Line: 10,
113121
Column: 11,
114122
},
@@ -131,6 +139,7 @@ func TestOrigin_Responses(t *testing.T) {
131139
require.NotNil(t, base.Origin)
132140
require.Equal(t,
133141
&Location{
142+
File: "testdata/origin/simple.yaml",
134143
Line: 17,
135144
Column: 7,
136145
},
@@ -140,13 +149,15 @@ func TestOrigin_Responses(t *testing.T) {
140149
require.Nil(t, base.Value("200").Origin)
141150
require.Equal(t,
142151
&Location{
152+
File: "testdata/origin/simple.yaml",
143153
Line: 18,
144154
Column: 9,
145155
},
146156
base.Value("200").Value.Origin.Key)
147157

148158
require.Equal(t,
149159
Location{
160+
File: "testdata/origin/simple.yaml",
150161
Line: 19,
151162
Column: 11,
152163
},
@@ -169,20 +180,23 @@ func TestOrigin_Parameters(t *testing.T) {
169180
require.NotNil(t, base)
170181
require.Equal(t,
171182
&Location{
183+
File: "testdata/origin/parameters.yaml",
172184
Line: 9,
173185
Column: 11,
174186
},
175187
base.Origin.Key)
176188

177189
require.Equal(t,
178190
Location{
191+
File: "testdata/origin/parameters.yaml",
179192
Line: 10,
180193
Column: 11,
181194
},
182195
base.Origin.Fields["in"])
183196

184197
require.Equal(t,
185198
Location{
199+
File: "testdata/origin/parameters.yaml",
186200
Line: 9,
187201
Column: 11,
188202
},
@@ -207,13 +221,15 @@ func TestOrigin_SchemaInAdditionalProperties(t *testing.T) {
207221
require.NotNil(t, base.Schema.Value.Origin)
208222
require.Equal(t,
209223
&Location{
224+
File: "testdata/origin/additional_properties.yaml",
210225
Line: 14,
211226
Column: 17,
212227
},
213228
base.Schema.Value.Origin.Key)
214229

215230
require.Equal(t,
216231
Location{
232+
File: "testdata/origin/additional_properties.yaml",
217233
Line: 15,
218234
Column: 19,
219235
},
@@ -237,20 +253,23 @@ func TestOrigin_ExternalDocs(t *testing.T) {
237253

238254
require.Equal(t,
239255
&Location{
256+
File: "testdata/origin/external_docs.yaml",
240257
Line: 13,
241258
Column: 1,
242259
},
243260
base.Origin.Key)
244261

245262
require.Equal(t,
246263
Location{
264+
File: "testdata/origin/external_docs.yaml",
247265
Line: 14,
248266
Column: 3,
249267
},
250268
base.Origin.Fields["description"])
251269

252270
require.Equal(t,
253271
Location{
272+
File: "testdata/origin/external_docs.yaml",
254273
Line: 15,
255274
Column: 3,
256275
},
@@ -274,34 +293,39 @@ func TestOrigin_Security(t *testing.T) {
274293

275294
require.Equal(t,
276295
&Location{
296+
File: "testdata/origin/security.yaml",
277297
Line: 29,
278298
Column: 5,
279299
},
280300
base.Origin.Key)
281301

282302
require.Equal(t,
283303
Location{
304+
File: "testdata/origin/security.yaml",
284305
Line: 30,
285306
Column: 7,
286307
},
287308
base.Origin.Fields["type"])
288309

289310
require.Equal(t,
290311
&Location{
312+
File: "testdata/origin/security.yaml",
291313
Line: 31,
292314
Column: 7,
293315
},
294316
base.Flows.Origin.Key)
295317

296318
require.Equal(t,
297319
&Location{
320+
File: "testdata/origin/security.yaml",
298321
Line: 32,
299322
Column: 9,
300323
},
301324
base.Flows.Implicit.Origin.Key)
302325

303326
require.Equal(t,
304327
Location{
328+
File: "testdata/origin/security.yaml",
305329
Line: 33,
306330
Column: 11,
307331
},
@@ -324,13 +348,15 @@ func TestOrigin_Example(t *testing.T) {
324348
require.NotNil(t, base.Origin)
325349
require.Equal(t,
326350
&Location{
351+
File: "testdata/origin/example.yaml",
327352
Line: 14,
328353
Column: 15,
329354
},
330355
base.Origin.Key)
331356

332357
require.Equal(t,
333358
Location{
359+
File: "testdata/origin/example.yaml",
334360
Line: 15,
335361
Column: 17,
336362
},
@@ -363,20 +389,23 @@ func TestOrigin_XML(t *testing.T) {
363389
require.NotNil(t, base.Origin)
364390
require.Equal(t,
365391
&Location{
392+
File: "testdata/origin/xml.yaml",
366393
Line: 21,
367394
Column: 19,
368395
},
369396
base.Origin.Key)
370397

371398
require.Equal(t,
372399
Location{
400+
File: "testdata/origin/xml.yaml",
373401
Line: 22,
374402
Column: 21,
375403
},
376404
base.Origin.Fields["namespace"])
377405

378406
require.Equal(t,
379407
Location{
408+
File: "testdata/origin/xml.yaml",
380409
Line: 23,
381410
Column: 21,
382411
},
@@ -416,3 +445,40 @@ components:
416445
require.Equal(t, `failed to unmarshal data: json error: invalid character 'p' looking for beginning of value, yaml error: error converting YAML to JSON: yaml: unmarshal errors:
417446
line 0: mapping key "__origin__" already defined at line 17`, err.Error())
418447
}
448+
449+
func TestOrigin_WithExternalRef(t *testing.T) {
450+
loader := NewLoader()
451+
loader.IsExternalRefsAllowed = true
452+
453+
IncludeOrigin = true
454+
defer unsetIncludeOrigin()
455+
456+
loader.Context = context.Background()
457+
458+
doc, err := loader.LoadFromFile("testdata/origin/external.yaml")
459+
require.NoError(t, err)
460+
461+
base := doc.Paths.Find("/subscribe").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["name"].Value
462+
require.NotNil(t, base.XML.Origin)
463+
require.Equal(t, base.XML.Origin.Key.File, "testdata/origin/external-schema.yaml")
464+
// require.Equal(t,
465+
// &Location{
466+
// Line: 1,
467+
// Column: 1,
468+
// },
469+
// base.Origin.Key)
470+
471+
// require.Equal(t,
472+
// Location{
473+
// Line: 2,
474+
// Column: 3,
475+
// },
476+
// base.Origin.Fields["namespace"])
477+
478+
// require.Equal(t,
479+
// Location{
480+
// Line: 3,
481+
// Column: 3,
482+
// },
483+
// base.Origin.Fields["prefix"])
484+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
type: string
2+
xml:
3+
namespace: http://example.com/schema/sample
4+
prefix: sample

0 commit comments

Comments
 (0)