Skip to content

Commit 384415f

Browse files
authored
Merge pull request #83 from fredbi/fix-circular-absolute
optional absolute path in circular $ref
2 parents bce47c9 + 837d3d5 commit 384415f

File tree

9 files changed

+10182
-36
lines changed

9 files changed

+10182
-36
lines changed

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ linters:
1818
disable:
1919
- maligned
2020
- unparam
21+
- lll

expander.go

Lines changed: 79 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ import (
3232

3333
// ExpandOptions provides options for expand.
3434
type ExpandOptions struct {
35-
RelativeBase string
36-
SkipSchemas bool
37-
ContinueOnError bool
35+
RelativeBase string
36+
SkipSchemas bool
37+
ContinueOnError bool
38+
AbsoluteCircularRef bool
3839
}
3940

4041
// ResolutionCache a cache for resolving urls
@@ -444,7 +445,7 @@ func normalizeFileRef(ref *Ref, relativeBase string) *Ref {
444445
return &r
445446
}
446447

447-
debugLog("normalizing %s against %s (%s)", ref.String(), relativeBase, ref.GetURL().String())
448+
debugLog("normalizing %s against %s", ref.String(), relativeBase)
448449

449450
s := normalizePaths(ref.String(), relativeBase)
450451
r, _ := NewRef(s)
@@ -607,25 +608,35 @@ func shouldStopOnError(err error, opts *ExpandOptions) bool {
607608
return false
608609
}
609610

610-
// ExpandSchema expands the refs in the schema object with reference to the root object
611-
// go-openapi/validate uses this function
612-
// notice that it is impossible to reference a json scema in a different file other than root
613-
func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error {
614-
// Only save the root to a tmp file if it isn't nil.
615-
var base string
611+
// baseForRoot loads in the cache the root document and produces a fake "root" base path entry
612+
// for further $ref resolution
613+
func baseForRoot(root interface{}, cache ResolutionCache) string {
614+
// cache the root document to resolve $ref's
615+
const rootBase = "root"
616616
if root != nil {
617-
base, _ = absPath("root")
617+
base, _ := absPath(rootBase)
618+
normalizedBase := normalizeAbsPath(base)
619+
debugLog("setting root doc in cache at: %s", normalizedBase)
618620
if cache == nil {
619621
cache = resCache
620622
}
621-
cache.Set(normalizeAbsPath(base), root)
622-
base = "root"
623+
cache.Set(normalizedBase, root)
624+
return rootBase
623625
}
626+
return ""
627+
}
624628

629+
// ExpandSchema expands the refs in the schema object with reference to the root object
630+
// go-openapi/validate uses this function
631+
// notice that it is impossible to reference a json schema in a different file other than root
632+
func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error {
625633
opts := &ExpandOptions{
626-
RelativeBase: base,
634+
// when a root is specified, cache the root as an in-memory document for $ref retrieval
635+
RelativeBase: baseForRoot(root, cache),
627636
SkipSchemas: false,
628637
ContinueOnError: false,
638+
// when no base path is specified, remaining $ref (circular) are rendered with an absolute path
639+
AbsoluteCircularRef: true,
629640
}
630641
return ExpandSchemaWithBasePath(schema, cache, opts)
631642
}
@@ -734,6 +745,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba
734745
otherwise the basePath should inherit the parent's */
735746
// important: ID can be relative path
736747
if target.ID != "" {
748+
debugLog("schema has ID: %s", target.ID)
737749
// handling the case when id is a folder
738750
// remember that basePath has to be a file
739751
refPath := target.ID
@@ -757,11 +769,13 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba
757769
// this means there is a cycle in the recursion tree: return the Ref
758770
// - circular refs cannot be expanded. We leave them as ref.
759771
// - denormalization means that a new local file ref is set relative to the original basePath
760-
debugLog("shortcut circular ref")
761-
debugLog("basePath: %s", basePath)
762-
debugLog("normalized basePath: %s", normalizedBasePath)
763-
debugLog("normalized ref: %s", normalizedRef.String())
764-
target.Ref = *denormalizeFileRef(normalizedRef, normalizedBasePath, resolver.context.basePath)
772+
debugLog("shortcut circular ref: basePath: %s, normalizedPath: %s, normalized ref: %s",
773+
basePath, normalizedBasePath, normalizedRef.String())
774+
if !resolver.options.AbsoluteCircularRef {
775+
target.Ref = *denormalizeFileRef(normalizedRef, normalizedBasePath, resolver.context.basePath)
776+
} else {
777+
target.Ref = *normalizedRef
778+
}
765779
return &target, nil
766780
}
767781

@@ -1015,19 +1029,40 @@ func transitiveResolver(basePath string, ref Ref, resolver *schemaLoader) (*sche
10151029
return resolver, nil
10161030
}
10171031

1032+
// ExpandResponseWithRoot expands a response based on a root document, not a fetchable document
1033+
func ExpandResponseWithRoot(response *Response, root interface{}, cache ResolutionCache) error {
1034+
opts := &ExpandOptions{
1035+
RelativeBase: baseForRoot(root, cache),
1036+
SkipSchemas: false,
1037+
ContinueOnError: false,
1038+
// when no base path is specified, remaining $ref (circular) are rendered with an absolute path
1039+
AbsoluteCircularRef: true,
1040+
}
1041+
resolver, err := defaultSchemaLoader(root, opts, nil, nil)
1042+
if err != nil {
1043+
return err
1044+
}
1045+
1046+
return expandResponse(response, resolver, opts.RelativeBase)
1047+
}
1048+
10181049
// ExpandResponse expands a response based on a basepath
10191050
// This is the exported version of expandResponse
10201051
// all refs inside response will be resolved relative to basePath
10211052
func ExpandResponse(response *Response, basePath string) error {
1053+
var specBasePath string
1054+
if basePath != "" {
1055+
specBasePath, _ = absPath(basePath)
1056+
}
10221057
opts := &ExpandOptions{
1023-
RelativeBase: basePath,
1058+
RelativeBase: specBasePath,
10241059
}
10251060
resolver, err := defaultSchemaLoader(nil, opts, nil, nil)
10261061
if err != nil {
10271062
return err
10281063
}
10291064

1030-
return expandResponse(response, resolver, basePath)
1065+
return expandResponse(response, resolver, opts.RelativeBase)
10311066
}
10321067

10331068
func derefResponse(response *Response, parentRefs []string, resolver *schemaLoader, basePath string) error {
@@ -1058,7 +1093,6 @@ func expandResponse(response *Response, resolver *schemaLoader, basePath string)
10581093
if response == nil {
10591094
return nil
10601095
}
1061-
10621096
parentRefs := []string{}
10631097
if err := derefResponse(response, parentRefs, resolver, basePath); shouldStopOnError(err, resolver.options) {
10641098
return err
@@ -1094,19 +1128,40 @@ func expandResponse(response *Response, resolver *schemaLoader, basePath string)
10941128
return nil
10951129
}
10961130

1131+
// ExpandParameterWithRoot expands a parameter based on a root document, not a fetchable document
1132+
func ExpandParameterWithRoot(parameter *Parameter, root interface{}, cache ResolutionCache) error {
1133+
opts := &ExpandOptions{
1134+
RelativeBase: baseForRoot(root, cache),
1135+
SkipSchemas: false,
1136+
ContinueOnError: false,
1137+
// when no base path is specified, remaining $ref (circular) are rendered with an absolute path
1138+
AbsoluteCircularRef: true,
1139+
}
1140+
resolver, err := defaultSchemaLoader(root, opts, nil, nil)
1141+
if err != nil {
1142+
return err
1143+
}
1144+
1145+
return expandParameter(parameter, resolver, opts.RelativeBase)
1146+
}
1147+
10971148
// ExpandParameter expands a parameter based on a basepath
10981149
// This is the exported version of expandParameter
10991150
// all refs inside parameter will be resolved relative to basePath
11001151
func ExpandParameter(parameter *Parameter, basePath string) error {
1152+
var specBasePath string
1153+
if basePath != "" {
1154+
specBasePath, _ = absPath(basePath)
1155+
}
11011156
opts := &ExpandOptions{
1102-
RelativeBase: basePath,
1157+
RelativeBase: specBasePath,
11031158
}
11041159
resolver, err := defaultSchemaLoader(nil, opts, nil, nil)
11051160
if err != nil {
11061161
return err
11071162
}
11081163

1109-
return expandParameter(parameter, resolver, basePath)
1164+
return expandParameter(parameter, resolver, opts.RelativeBase)
11101165
}
11111166

11121167
func derefParameter(parameter *Parameter, parentRefs []string, resolver *schemaLoader, basePath string) error {

expander_test.go

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ import (
3232
"github.com/stretchr/testify/assert"
3333
)
3434

35+
var (
36+
rex = regexp.MustCompile(`"\$ref":\s*"(.+)"`)
37+
)
38+
3539
func jsonDoc(path string) (json.RawMessage, error) {
3640
data, err := swag.LoadFromFileOrHTTP(path)
3741
if err != nil {
@@ -286,6 +290,39 @@ func TestExportedResponseExpansion(t *testing.T) {
286290
// assert.Equal(t, expected, resp)
287291
}
288292

293+
func TestExpandResponseAndParamWithRoot(t *testing.T) {
294+
specDoc, err := jsonDoc("fixtures/bugs/1614/gitea.json")
295+
if !assert.NoError(t, err) {
296+
t.FailNow()
297+
return
298+
}
299+
var spec Swagger
300+
_ = json.Unmarshal(specDoc, &spec)
301+
302+
// check responses with $ref
303+
resp := spec.Paths.Paths["/admin/users"].Post.Responses.StatusCodeResponses[201]
304+
err = ExpandResponseWithRoot(&resp, spec, nil)
305+
assert.NoError(t, err)
306+
jazon, _ := json.MarshalIndent(resp, "", " ")
307+
m := rex.FindAllStringSubmatch(string(jazon), -1)
308+
assert.Nil(t, m)
309+
310+
resp = spec.Paths.Paths["/admin/users"].Post.Responses.StatusCodeResponses[403]
311+
err = ExpandResponseWithRoot(&resp, spec, nil)
312+
assert.NoError(t, err)
313+
jazon, _ = json.MarshalIndent(resp, "", " ")
314+
m = rex.FindAllStringSubmatch(string(jazon), -1)
315+
assert.Nil(t, m)
316+
317+
// check param with $ref
318+
param := spec.Paths.Paths["/admin/users"].Post.Parameters[0]
319+
err = ExpandParameterWithRoot(&param, spec, nil)
320+
assert.NoError(t, err)
321+
jazon, _ = json.MarshalIndent(param, "", " ")
322+
m = rex.FindAllStringSubmatch(string(jazon), -1)
323+
assert.Nil(t, m)
324+
}
325+
289326
func TestIssue3(t *testing.T) {
290327
spec := new(Swagger)
291328
specDoc, err := jsonDoc("fixtures/expansion/overflow.json")
@@ -428,7 +465,6 @@ func Test_MoreCircular(t *testing.T) {
428465

429466
fixturePath := "fixtures/more_circulars/spec.json"
430467
jazon := expandThisOrDieTrying(t, fixturePath)
431-
rex := regexp.MustCompile(`"\$ref":\s*"(.+)"`)
432468
m := rex.FindAllStringSubmatch(jazon, -1)
433469
if assert.NotNil(t, m) {
434470
for _, matched := range m {
@@ -440,7 +476,6 @@ func Test_MoreCircular(t *testing.T) {
440476

441477
fixturePath = "fixtures/more_circulars/spec2.json"
442478
jazon = expandThisOrDieTrying(t, fixturePath)
443-
rex = regexp.MustCompile(`"\$ref":\s*"(.+)"`)
444479
m = rex.FindAllStringSubmatch(jazon, -1)
445480
if assert.NotNil(t, m) {
446481
for _, matched := range m {
@@ -452,7 +487,6 @@ func Test_MoreCircular(t *testing.T) {
452487

453488
fixturePath = "fixtures/more_circulars/spec3.json"
454489
jazon = expandThisOrDieTrying(t, fixturePath)
455-
rex = regexp.MustCompile(`"\$ref":\s*"(.+)"`)
456490
m = rex.FindAllStringSubmatch(jazon, -1)
457491
if assert.NotNil(t, m) {
458492
for _, matched := range m {
@@ -464,7 +498,6 @@ func Test_MoreCircular(t *testing.T) {
464498

465499
fixturePath = "fixtures/more_circulars/spec4.json"
466500
jazon = expandThisOrDieTrying(t, fixturePath)
467-
rex = regexp.MustCompile(`"\$ref":\s*"(.+)"`)
468501
m = rex.FindAllStringSubmatch(jazon, -1)
469502
if assert.NotNil(t, m) {
470503
for _, matched := range m {
@@ -481,7 +514,6 @@ func Test_Issue957(t *testing.T) {
481514
if assert.NotEmpty(t, jazon) {
482515
assert.NotContainsf(t, jazon, "fixture-957.json#/",
483516
"expected %s to be expanded with stripped circular $ref", fixturePath)
484-
rex := regexp.MustCompile(`"\$ref":\s*"(.+)"`)
485517
m := rex.FindAllStringSubmatch(jazon, -1)
486518
if assert.NotNil(t, m) {
487519
for _, matched := range m {
@@ -499,7 +531,6 @@ func Test_Bitbucket(t *testing.T) {
499531

500532
fixturePath := "fixtures/more_circulars/bitbucket.json"
501533
jazon := expandThisOrDieTrying(t, fixturePath)
502-
rex := regexp.MustCompile(`"\$ref":\s*"(.+)"`)
503534
m := rex.FindAllStringSubmatch(jazon, -1)
504535
if assert.NotNil(t, m) {
505536
for _, matched := range m {
@@ -514,7 +545,6 @@ func Test_ExpandJSONSchemaDraft4(t *testing.T) {
514545
fixturePath := filepath.Join("schemas", "jsonschema-draft-04.json")
515546
jazon := expandThisSchemaOrDieTrying(t, fixturePath)
516547
// assert all $ref maches "$ref": "http://json-schema.org/draft-04/something"
517-
rex := regexp.MustCompile(`"\$ref":\s*"(.+)"`)
518548
m := rex.FindAllStringSubmatch(jazon, -1)
519549
if assert.NotNil(t, m) {
520550
for _, matched := range m {
@@ -529,7 +559,6 @@ func Test_ExpandSwaggerSchema(t *testing.T) {
529559
fixturePath := filepath.Join("schemas", "v2", "schema.json")
530560
jazon := expandThisSchemaOrDieTrying(t, fixturePath)
531561
// assert all $ref maches "$ref": "#/definitions/something"
532-
rex := regexp.MustCompile(`"\$ref":\s*"(.+)"`)
533562
m := rex.FindAllStringSubmatch(jazon, -1)
534563
if assert.NotNil(t, m) {
535564
for _, matched := range m {
@@ -1412,6 +1441,77 @@ func TestResolveForTransitiveRefs(t *testing.T) {
14121441
assert.NoError(t, err)
14131442
}
14141443

1444+
const (
1445+
withoutSchemaID = "removed"
1446+
withSchemaID = "schema"
1447+
)
1448+
1449+
func TestExpandSchemaWithRoot(t *testing.T) {
1450+
root := new(Swagger)
1451+
_ = json.Unmarshal(PetStoreJSONMessage, root)
1452+
1453+
// 1. remove ID from root definition
1454+
origPet := root.Definitions["Pet"]
1455+
newPet := origPet
1456+
newPet.ID = ""
1457+
root.Definitions["Pet"] = newPet
1458+
expandRootWithID(t, root, withoutSchemaID)
1459+
1460+
// 2. put back ID in Pet definition
1461+
// nested $ref should fail
1462+
//Debug = true
1463+
root.Definitions["Pet"] = origPet
1464+
expandRootWithID(t, root, withSchemaID)
1465+
}
1466+
1467+
func expandRootWithID(t *testing.T, root *Swagger, testcase string) {
1468+
t.Logf("case: expanding $ref to schema without ID, with nested $ref with %s ID", testcase)
1469+
sch := &Schema{
1470+
SchemaProps: SchemaProps{
1471+
Ref: MustCreateRef("#/definitions/newPet"),
1472+
},
1473+
}
1474+
err := ExpandSchema(sch, root, nil)
1475+
if testcase == withSchemaID {
1476+
assert.Errorf(t, err, "expected %s NOT to expand properly because of the ID in the parent schema", sch.Ref.String())
1477+
} else {
1478+
assert.NoErrorf(t, err, "expected %s to expand properly", sch.Ref.String())
1479+
}
1480+
if Debug {
1481+
bbb, _ := json.MarshalIndent(sch, "", " ")
1482+
t.Log(string(bbb))
1483+
}
1484+
1485+
t.Log("case: expanding $ref to schema without nested $ref")
1486+
sch = &Schema{
1487+
SchemaProps: SchemaProps{
1488+
Ref: MustCreateRef("#/definitions/Category"),
1489+
},
1490+
}
1491+
err = ExpandSchema(sch, root, nil)
1492+
assert.NoErrorf(t, err, "expected %s to expand properly", sch.Ref.String())
1493+
if Debug {
1494+
bbb, _ := json.MarshalIndent(sch, "", " ")
1495+
t.Log(string(bbb))
1496+
}
1497+
t.Logf("case: expanding $ref to schema with %s ID and nested $ref", testcase)
1498+
sch = &Schema{
1499+
SchemaProps: SchemaProps{
1500+
Ref: MustCreateRef("#/definitions/Pet"),
1501+
},
1502+
}
1503+
err = ExpandSchema(sch, root, nil)
1504+
if testcase == withSchemaID {
1505+
assert.Errorf(t, err, "expected %s NOT to expand properly because of the ID in the parent schema", sch.Ref.String())
1506+
} else {
1507+
assert.NoErrorf(t, err, "expected %s to expand properly", sch.Ref.String())
1508+
}
1509+
if Debug {
1510+
bbb, _ := json.MarshalIndent(sch, "", " ")
1511+
t.Log(string(bbb))
1512+
}
1513+
}
1514+
14151515
// PetStoreJSONMessage json raw message for Petstore20
14161516
var PetStoreJSONMessage = json.RawMessage([]byte(PetStore20))
14171517

0 commit comments

Comments
 (0)