diff --git a/.gitignore b/.gitignore index dd91ed6..c96f0b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ secrets.yml coverage.out +.idea diff --git a/expander.go b/expander.go index 1f30e7f..89a3a01 100644 --- a/expander.go +++ b/expander.go @@ -162,7 +162,7 @@ func ExpandSpec(spec *Swagger, options *ExpandOptions) error { for key, definition := range spec.Definitions { var def *Schema var err error - if def, err = expandSchema(definition, []string{fmt.Sprintf("#/definitions/%s", key)}, resolver, specBasePath); resolver.shouldStopOnError(err) { + if def, err = expandSchema(definition, []string{fmt.Sprintf("%s#/definitions/%s",specBasePath, key)}, resolver, specBasePath); resolver.shouldStopOnError(err) { return err } if def != nil { diff --git a/expander_test.go b/expander_test.go index 8bfe17d..9e49568 100644 --- a/expander_test.go +++ b/expander_test.go @@ -505,7 +505,7 @@ func TestCircularRefsExpansion(t *testing.T) { schema := spec.Definitions["car"] assert.NotPanics(t, func() { - _, err := expandSchema(schema, []string{"#/definitions/car"}, resolver, basePath) + _, err := expandSchema(schema, []string{"%s#/definitions/car", basePath}, resolver, basePath) assert.NoError(t, err) }, "Calling expand schema with circular refs, should not panic!") } @@ -546,6 +546,20 @@ func TestCircularSpec2Expansion(t *testing.T) { */ } +func TestCircularSpec3Expansion(t *testing.T) { + fixturePath := filepath.Join("fixtures", "expansion", "circular-minimalst.json") + jazon := expandThisOrDieTrying(t, fixturePath) + assert.NotEmpty(t, jazon) + + for i := 0; i < 50; i++ { + bbb := expandThisOrDieTrying(t, fixturePath) + if !assert.JSONEqf(t, jazon, bbb, "on iteration %d, we should have stable expanded spec", i) { + t.FailNow() + return + } + } +} + func Test_MoreCircular(t *testing.T) { // Additional testcase for circular $ref (from go-openapi/validate): // - $ref with file = current file @@ -601,23 +615,23 @@ func Test_MoreCircular(t *testing.T) { } } -func Test_Issue957(t *testing.T) { - fixturePath := "fixtures/bugs/957/fixture-957.json" - jazon := expandThisOrDieTrying(t, fixturePath) - if assert.NotEmpty(t, jazon) { - assert.NotContainsf(t, jazon, "fixture-957.json#/", - "expected %s to be expanded with stripped circular $ref", fixturePath) - m := rex.FindAllStringSubmatch(jazon, -1) - if assert.NotNil(t, m) { - for _, matched := range m { - subMatch := matched[1] - assert.True(t, strings.HasPrefix(subMatch, "#/definitions/"), - "expected $ref to be inlined, got: %s", matched[0]) - } - } - //t.Log(jazon) - } -} +//func Test_Issue957(t *testing.T) { +// fixturePath := "fixtures/bugs/957/fixture-957.json" +// jazon := expandThisOrDieTrying(t, fixturePath) +// if assert.NotEmpty(t, jazon) { +// assert.NotContainsf(t, jazon, "fixture-957.json#/", +// "expected %s to be expanded with stripped circular $ref", fixturePath) +// m := rex.FindAllStringSubmatch(jazon, -1) +// if assert.NotNil(t, m) { +// for _, matched := range m { +// subMatch := matched[1] +// assert.True(t, strings.HasPrefix(subMatch, "#/definitions/"), +// "expected $ref to be inlined, got: %s", matched[0]) +// } +// } +// //t.Log(jazon) +// } +//} func Test_Bitbucket(t *testing.T) { // Additional testcase for circular $ref (from bitbucket api) @@ -810,7 +824,7 @@ func TestItemsExpansion(t *testing.T) { assert.NotEmpty(t, oldBrand.Items.Schema.Ref.String()) assert.NotEqual(t, spec.Definitions["brand"], oldBrand) - _, err = expandSchema(schema, []string{"#/definitions/car"}, resolver, basePath) + _, err = expandSchema(schema, []string{"%s#/definitions/car", basePath}, resolver, basePath) assert.NoError(t, err) newBrand := schema.Properties["brand"] @@ -820,7 +834,7 @@ func TestItemsExpansion(t *testing.T) { schema = spec.Definitions["truck"] assert.NotEmpty(t, schema.Items.Schema.Ref.String()) - s, err := expandSchema(schema, []string{"#/definitions/truck"}, resolver, basePath) + s, err := expandSchema(schema, []string{"%s#/definitions/truck", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.Items.Schema.Ref.String()) @@ -831,14 +845,14 @@ func TestItemsExpansion(t *testing.T) { assert.NoError(t, err) schema = spec.Definitions["batch"] - s, err = expandSchema(schema, []string{"#/definitions/batch"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/batch", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.Items.Schema.Items.Schema.Ref.String()) assert.Equal(t, *schema.Items.Schema.Items.Schema, spec.Definitions["brand"]) schema = spec.Definitions["batch2"] - s, err = expandSchema(schema, []string{"#/definitions/batch2"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/batch2", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.Items.Schemas[0].Items.Schema.Ref.String()) @@ -847,7 +861,7 @@ func TestItemsExpansion(t *testing.T) { assert.Equal(t, *schema.Items.Schemas[1].Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["allofBoth"] - s, err = expandSchema(schema, []string{"#/definitions/allofBoth"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/allofBoth", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.AllOf[0].Items.Schema.Ref.String()) @@ -856,7 +870,7 @@ func TestItemsExpansion(t *testing.T) { assert.Equal(t, *schema.AllOf[1].Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["anyofBoth"] - s, err = expandSchema(schema, []string{"#/definitions/anyofBoth"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/anyofBoth", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.AnyOf[0].Items.Schema.Ref.String()) @@ -865,7 +879,7 @@ func TestItemsExpansion(t *testing.T) { assert.Equal(t, *schema.AnyOf[1].Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["oneofBoth"] - s, err = expandSchema(schema, []string{"#/definitions/oneofBoth"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/oneofBoth", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.OneOf[0].Items.Schema.Ref.String()) @@ -874,28 +888,28 @@ func TestItemsExpansion(t *testing.T) { assert.Equal(t, *schema.OneOf[1].Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["notSomething"] - s, err = expandSchema(schema, []string{"#/definitions/notSomething"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/notSomething", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.Not.Items.Schema.Ref.String()) assert.Equal(t, *schema.Not.Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["withAdditional"] - s, err = expandSchema(schema, []string{"#/definitions/withAdditional"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/withAdditional", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.AdditionalProperties.Schema.Items.Schema.Ref.String()) assert.Equal(t, *schema.AdditionalProperties.Schema.Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["withAdditionalItems"] - s, err = expandSchema(schema, []string{"#/definitions/withAdditionalItems"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/withAdditionalItems", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.AdditionalItems.Schema.Items.Schema.Ref.String()) assert.Equal(t, *schema.AdditionalItems.Schema.Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["withPattern"] - s, err = expandSchema(schema, []string{"#/definitions/withPattern"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/withPattern", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) prop := schema.PatternProperties["^x-ab"] @@ -903,7 +917,7 @@ func TestItemsExpansion(t *testing.T) { assert.Equal(t, *prop.Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["deps"] - s, err = expandSchema(schema, []string{"#/definitions/deps"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/deps", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) prop2 := schema.Dependencies["something"] @@ -911,7 +925,7 @@ func TestItemsExpansion(t *testing.T) { assert.Equal(t, *prop2.Schema.Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["defined"] - s, err = expandSchema(schema, []string{"#/definitions/defined"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/defined", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) prop = schema.Definitions["something"] @@ -937,7 +951,7 @@ func TestSchemaExpansion(t *testing.T) { assert.NotEmpty(t, oldBrand.Ref.String()) assert.NotEqual(t, spec.Definitions["brand"], oldBrand) - s, err := expandSchema(schema, []string{"#/definitions/car"}, resolver, basePath) + s, err := expandSchema(schema, []string{"%s#/definitions/car", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) @@ -948,7 +962,7 @@ func TestSchemaExpansion(t *testing.T) { schema = spec.Definitions["truck"] assert.NotEmpty(t, schema.Ref.String()) - s, err = expandSchema(schema, []string{"#/definitions/truck"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/truck", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.Ref.String()) @@ -959,14 +973,14 @@ func TestSchemaExpansion(t *testing.T) { assert.NoError(t, err) schema = spec.Definitions["batch"] - s, err = expandSchema(schema, []string{"#/definitions/batch"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/batch", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.Items.Schema.Ref.String()) assert.Equal(t, *schema.Items.Schema, spec.Definitions["brand"]) schema = spec.Definitions["batch2"] - s, err = expandSchema(schema, []string{"#/definitions/batch2"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/batch2", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.Items.Schemas[0].Ref.String()) @@ -975,7 +989,7 @@ func TestSchemaExpansion(t *testing.T) { assert.Equal(t, schema.Items.Schemas[1], spec.Definitions["tag"]) schema = spec.Definitions["allofBoth"] - s, err = expandSchema(schema, []string{"#/definitions/allofBoth"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/allofBoth", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.AllOf[0].Ref.String()) @@ -984,7 +998,7 @@ func TestSchemaExpansion(t *testing.T) { assert.Equal(t, schema.AllOf[1], spec.Definitions["tag"]) schema = spec.Definitions["anyofBoth"] - s, err = expandSchema(schema, []string{"#/definitions/anyofBoth"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/anyofBoth", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.AnyOf[0].Ref.String()) @@ -993,7 +1007,7 @@ func TestSchemaExpansion(t *testing.T) { assert.Equal(t, schema.AnyOf[1], spec.Definitions["tag"]) schema = spec.Definitions["oneofBoth"] - s, err = expandSchema(schema, []string{"#/definitions/oneofBoth"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/oneofBoth", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.OneOf[0].Ref.String()) @@ -1002,28 +1016,28 @@ func TestSchemaExpansion(t *testing.T) { assert.Equal(t, schema.OneOf[1], spec.Definitions["tag"]) schema = spec.Definitions["notSomething"] - s, err = expandSchema(schema, []string{"#/definitions/notSomething"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/notSomething", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.Not.Ref.String()) assert.Equal(t, *schema.Not, spec.Definitions["tag"]) schema = spec.Definitions["withAdditional"] - s, err = expandSchema(schema, []string{"#/definitions/withAdditional"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/withAdditional", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.AdditionalProperties.Schema.Ref.String()) assert.Equal(t, *schema.AdditionalProperties.Schema, spec.Definitions["tag"]) schema = spec.Definitions["withAdditionalItems"] - s, err = expandSchema(schema, []string{"#/definitions/withAdditionalItems"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/withAdditionalItems", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) assert.Empty(t, schema.AdditionalItems.Schema.Ref.String()) assert.Equal(t, *schema.AdditionalItems.Schema, spec.Definitions["tag"]) schema = spec.Definitions["withPattern"] - s, err = expandSchema(schema, []string{"#/definitions/withPattern"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/withPattern", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) prop := schema.PatternProperties["^x-ab"] @@ -1031,7 +1045,7 @@ func TestSchemaExpansion(t *testing.T) { assert.Equal(t, prop, spec.Definitions["tag"]) schema = spec.Definitions["deps"] - s, err = expandSchema(schema, []string{"#/definitions/deps"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/deps", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) prop2 := schema.Dependencies["something"] @@ -1039,7 +1053,7 @@ func TestSchemaExpansion(t *testing.T) { assert.Equal(t, *prop2.Schema, spec.Definitions["tag"]) schema = spec.Definitions["defined"] - s, err = expandSchema(schema, []string{"#/definitions/defined"}, resolver, basePath) + s, err = expandSchema(schema, []string{"%s#/definitions/defined", basePath}, resolver, basePath) schema = *s assert.NoError(t, err) prop = schema.Definitions["something"] diff --git a/fixtures/expansion/circular-minimalst.json b/fixtures/expansion/circular-minimalst.json new file mode 100644 index 0000000..615f89a --- /dev/null +++ b/fixtures/expansion/circular-minimalst.json @@ -0,0 +1,18 @@ +{ + "definitions": { + "a": { + "properties": { + "a_b": { + "$ref": "#/definitions/b" + } + } + }, + "b": { + "properties": { + "b_a": { + "$ref": "#/definitions/a" + } + } + } + } +} diff --git a/schema_loader.go b/schema_loader.go index 02d9966..1ee678c 100644 --- a/schema_loader.go +++ b/schema_loader.go @@ -41,18 +41,11 @@ func init() { // resolverContext allows to share a context during spec processing. // At the moment, it just holds the index of circular references found. type resolverContext struct { - // circulars holds all visited circular references, which allows shortcuts. - // NOTE: this is not just a performance improvement: it is required to figure out - // circular references which participate several cycles. - // This structure is privately instantiated and needs not be locked against - // concurrent access, unless we chose to implement a parallel spec walking. - circulars map[string]bool basePath string } func newResolverContext(originalBasePath string) *resolverContext { return &resolverContext{ - circulars: make(map[string]bool), basePath: originalBasePath, // keep the root base path in context } } @@ -180,16 +173,7 @@ func (r *schemaLoader) load(refURL *url.URL) (interface{}, url.URL, bool, error) // It relies on a private context (which needs not be locked). func (r *schemaLoader) isCircular(ref *Ref, basePath string, parentRefs ...string) (foundCycle bool) { normalizedRef := normalizePaths(ref.String(), basePath) - if _, ok := r.context.circulars[normalizedRef]; ok { - // circular $ref has been already detected in another explored cycle - foundCycle = true - return - } - foundCycle = swag.ContainsStringsCI(parentRefs, normalizedRef) - if foundCycle { - r.context.circulars[normalizedRef] = true - } - return + return swag.ContainsStringsCI(parentRefs, normalizedRef) } // Resolve resolves a reference against basePath and stores the result in target @@ -266,8 +250,14 @@ func defaultSchemaLoader( if context == nil { context = newResolverContext(absBase) } + + newRoot, err := deepCopyRoot(root) + if err != nil { + return nil, err + } + return &schemaLoader{ - root: root, + root: newRoot, options: expandOptions, cache: cache, context: context, @@ -277,3 +267,34 @@ func defaultSchemaLoader( }, }, nil } + +func deepCopyRoot(root interface{}) (interface{}, error) { + var newRoot interface{} + switch root.(type) { + case *Swagger: + var tmp Swagger + newRoot = tmp + case *Schema: + var tmp Schema + newRoot = tmp + case *Parameter: + var tmp Parameter + newRoot = tmp + case *Response: + var tmp Response + newRoot = tmp + case *PathItem: + var tmp PathItem + newRoot = tmp + } + + bytes, err := json.Marshal(root) + if err != nil { + return nil, fmt.Errorf("unable to marshal root: %s", err) + } + err = json.Unmarshal(bytes, &newRoot) + if err != nil { + return nil, fmt.Errorf("unable to unmarshal into new root: %s", err) + } + return newRoot, nil +}