From ed00e7169ddfe4d5d2d6c668d81f54867bc24047 Mon Sep 17 00:00:00 2001 From: fredbi Date: Tue, 9 Jan 2024 08:23:18 +0100 Subject: [PATCH] fix(expand): parameters & responses should properly follow remote doc resolution when SKipSchema (#183) * fix(expand): parameters & responses should properly follow remote doc resolution when SKipSchema * fixes #182 * contributes go-swagger/go-swagger#2743 Signed-off-by: Frederic BIDON * fixed assertion in test for windows paths Signed-off-by: Frederic BIDON --------- Signed-off-by: Frederic BIDON --- expander.go | 56 +++++++++++-------- fixtures/bugs/2743/not-working/minimal.yaml | 7 +++ fixtures/bugs/2743/not-working/spec.yaml | 11 ++++ .../2743/not-working/swagger/definitions.yml | 9 +++ .../bugs/2743/not-working/swagger/items.yml | 15 +++++ .../2743/not-working/swagger/paths/bar.yml | 8 +++ .../2743/not-working/swagger/paths/foo.yml | 8 +++ .../2743/not-working/swagger/paths/index.yml | 6 ++ .../2743/not-working/swagger/paths/nested.yml | 14 +++++ .../2743/not-working/swagger/user/index.yml | 2 + .../2743/not-working/swagger/user/model.yml | 4 ++ fixtures/bugs/2743/working/spec.yaml | 11 ++++ .../bugs/2743/working/swagger/definitions.yml | 9 +++ fixtures/bugs/2743/working/swagger/items.yml | 15 +++++ .../bugs/2743/working/swagger/paths/bar.yml | 8 +++ .../bugs/2743/working/swagger/paths/foo.yml | 8 +++ .../bugs/2743/working/swagger/paths/index.yml | 6 ++ .../2743/working/swagger/paths/nested.yml | 14 +++++ .../bugs/2743/working/swagger/user/index.yml | 2 + .../bugs/2743/working/swagger/user/model.yml | 4 ++ spec_test.go | 32 +++++++++++ 21 files changed, 225 insertions(+), 24 deletions(-) create mode 100644 fixtures/bugs/2743/not-working/minimal.yaml create mode 100644 fixtures/bugs/2743/not-working/spec.yaml create mode 100644 fixtures/bugs/2743/not-working/swagger/definitions.yml create mode 100644 fixtures/bugs/2743/not-working/swagger/items.yml create mode 100644 fixtures/bugs/2743/not-working/swagger/paths/bar.yml create mode 100644 fixtures/bugs/2743/not-working/swagger/paths/foo.yml create mode 100644 fixtures/bugs/2743/not-working/swagger/paths/index.yml create mode 100644 fixtures/bugs/2743/not-working/swagger/paths/nested.yml create mode 100644 fixtures/bugs/2743/not-working/swagger/user/index.yml create mode 100644 fixtures/bugs/2743/not-working/swagger/user/model.yml create mode 100644 fixtures/bugs/2743/working/spec.yaml create mode 100644 fixtures/bugs/2743/working/swagger/definitions.yml create mode 100644 fixtures/bugs/2743/working/swagger/items.yml create mode 100644 fixtures/bugs/2743/working/swagger/paths/bar.yml create mode 100644 fixtures/bugs/2743/working/swagger/paths/foo.yml create mode 100644 fixtures/bugs/2743/working/swagger/paths/index.yml create mode 100644 fixtures/bugs/2743/working/swagger/paths/nested.yml create mode 100644 fixtures/bugs/2743/working/swagger/user/index.yml create mode 100644 fixtures/bugs/2743/working/swagger/user/model.yml diff --git a/expander.go b/expander.go index fae9c12..74f91c3 100644 --- a/expander.go +++ b/expander.go @@ -213,7 +213,19 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba } if target.Ref.String() != "" { - return expandSchemaRef(target, parentRefs, resolver, basePath) + if !resolver.options.SkipSchemas { + return expandSchemaRef(target, parentRefs, resolver, basePath) + } + + // when "expand" with SkipSchema, we just rebase the existing $ref without replacing + // the full schema. + rebasedRef, err := NewRef(normalizeURI(target.Ref.String(), basePath)) + if err != nil { + return nil, err + } + target.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID) + + return &target, nil } for k := range target.Definitions { @@ -525,21 +537,25 @@ func getRefAndSchema(input interface{}) (*Ref, *Schema, error) { } func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePath string) error { - ref, _, err := getRefAndSchema(input) + ref, sch, err := getRefAndSchema(input) if err != nil { return err } - if ref == nil { + if ref == nil && sch == nil { // nothing to do return nil } parentRefs := make([]string, 0, 10) - if err = resolver.deref(input, parentRefs, basePath); resolver.shouldStopOnError(err) { - return err + if ref != nil { + // dereference this $ref + if err = resolver.deref(input, parentRefs, basePath); resolver.shouldStopOnError(err) { + return err + } + + ref, sch, _ = getRefAndSchema(input) } - ref, sch, _ := getRefAndSchema(input) if ref.String() != "" { transitiveResolver := resolver.transitiveResolver(basePath, *ref) basePath = resolver.updateBasePath(transitiveResolver, basePath) @@ -551,6 +567,7 @@ func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePa if ref != nil { *ref = Ref{} } + return nil } @@ -560,38 +577,29 @@ func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePa return ern } - switch { - case resolver.isCircular(&rebasedRef, basePath, parentRefs...): + if resolver.isCircular(&rebasedRef, basePath, parentRefs...) { // this is a circular $ref: stop expansion if !resolver.options.AbsoluteCircularRef { sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID) } else { sch.Ref = rebasedRef } - case !resolver.options.SkipSchemas: - // schema expanded to a $ref in another root - sch.Ref = rebasedRef - debugLog("rebased to: %s", sch.Ref.String()) - default: - // skip schema expansion but rebase $ref to schema - sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID) } } + // $ref expansion or rebasing is performed by expandSchema below if ref != nil { *ref = Ref{} } // expand schema - if !resolver.options.SkipSchemas { - s, err := expandSchema(*sch, parentRefs, resolver, basePath) - if resolver.shouldStopOnError(err) { - return err - } - if s == nil { - // guard for when continuing on error - return nil - } + // yes, we do it even if options.SkipSchema is true: we have to go down that rabbit hole and rebase nested $ref) + s, err := expandSchema(*sch, parentRefs, resolver, basePath) + if resolver.shouldStopOnError(err) { + return err + } + + if s != nil { // guard for when continuing on error *sch = *s } diff --git a/fixtures/bugs/2743/not-working/minimal.yaml b/fixtures/bugs/2743/not-working/minimal.yaml new file mode 100644 index 0000000..2c46773 --- /dev/null +++ b/fixtures/bugs/2743/not-working/minimal.yaml @@ -0,0 +1,7 @@ +swagger: '2.0' +info: + version: 0.0.0 + title: Simple API +paths: + /bar: + $ref: 'swagger/paths/bar.yml' diff --git a/fixtures/bugs/2743/not-working/spec.yaml b/fixtures/bugs/2743/not-working/spec.yaml new file mode 100644 index 0000000..9255173 --- /dev/null +++ b/fixtures/bugs/2743/not-working/spec.yaml @@ -0,0 +1,11 @@ +swagger: '2.0' +info: + version: 0.0.0 + title: Simple API +paths: + /foo: + $ref: 'swagger/paths/foo.yml' + /bar: + $ref: 'swagger/paths/bar.yml' + /nested: + $ref: 'swagger/paths/nested.yml#/response' diff --git a/fixtures/bugs/2743/not-working/swagger/definitions.yml b/fixtures/bugs/2743/not-working/swagger/definitions.yml new file mode 100644 index 0000000..438d6ee --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/definitions.yml @@ -0,0 +1,9 @@ +ErrorPayload: + title: Error Payload + required: + - errors + properties: + errors: + type: array + items: + $ref: './items.yml#/ErrorDetailsItem' \ No newline at end of file diff --git a/fixtures/bugs/2743/not-working/swagger/items.yml b/fixtures/bugs/2743/not-working/swagger/items.yml new file mode 100644 index 0000000..e4a050c --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/items.yml @@ -0,0 +1,15 @@ +ErrorDetailsItem: + title: Error details item + description: Represents an item of the list of details of an error. + required: + - message + - code + properties: + message: + type: string + code: + type: string + details: + type: array + items: + type: string \ No newline at end of file diff --git a/fixtures/bugs/2743/not-working/swagger/paths/bar.yml b/fixtures/bugs/2743/not-working/swagger/paths/bar.yml new file mode 100644 index 0000000..c813579 --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/paths/bar.yml @@ -0,0 +1,8 @@ +get: + responses: + 200: + description: OK + schema: + type: array + items: + $ref: '../user/index.yml#/User' ## this doesn't work \ No newline at end of file diff --git a/fixtures/bugs/2743/not-working/swagger/paths/foo.yml b/fixtures/bugs/2743/not-working/swagger/paths/foo.yml new file mode 100644 index 0000000..a5c0de1 --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/paths/foo.yml @@ -0,0 +1,8 @@ +get: + responses: + 200: + description: OK + 500: + description: OK + schema: + $ref: '../definitions.yml#/ErrorPayload' diff --git a/fixtures/bugs/2743/not-working/swagger/paths/index.yml b/fixtures/bugs/2743/not-working/swagger/paths/index.yml new file mode 100644 index 0000000..7cdcdd4 --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/paths/index.yml @@ -0,0 +1,6 @@ +/foo: + $ref: ./foo.yml +/bar: + $ref: ./bar.yml +/nested: + $ref: ./nested.yml#/response diff --git a/fixtures/bugs/2743/not-working/swagger/paths/nested.yml b/fixtures/bugs/2743/not-working/swagger/paths/nested.yml new file mode 100644 index 0000000..5c7cf0d --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/paths/nested.yml @@ -0,0 +1,14 @@ +response: + get: + responses: + 200: + description: OK + schema: + $ref: '#/definitions/SameFileReference' + +definitions: + SameFileReference: + type: object + properties: + name: + type: string diff --git a/fixtures/bugs/2743/not-working/swagger/user/index.yml b/fixtures/bugs/2743/not-working/swagger/user/index.yml new file mode 100644 index 0000000..99c1c6f --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/user/index.yml @@ -0,0 +1,2 @@ +User: + $ref: './model.yml' diff --git a/fixtures/bugs/2743/not-working/swagger/user/model.yml b/fixtures/bugs/2743/not-working/swagger/user/model.yml new file mode 100644 index 0000000..5cb91cd --- /dev/null +++ b/fixtures/bugs/2743/not-working/swagger/user/model.yml @@ -0,0 +1,4 @@ +type: object +properties: + name: + type: string diff --git a/fixtures/bugs/2743/working/spec.yaml b/fixtures/bugs/2743/working/spec.yaml new file mode 100644 index 0000000..9255173 --- /dev/null +++ b/fixtures/bugs/2743/working/spec.yaml @@ -0,0 +1,11 @@ +swagger: '2.0' +info: + version: 0.0.0 + title: Simple API +paths: + /foo: + $ref: 'swagger/paths/foo.yml' + /bar: + $ref: 'swagger/paths/bar.yml' + /nested: + $ref: 'swagger/paths/nested.yml#/response' diff --git a/fixtures/bugs/2743/working/swagger/definitions.yml b/fixtures/bugs/2743/working/swagger/definitions.yml new file mode 100644 index 0000000..438d6ee --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/definitions.yml @@ -0,0 +1,9 @@ +ErrorPayload: + title: Error Payload + required: + - errors + properties: + errors: + type: array + items: + $ref: './items.yml#/ErrorDetailsItem' \ No newline at end of file diff --git a/fixtures/bugs/2743/working/swagger/items.yml b/fixtures/bugs/2743/working/swagger/items.yml new file mode 100644 index 0000000..e4a050c --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/items.yml @@ -0,0 +1,15 @@ +ErrorDetailsItem: + title: Error details item + description: Represents an item of the list of details of an error. + required: + - message + - code + properties: + message: + type: string + code: + type: string + details: + type: array + items: + type: string \ No newline at end of file diff --git a/fixtures/bugs/2743/working/swagger/paths/bar.yml b/fixtures/bugs/2743/working/swagger/paths/bar.yml new file mode 100644 index 0000000..41dc76a --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/paths/bar.yml @@ -0,0 +1,8 @@ +get: + responses: + 200: + description: OK + schema: + type: array + items: + $ref: './swagger/user/index.yml#/User' ## this works \ No newline at end of file diff --git a/fixtures/bugs/2743/working/swagger/paths/foo.yml b/fixtures/bugs/2743/working/swagger/paths/foo.yml new file mode 100644 index 0000000..a5c0de1 --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/paths/foo.yml @@ -0,0 +1,8 @@ +get: + responses: + 200: + description: OK + 500: + description: OK + schema: + $ref: '../definitions.yml#/ErrorPayload' diff --git a/fixtures/bugs/2743/working/swagger/paths/index.yml b/fixtures/bugs/2743/working/swagger/paths/index.yml new file mode 100644 index 0000000..7cdcdd4 --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/paths/index.yml @@ -0,0 +1,6 @@ +/foo: + $ref: ./foo.yml +/bar: + $ref: ./bar.yml +/nested: + $ref: ./nested.yml#/response diff --git a/fixtures/bugs/2743/working/swagger/paths/nested.yml b/fixtures/bugs/2743/working/swagger/paths/nested.yml new file mode 100644 index 0000000..5c7cf0d --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/paths/nested.yml @@ -0,0 +1,14 @@ +response: + get: + responses: + 200: + description: OK + schema: + $ref: '#/definitions/SameFileReference' + +definitions: + SameFileReference: + type: object + properties: + name: + type: string diff --git a/fixtures/bugs/2743/working/swagger/user/index.yml b/fixtures/bugs/2743/working/swagger/user/index.yml new file mode 100644 index 0000000..99c1c6f --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/user/index.yml @@ -0,0 +1,2 @@ +User: + $ref: './model.yml' diff --git a/fixtures/bugs/2743/working/swagger/user/model.yml b/fixtures/bugs/2743/working/swagger/user/model.yml new file mode 100644 index 0000000..5cb91cd --- /dev/null +++ b/fixtures/bugs/2743/working/swagger/user/model.yml @@ -0,0 +1,4 @@ +type: object +properties: + name: + type: string diff --git a/spec_test.go b/spec_test.go index 8274c95..ae7d492 100644 --- a/spec_test.go +++ b/spec_test.go @@ -25,6 +25,38 @@ import ( ) // Test unitary fixture for dev and bug fixing + +func TestSpec_Issue2743(t *testing.T) { + t.Run("should expand but produce unresolvable $ref", func(t *testing.T) { + path := filepath.Join("fixtures", "bugs", "2743", "working", "spec.yaml") + sp := loadOrFail(t, path) + require.NoError(t, + spec.ExpandSpec(sp, &spec.ExpandOptions{RelativeBase: path, SkipSchemas: true, PathLoader: testLoader}), + ) + + t.Run("all $ref do not resolve when expanding again", func(t *testing.T) { + err := spec.ExpandSpec(sp, &spec.ExpandOptions{RelativeBase: path, SkipSchemas: false, PathLoader: testLoader}) + require.Error(t, err) + require.ErrorContains(t, err, filepath.FromSlash("swagger/paths/swagger/user/index.yml")) + }) + }) + + t.Run("should expand and produce resolvable $ref", func(t *testing.T) { + path := filepath.Join("fixtures", "bugs", "2743", "not-working", "spec.yaml") + sp := loadOrFail(t, path) + require.NoError(t, + spec.ExpandSpec(sp, &spec.ExpandOptions{RelativeBase: path, SkipSchemas: true, PathLoader: testLoader}), + ) + + t.Run("all $ref properly reolve when expanding again", func(t *testing.T) { + require.NoError(t, + spec.ExpandSpec(sp, &spec.ExpandOptions{RelativeBase: path, SkipSchemas: false, PathLoader: testLoader}), + ) + require.NotContainsf(t, asJSON(t, sp), "$ref", "all $ref's should have been expanded properly") + }) + }) +} + func TestSpec_Issue1429(t *testing.T) { path := filepath.Join("fixtures", "bugs", "1429", "swagger.yaml")