From 9dac81781a162e02f72bf77fc9badf31edecb3fa Mon Sep 17 00:00:00 2001 From: fredbi Date: Tue, 29 Dec 2020 18:03:24 +0100 Subject: [PATCH] refactored expander and ref resolver for readability (#135) [NO CHANGE IN FUNCTIONALITY] * refactored associated units tests for readability * reshuffled resolver funcs and associated tests to a dedicated file * removed panic() in ResolveRef (now return error) [used by go-swagger] Signed-off-by: Frederic BIDON --- cache_test.go | 31 ++ errors.go | 18 + expander.go | 291 ++++------- expander_test.go | 1152 +++++++++++--------------------------------- go.mod | 2 +- go.sum | 11 +- helpers_test.go | 132 +++++ normalizer.go | 2 - normalizer_test.go | 84 ++++ resolver.go | 131 +++++ resolver_test.go | 451 +++++++++++++++++ schema_loader.go | 54 +-- 12 files changed, 1243 insertions(+), 1116 deletions(-) create mode 100644 cache_test.go create mode 100644 errors.go create mode 100644 helpers_test.go create mode 100644 normalizer_test.go create mode 100644 resolver.go create mode 100644 resolver_test.go diff --git a/cache_test.go b/cache_test.go new file mode 100644 index 0000000..8d6d274 --- /dev/null +++ b/cache_test.go @@ -0,0 +1,31 @@ +package spec + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDefaultResolutionCache(t *testing.T) { + jsonSchema := MustLoadJSONSchemaDraft04() + swaggerSchema := MustLoadSwagger20Schema() + + cache := defaultResolutionCache() + + sch, ok := cache.Get("not there") + assert.False(t, ok) + assert.Nil(t, sch) + + sch, ok = cache.Get("http://swagger.io/v2/schema.json") + assert.True(t, ok) + assert.Equal(t, swaggerSchema, sch) + + sch, ok = cache.Get("http://json-schema.org/draft-04/schema") + assert.True(t, ok) + assert.Equal(t, jsonSchema, sch) + + cache.Set("something", "here") + sch, ok = cache.Get("something") + assert.True(t, ok) + assert.Equal(t, "here", sch) +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..10a693a --- /dev/null +++ b/errors.go @@ -0,0 +1,18 @@ +package spec + +import "errors" + +var ( + // ErrUnknownTypeForReference indicates that a resolved reference was found in an unsupported container type + ErrUnknownTypeForReference = errors.New("unknown type for the resolved reference") + + // ErrResolveRefNeedsAPointer indicates that a $ref target must be a valid JSON pointer + ErrResolveRefNeedsAPointer = errors.New("resolve ref: target needs to be a pointer") + + // ErrDerefUnsupportedType indicates that a resolved reference was found in an unsupported container type. + // At the moment, $ref are supported only inside: schemas, parameters, responses, path items + ErrDerefUnsupportedType = errors.New("deref: unsupported type") + + // ErrExpandUnsupportedType indicates that $ref expansion is attempted on some invalid type + ErrExpandUnsupportedType = errors.New("expand: unsupported type. Input should be of type *Parameter or *Response") +) diff --git a/expander.go b/expander.go index a44cb0b..3cd39cd 100644 --- a/expander.go +++ b/expander.go @@ -29,137 +29,6 @@ type ExpandOptions struct { PathLoader func(string) (json.RawMessage, error) `json:"-"` } -// ResolveRefWithBase resolves a reference against a context root with preservation of base path -func ResolveRefWithBase(root interface{}, ref *Ref, opts *ExpandOptions) (*Schema, error) { - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return nil, err - } - - specBasePath := "" - if opts != nil && opts.RelativeBase != "" { - specBasePath, _ = absPath(opts.RelativeBase) - } - - result := new(Schema) - if err := resolver.Resolve(ref, result, specBasePath); err != nil { - return nil, err - } - return result, nil -} - -// ResolveRef resolves a reference against a context root -// ref is guaranteed to be in root (no need to go to external files) -// ResolveRef is ONLY called from the code generation module -func ResolveRef(root interface{}, ref *Ref) (*Schema, error) { - res, _, err := ref.GetPointer().Get(root) - if err != nil { - panic(err) - } - switch sch := res.(type) { - case Schema: - return &sch, nil - case *Schema: - return sch, nil - case map[string]interface{}: - b, _ := json.Marshal(sch) - newSch := new(Schema) - _ = json.Unmarshal(b, newSch) - return newSch, nil - default: - return nil, fmt.Errorf("unknown type for the resolved reference") - } -} - -// ResolveParameter resolves a parameter reference against a context root -func ResolveParameter(root interface{}, ref Ref) (*Parameter, error) { - return ResolveParameterWithBase(root, ref, nil) -} - -// ResolveParameterWithBase resolves a parameter reference against a context root and base path -func ResolveParameterWithBase(root interface{}, ref Ref, opts *ExpandOptions) (*Parameter, error) { - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return nil, err - } - - specBasePath := "" - if opts != nil && opts.RelativeBase != "" { - specBasePath, _ = absPath(opts.RelativeBase) - } - - result := new(Parameter) - if err := resolver.Resolve(&ref, result, specBasePath); err != nil { - return nil, err - } - return result, nil -} - -// ResolveResponse resolves response a reference against a context root -func ResolveResponse(root interface{}, ref Ref) (*Response, error) { - return ResolveResponseWithBase(root, ref, nil) -} - -// ResolveResponseWithBase resolves response a reference against a context root and base path -func ResolveResponseWithBase(root interface{}, ref Ref, opts *ExpandOptions) (*Response, error) { - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return nil, err - } - - specBasePath := "" - if opts != nil && opts.RelativeBase != "" { - specBasePath, _ = absPath(opts.RelativeBase) - } - - result := new(Response) - if err := resolver.Resolve(&ref, result, specBasePath); err != nil { - return nil, err - } - return result, nil -} - -// ResolveItems resolves parameter items reference against a context root and base path. -// -// NOTE: stricly speaking, this construct is not supported by Swagger 2.0. -// Similarly, $ref are forbidden in response headers. -func ResolveItems(root interface{}, ref Ref, opts *ExpandOptions) (*Items, error) { - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return nil, err - } - - basePath := "" - if opts.RelativeBase != "" { - basePath = opts.RelativeBase - } - - result := new(Items) - if err := resolver.Resolve(&ref, result, basePath); err != nil { - return nil, err - } - return result, nil -} - -// ResolvePathItem resolves response a path item against a context root and base path -func ResolvePathItem(root interface{}, ref Ref, opts *ExpandOptions) (*PathItem, error) { - resolver, err := defaultSchemaLoader(root, opts, nil, nil) - if err != nil { - return nil, err - } - - basePath := "" - if opts.RelativeBase != "" { - basePath = opts.RelativeBase - } - - result := new(PathItem) - if err := resolver.Resolve(&ref, result, basePath); err != nil { - return nil, err - } - return result, nil -} - // ExpandSpec expands the references in a swagger spec func ExpandSpec(spec *Swagger, options *ExpandOptions) error { resolver, err := defaultSchemaLoader(spec, options, nil, nil) @@ -251,6 +120,7 @@ func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error // when no base path is specified, remaining $ref (circular) are rendered with an absolute path AbsoluteCircularRef: false, } + return ExpandSchemaWithBasePath(schema, cache, opts) } @@ -284,37 +154,41 @@ func ExpandSchemaWithBasePath(schema *Schema, cache ResolutionCache, opts *Expan } func expandItems(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { - if target.Items != nil { - if target.Items.Schema != nil { - t, err := expandSchema(*target.Items.Schema, parentRefs, resolver, basePath) - if err != nil { - return nil, err - } - *target.Items.Schema = *t + if target.Items == nil { + return &target, nil + } + + // array + if target.Items.Schema != nil { + t, err := expandSchema(*target.Items.Schema, parentRefs, resolver, basePath) + if err != nil { + return nil, err } - for i := range target.Items.Schemas { - t, err := expandSchema(target.Items.Schemas[i], parentRefs, resolver, basePath) - if err != nil { - return nil, err - } - target.Items.Schemas[i] = *t + *target.Items.Schema = *t + } + + // tuple + for i := range target.Items.Schemas { + t, err := expandSchema(target.Items.Schemas[i], parentRefs, resolver, basePath) + if err != nil { + return nil, err } + target.Items.Schemas[i] = *t } + return &target, nil } func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { if target.Ref.String() == "" && target.Ref.IsRoot() { - // normalizing is important newRef := normalizeFileRef(&target.Ref, basePath) target.Ref = *newRef return &target, nil - } // change the base path of resolution when an ID is encountered // otherwise the basePath should inherit the parent's - // important: ID can be relative path + // important: ID can be a relative path if target.ID != "" { debugLog("schema has ID: %s", target.ID) // handling the case when id is a folder @@ -330,45 +204,8 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba resolver.cache.Set(basePath, target) } - var t *Schema - // if Ref is found, everything else doesn't matter - // Ref also changes the resolution scope of children expandSchema if target.Ref.String() != "" { - // here the resolution scope is changed because a $ref was encountered - normalizedRef := normalizeFileRef(&target.Ref, basePath) - normalizedBasePath := normalizedRef.RemoteURI() - - if resolver.isCircular(normalizedRef, basePath, parentRefs...) { - // this means there is a cycle in the recursion tree: return the Ref - // - circular refs cannot be expanded. We leave them as ref. - // - denormalization means that a new local file ref is set relative to the original basePath - debugLog("short circuit circular ref: basePath: %s, normalizedPath: %s, normalized ref: %s", - basePath, normalizedBasePath, normalizedRef.String()) - if !resolver.options.AbsoluteCircularRef { - target.Ref = *denormalizeFileRef(normalizedRef, normalizedBasePath, resolver.context.basePath) - } else { - target.Ref = *normalizedRef - } - return &target, nil - } - - debugLog("basePath: %s: calling Resolve with target: %s", basePath, target.Ref.String()) - if err := resolver.Resolve(&target.Ref, &t, basePath); resolver.shouldStopOnError(err) { - return nil, err - } - - if t != nil { - parentRefs = append(parentRefs, normalizedRef.String()) - var err error - transitiveResolver, err := resolver.transitiveResolver(basePath, target.Ref) - if transitiveResolver.shouldStopOnError(err) { - return nil, err - } - - basePath = resolver.updateBasePath(transitiveResolver, normalizedBasePath) - - return expandSchema(*t, parentRefs, transitiveResolver, basePath) - } + return expandSchemaRef(target, parentRefs, resolver, basePath) } for k := range target.Definitions { @@ -396,6 +233,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba } target.AllOf[i] = *t } + for i := range target.AnyOf { t, err := expandSchema(target.AnyOf[i], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -403,6 +241,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba } target.AnyOf[i] = *t } + for i := range target.OneOf { t, err := expandSchema(target.OneOf[i], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -412,6 +251,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba target.OneOf[i] = *t } } + if target.Not != nil { t, err := expandSchema(*target.Not, parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -421,6 +261,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba *target.Not = *t } } + for k := range target.Properties { t, err := expandSchema(target.Properties[k], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -430,6 +271,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba target.Properties[k] = *t } } + if target.AdditionalProperties != nil && target.AdditionalProperties.Schema != nil { t, err := expandSchema(*target.AdditionalProperties.Schema, parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -439,6 +281,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba *target.AdditionalProperties.Schema = *t } } + for k := range target.PatternProperties { t, err := expandSchema(target.PatternProperties[k], parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -448,6 +291,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba target.PatternProperties[k] = *t } } + for k := range target.Dependencies { if target.Dependencies[k].Schema != nil { t, err := expandSchema(*target.Dependencies[k].Schema, parentRefs, resolver, basePath) @@ -459,6 +303,7 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba } } } + if target.AdditionalItems != nil && target.AdditionalItems.Schema != nil { t, err := expandSchema(*target.AdditionalItems.Schema, parentRefs, resolver, basePath) if resolver.shouldStopOnError(err) { @@ -471,6 +316,50 @@ func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, ba return &target, nil } +func expandSchemaRef(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) { + // if a Ref is found, all sibling fields are skipped + // Ref also changes the resolution scope of children expandSchema + + // here the resolution scope is changed because a $ref was encountered + normalizedRef := normalizeFileRef(&target.Ref, basePath) + normalizedBasePath := normalizedRef.RemoteURI() + + if resolver.isCircular(normalizedRef, basePath, parentRefs...) { + // this means there is a cycle in the recursion tree: return the Ref + // - circular refs cannot be expanded. We leave them as ref. + // - denormalization means that a new local file ref is set relative to the original basePath + debugLog("short circuit circular ref: basePath: %s, normalizedPath: %s, normalized ref: %s", + basePath, normalizedBasePath, normalizedRef.String()) + if !resolver.options.AbsoluteCircularRef { + target.Ref = *denormalizeFileRef(normalizedRef, normalizedBasePath, resolver.context.basePath) + } else { + target.Ref = *normalizedRef + } + return &target, nil + } + + var t *Schema + err := resolver.Resolve(&target.Ref, &t, basePath) + if resolver.shouldStopOnError(err) { + return nil, err + } + + if t == nil { + // guard for when continuing on error + return &target, nil + } + + parentRefs = append(parentRefs, normalizedRef.String()) + transitiveResolver, err := resolver.transitiveResolver(basePath, target.Ref) + if transitiveResolver.shouldStopOnError(err) { + return nil, err + } + + basePath = resolver.updateBasePath(transitiveResolver, normalizedBasePath) + + return expandSchema(*t, parentRefs, transitiveResolver, basePath) +} + func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) error { if pathItem == nil { return nil @@ -480,6 +369,7 @@ func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) if err := resolver.deref(pathItem, parentRefs, basePath); resolver.shouldStopOnError(err) { return err } + if pathItem.Ref.String() != "" { transitiveResolver, err := resolver.transitiveResolver(basePath, pathItem.Ref) if transitiveResolver.shouldStopOnError(err) { @@ -488,13 +378,14 @@ func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) basePath = transitiveResolver.updateBasePath(resolver, basePath) resolver = transitiveResolver } - pathItem.Ref = Ref{} + pathItem.Ref = Ref{} for idx := range pathItem.Parameters { if err := expandParameterOrResponse(&(pathItem.Parameters[idx]), resolver, basePath); resolver.shouldStopOnError(err) { return err } } + ops := []*Operation{ pathItem.Get, pathItem.Head, @@ -509,6 +400,7 @@ func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) return err } } + return nil } @@ -525,19 +417,23 @@ func expandOperation(op *Operation, resolver *schemaLoader, basePath string) err op.Parameters[i] = param } - if op.Responses != nil { - responses := op.Responses - if err := expandParameterOrResponse(responses.Default, resolver, basePath); resolver.shouldStopOnError(err) { + if op.Responses == nil { + return nil + } + + responses := op.Responses + if err := expandParameterOrResponse(responses.Default, resolver, basePath); resolver.shouldStopOnError(err) { + return err + } + + for code := range responses.StatusCodeResponses { + response := responses.StatusCodeResponses[code] + if err := expandParameterOrResponse(&response, resolver, basePath); resolver.shouldStopOnError(err) { return err } - for code := range responses.StatusCodeResponses { - response := responses.StatusCodeResponses[code] - if err := expandParameterOrResponse(&response, resolver, basePath); resolver.shouldStopOnError(err) { - return err - } - responses.StatusCodeResponses[code] = response - } + responses.StatusCodeResponses[code] = response } + return nil } @@ -634,8 +530,9 @@ func getRefAndSchema(input interface{}) (*Ref, *Schema, error) { ref = &refable.Ref sch = refable.Schema default: - return nil, nil, fmt.Errorf("expand: unsupported type %T. Input should be of type *Parameter or *Response", input) + return nil, nil, fmt.Errorf("unsupported type: %T: %w", input, ErrExpandUnsupportedType) } + return ref, sch, nil } diff --git a/expander_test.go b/expander_test.go index 4ebb9a9..d9e921a 100644 --- a/expander_test.go +++ b/expander_test.go @@ -23,106 +23,29 @@ import ( "os" "path/filepath" "regexp" - "runtime" "strings" "testing" "time" - "github.com/go-openapi/jsonpointer" - "github.com/go-openapi/swag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var specs = filepath.Join("fixtures", "specs") +const ( + crossFileRefFixture = "fixtures/expansion/crossFileRef.json" + withoutSchemaID = "removed" + withSchemaID = "schema" + pathItemsFixture = "fixtures/expansion/pathItems.json" + extraRefFixture = "fixtures/expansion/extraRef.json" +) var ( - rex = regexp.MustCompile(`"\$ref":\s*"(.+)"`) + // PetStoreJSONMessage json raw message for Petstore20 + PetStoreJSONMessage = json.RawMessage([]byte(PetStore20)) + specs = filepath.Join("fixtures", "specs") + rex = regexp.MustCompile(`"\$ref":\s*"(.+)"`) ) -func jsonDoc(path string) (json.RawMessage, error) { - data, err := swag.LoadFromFileOrHTTP(path) - if err != nil { - return nil, err - } - return json.RawMessage(data), nil -} - -// tests that paths are normalized correctly -func TestNormalizePaths(t *testing.T) { - type testNormalizePathsTestCases []struct { - refPath string - base string - expOutput string - } - testCases := func() testNormalizePathsTestCases { - testCases := testNormalizePathsTestCases{ - { - // http basePath, absolute refPath - refPath: "http://www.anotherexample.com/another/base/path/swagger.json#/definitions/Pet", - base: "http://www.example.com/base/path/swagger.json", - expOutput: "http://www.anotherexample.com/another/base/path/swagger.json#/definitions/Pet", - }, - { - // http basePath, relative refPath - refPath: "another/base/path/swagger.json#/definitions/Pet", - base: "http://www.example.com/base/path/swagger.json", - expOutput: "http://www.example.com/base/path/another/base/path/swagger.json#/definitions/Pet", - }, - } - if runtime.GOOS == "windows" { - testCases = append(testCases, testNormalizePathsTestCases{ - { - // file basePath, absolute refPath, no fragment - refPath: `C:\another\base\path.json`, - base: `C:\base\path.json`, - expOutput: `c:\another\base\path.json`, - }, - { - // file basePath, absolute refPath - refPath: `C:\another\base\path.json#/definitions/Pet`, - base: `C:\base\path.json`, - expOutput: `c:\another\base\path.json#/definitions/Pet`, - }, - { - // file basePath, relative refPath - refPath: `another\base\path.json#/definitions/Pet`, - base: `C:\base\path.json`, - expOutput: `c:\base\another\base\path.json#/definitions/Pet`, - }, - }...) - return testCases - } - // linux case - testCases = append(testCases, testNormalizePathsTestCases{ - { - // file basePath, absolute refPath, no fragment - refPath: "/another/base/path.json", - base: "/base/path.json", - expOutput: "/another/base/path.json", - }, - { - // file basePath, absolute refPath - refPath: "/another/base/path.json#/definitions/Pet", - base: "/base/path.json", - expOutput: "/another/base/path.json#/definitions/Pet", - }, - { - // file basePath, relative refPath - refPath: "another/base/path.json#/definitions/Pet", - base: "/base/path.json", - expOutput: "/base/another/base/path.json#/definitions/Pet", - }, - }...) - return testCases - }() - - for _, tcase := range testCases { - out := normalizePaths(tcase.refPath, tcase.base) - assert.Equal(t, tcase.expOutput, out) - } -} - func TestExpandsKnownRef(t *testing.T) { schema := RefProperty("http://json-schema.org/draft-04/schema#") if assert.NoError(t, ExpandSchema(schema, nil, nil)) { @@ -152,11 +75,10 @@ func TestExpandResponseSchema(t *testing.T) { func TestSpecExpansion(t *testing.T) { spec := new(Swagger) - err := ExpandSpec(spec, nil) - assert.NoError(t, err) + require.NoError(t, ExpandSpec(spec, nil)) specDoc, err := jsonDoc("fixtures/expansion/all-the-things.json") - assert.NoError(t, err) + require.NoError(t, err) specPath, _ := absPath("fixtures/expansion/all-the-things.json") opts := &ExpandOptions{ @@ -164,8 +86,7 @@ func TestSpecExpansion(t *testing.T) { } spec = new(Swagger) - err = json.Unmarshal(specDoc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(specDoc, spec)) pet := spec.Definitions["pet"] errorModel := spec.Definitions["errorModel"] @@ -175,8 +96,7 @@ func TestSpecExpansion(t *testing.T) { tagParam := spec.Parameters["tag"] idParam := spec.Parameters["idParam"] - err = ExpandSpec(spec, opts) - assert.NoError(t, err) + require.NoError(t, ExpandSpec(spec, opts)) assert.Equal(t, tagParam, spec.Parameters["query"]) assert.Equal(t, petResponse, spec.Responses["petResponse"]) @@ -198,46 +118,26 @@ func TestSpecExpansion(t *testing.T) { assert.Equal(t, errorModel, *pi.Delete.Responses.Default.Schema) } -func TestResolveRef(t *testing.T) { - var root interface{} - err := json.Unmarshal([]byte(PetStore20), &root) - assert.NoError(t, err) - ref, err := NewRef("#/definitions/Category") - assert.NoError(t, err) - sch, err := ResolveRef(root, &ref) - assert.NoError(t, err) - b, _ := sch.MarshalJSON() - assert.JSONEq(t, `{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}`, string(b)) - - // WithBase variant - sch, err = ResolveRefWithBase(root, &ref, &ExpandOptions{ - RelativeBase: "/", - }) - assert.NoError(t, err) - b, _ = sch.MarshalJSON() - assert.JSONEq(t, `{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}`, string(b)) -} - func TestResponseExpansion(t *testing.T) { specDoc, err := jsonDoc("fixtures/expansion/all-the-things.json") - assert.NoError(t, err) + require.NoError(t, err) basePath, err := absPath("fixtures/expansion/all-the-things.json") - assert.NoError(t, err) + require.NoError(t, err) spec := new(Swagger) - err = json.Unmarshal(specDoc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(specDoc, spec)) resolver, err := defaultSchemaLoader(spec, nil, nil, nil) - assert.NoError(t, err) + require.NoError(t, err) resp := spec.Responses["anotherPet"] expected := spec.Responses["petResponse"] - err = expandParameterOrResponse(&expected, resolver, basePath) - assert.NoError(t, err) + require.NoError(t, expandParameterOrResponse(&expected, resolver, basePath)) + + jazon, err := json.MarshalIndent(expected, "", " ") + require.NoError(t, err) - jazon, _ := json.MarshalIndent(expected, "", " ") assert.JSONEq(t, `{ "description": "pet response", "schema": { @@ -260,107 +160,57 @@ func TestResponseExpansion(t *testing.T) { } }`, string(jazon)) - err = expandParameterOrResponse(&resp, resolver, basePath) - assert.NoError(t, err) + require.NoError(t, expandParameterOrResponse(&resp, resolver, basePath)) assert.Equal(t, expected, resp) resp2 := spec.Paths.Paths["/"].Get.Responses.Default expected = spec.Responses["stringResponse"] - err = expandParameterOrResponse(resp2, resolver, basePath) - assert.NoError(t, err) + require.NoError(t, expandParameterOrResponse(resp2, resolver, basePath)) assert.Equal(t, expected, *resp2) // cascading ref resp = spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200] expected = spec.Responses["petResponse"] - jazon, _ = json.MarshalIndent(resp, "", " ") + jazon, err = json.MarshalIndent(resp, "", " ") + require.NoError(t, err) + assert.JSONEq(t, `{ "$ref": "#/responses/anotherPet" }`, string(jazon)) - err = expandParameterOrResponse(&resp, resolver, basePath) - assert.NoError(t, err) - // NOTE(fredbi): fixed this testcase in schema_loader.go#227 + require.NoError(t, expandParameterOrResponse(&resp, resolver, basePath)) assert.Equal(t, expected, resp) } -func TestResponseResolve(t *testing.T) { - specDoc, err := jsonDoc("fixtures/expansion/all-the-things.json") - assert.NoError(t, err) - - spec := new(Swagger) - err = json.Unmarshal(specDoc, spec) - assert.NoError(t, err) - - // Resolve with root version - resp := spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200] - resp2, err := ResolveResponse(spec, resp.Ref) - assert.NoError(t, err) - // resolve resolves the ref, but dos not expand - jazon, _ := json.MarshalIndent(resp2, "", " ") - assert.JSONEq(t, `{ - "$ref": "#/responses/petResponse" - }`, string(jazon)) -} - -const crossFileRefFixture = "fixtures/expansion/crossFileRef.json" - -func TestResponseResolveWithBase(t *testing.T) { - specDoc, err := jsonDoc(crossFileRefFixture) - assert.NoError(t, err) - - spec := new(Swagger) - err = json.Unmarshal(specDoc, spec) - assert.NoError(t, err) - - // Resolve with root version - resp := spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200] - resp2, err := ResolveResponseWithBase(spec, resp.Ref, &ExpandOptions{RelativeBase: crossFileRefFixture}) - assert.NoError(t, err) - // resolve resolves the ref, but dos not expand - jazon, _ := json.MarshalIndent(resp2, "", " ") - assert.JSONEq(t, `{ - "$ref": "#/responses/petResponse" - }`, string(jazon)) -} - // test the exported version of ExpandResponse func TestExportedResponseExpansion(t *testing.T) { specDoc, err := jsonDoc("fixtures/expansion/all-the-things.json") - if !assert.NoError(t, err) { - t.FailNow() - } + require.NoError(t, err) basePath, err := absPath("fixtures/expansion/all-the-things.json") - assert.NoError(t, err) + require.NoError(t, err) spec := new(Swagger) - err = json.Unmarshal(specDoc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(specDoc, spec)) resp := spec.Responses["anotherPet"] expected := spec.Responses["petResponse"] - err = ExpandResponse(&expected, basePath) - assert.NoError(t, err) + require.NoError(t, ExpandResponse(&expected, basePath)) - err = ExpandResponse(&resp, basePath) - assert.NoError(t, err) + require.NoError(t, ExpandResponse(&resp, basePath)) assert.Equal(t, expected, resp) resp2 := spec.Paths.Paths["/"].Get.Responses.Default expected = spec.Responses["stringResponse"] - err = ExpandResponse(resp2, basePath) - assert.NoError(t, err) + require.NoError(t, ExpandResponse(resp2, basePath)) assert.Equal(t, expected, *resp2) resp = spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200] expected = spec.Responses["petResponse"] - err = ExpandResponse(&resp, basePath) - assert.NoError(t, err) - // NOTE(fredbi): fixed this testcase in schema_loader.go#227 + require.NoError(t, ExpandResponse(&resp, basePath)) assert.Equal(t, expected, resp) } @@ -369,7 +219,8 @@ func TestExpandResponseAndParamWithRoot(t *testing.T) { require.NoError(t, err) var spec Swagger - _ = json.Unmarshal(specDoc, &spec) + err = json.Unmarshal(specDoc, &spec) + require.NoError(t, err) // check responses with $ref resp := spec.Paths.Paths["/admin/users"].Post.Responses.StatusCodeResponses[201] @@ -415,49 +266,6 @@ func TestExpandResponseWithRoot_CircularRefs(t *testing.T) { require.NoError(t, ExpandResponseWithRoot(&resp, rootDoc, thisCache)) } -func TestResolveParam(t *testing.T) { - specDoc, err := jsonDoc("fixtures/expansion/all-the-things.json") - require.NoError(t, err) - - var spec Swagger - _ = json.Unmarshal(specDoc, &spec) - - param := spec.Paths.Paths["/pets/{id}"].Get.Parameters[0] - par, err := ResolveParameter(spec, param.Ref) - require.NoError(t, err) - - jazon, _ := json.MarshalIndent(par, "", " ") - assert.JSONEq(t, `{ - "name": "id", - "in": "path", - "description": "ID of pet to fetch", - "required": true, - "type": "integer", - "format": "int64" - }`, string(jazon)) -} - -func TestResolveParamWithBase(t *testing.T) { - specDoc, err := jsonDoc(crossFileRefFixture) - require.NoError(t, err) - - var spec Swagger - _ = json.Unmarshal(specDoc, &spec) - - param := spec.Paths.Paths["/pets"].Get.Parameters[0] - par, err := ResolveParameterWithBase(spec, param.Ref, &ExpandOptions{RelativeBase: crossFileRefFixture}) - assert.NoError(t, err) - jazon, _ := json.MarshalIndent(par, "", " ") - assert.JSONEq(t, `{ -"description":"ID of pet to fetch", -"format":"int64", -"in":"path", -"name":"id", -"required":true, -"type":"integer" -}`, string(jazon)) -} - func TestIssue3(t *testing.T) { spec := new(Swagger) specDoc, err := jsonDoc("fixtures/expansion/overflow.json") @@ -509,44 +317,41 @@ func TestExportedParameterExpansion(t *testing.T) { require.NoError(t, err) spec := new(Swagger) - err = json.Unmarshal(paramDoc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(paramDoc, spec)) basePath, err := absPath("fixtures/expansion/params.json") - assert.NoError(t, err) + require.NoError(t, err) param := spec.Parameters["query"] expected := spec.Parameters["tag"] - err = ExpandParameter(¶m, basePath) - assert.NoError(t, err) + require.NoError(t, ExpandParameter(¶m, basePath)) assert.Equal(t, expected, param) param = spec.Paths.Paths["/cars/{id}"].Parameters[0] expected = spec.Parameters["id"] - err = ExpandParameter(¶m, basePath) - assert.NoError(t, err) + require.NoError(t, ExpandParameter(¶m, basePath)) assert.Equal(t, expected, param) } func TestCircularRefsExpansion(t *testing.T) { carsDoc, err := jsonDoc("fixtures/expansion/circularRefs.json") - assert.NoError(t, err) + require.NoError(t, err) basePath, _ := absPath("fixtures/expansion/circularRefs.json") spec := new(Swagger) - err = json.Unmarshal(carsDoc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(carsDoc, spec)) resolver, err := defaultSchemaLoader(spec, &ExpandOptions{RelativeBase: basePath}, nil, nil) - assert.NoError(t, err) + require.NoError(t, err) + schema := spec.Definitions["car"] assert.NotPanics(t, func() { _, err := expandSchema(schema, []string{"#/definitions/car"}, resolver, basePath) - assert.NoError(t, err) + require.NoError(t, err) }, "Calling expand schema with circular refs, should not panic!") } @@ -556,6 +361,7 @@ func TestCircularSpec2Expansion(t *testing.T) { fixturePath := filepath.Join("fixtures", "expansion", "circular-minimal.json") jazon := expandThisOrDieTrying(t, fixturePath) assert.NotEmpty(t, jazon) + // assert stripped $ref in result assert.NotContainsf(t, jazon, "circular-minimal.json#/", "expected %s to be expanded with stripped circular $ref", fixturePath) @@ -563,6 +369,7 @@ func TestCircularSpec2Expansion(t *testing.T) { fixturePath = "fixtures/expansion/circularSpec2.json" jazon = expandThisOrDieTrying(t, fixturePath) assert.NotEmpty(t, jazon) + assert.NotContainsf(t, jazon, "circularSpec.json#/", "expected %s to be expanded with stripped circular $ref", fixturePath) @@ -599,63 +406,57 @@ func Test_MoreCircular(t *testing.T) { fixturePath := "fixtures/more_circulars/spec.json" jazon := expandThisOrDieTrying(t, fixturePath) m := rex.FindAllStringSubmatch(jazon, -1) - if assert.NotNil(t, m) { - for _, matched := range m { - subMatch := matched[1] - assert.True(t, strings.HasPrefix(subMatch, "item.json#/item"), - "expected $ref to be relative, got: %s", matched[0]) - } + require.NotNil(t, m) + for _, matched := range m { + subMatch := matched[1] + assert.True(t, strings.HasPrefix(subMatch, "item.json#/item"), + "expected $ref to be relative, got: %s", matched[0]) } fixturePath = "fixtures/more_circulars/spec2.json" jazon = expandThisOrDieTrying(t, fixturePath) m = rex.FindAllStringSubmatch(jazon, -1) - if assert.NotNil(t, m) { - for _, matched := range m { - subMatch := matched[1] - assert.True(t, strings.HasPrefix(subMatch, "item2.json#/item"), - "expected $ref to be relative, got: %s", matched[0]) - } + require.NotNil(t, m) + for _, matched := range m { + subMatch := matched[1] + assert.True(t, strings.HasPrefix(subMatch, "item2.json#/item"), + "expected $ref to be relative, got: %s", matched[0]) } fixturePath = "fixtures/more_circulars/spec3.json" jazon = expandThisOrDieTrying(t, fixturePath) m = rex.FindAllStringSubmatch(jazon, -1) - if assert.NotNil(t, m) { - for _, matched := range m { - subMatch := matched[1] - assert.True(t, strings.HasPrefix(subMatch, "item.json#/item"), - "expected $ref to be relative, got: %s", matched[0]) - } + require.NotNil(t, m) + for _, matched := range m { + subMatch := matched[1] + assert.True(t, strings.HasPrefix(subMatch, "item.json#/item"), + "expected $ref to be relative, got: %s", matched[0]) } fixturePath = "fixtures/more_circulars/spec4.json" jazon = expandThisOrDieTrying(t, fixturePath) m = rex.FindAllStringSubmatch(jazon, -1) - if assert.NotNil(t, m) { - for _, matched := range m { - subMatch := matched[1] - assert.True(t, strings.HasPrefix(subMatch, "item4.json#/item"), - "expected $ref to be relative, got: %s", matched[0]) - } + require.NotNil(t, m) + for _, matched := range m { + subMatch := matched[1] + assert.True(t, strings.HasPrefix(subMatch, "item4.json#/item"), + "expected $ref to be relative, got: %s", matched[0]) } } 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) + require.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) + require.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]) } } @@ -665,12 +466,11 @@ func Test_Bitbucket(t *testing.T) { fixturePath := "fixtures/more_circulars/bitbucket.json" jazon := expandThisOrDieTrying(t, 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]) - } + require.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]) } } @@ -679,12 +479,11 @@ func Test_ExpandJSONSchemaDraft4(t *testing.T) { jazon := expandThisSchemaOrDieTrying(t, fixturePath) // assert all $ref maches "$ref": "http://json-schema.org/draft-04/something" m := rex.FindAllStringSubmatch(jazon, -1) - if assert.NotNil(t, m) { - for _, matched := range m { - subMatch := matched[1] - assert.True(t, strings.HasPrefix(subMatch, "http://json-schema.org/draft-04/"), - "expected $ref to be remote, got: %s", matched[0]) - } + require.NotNil(t, m) + for _, matched := range m { + subMatch := matched[1] + assert.True(t, strings.HasPrefix(subMatch, "http://json-schema.org/draft-04/"), + "expected $ref to be remote, got: %s", matched[0]) } } @@ -693,58 +492,15 @@ func Test_ExpandSwaggerSchema(t *testing.T) { jazon := expandThisSchemaOrDieTrying(t, fixturePath) // assert all $ref maches "$ref": "#/definitions/something" m := rex.FindAllStringSubmatch(jazon, -1) - if assert.NotNil(t, m) { - for _, matched := range m { - // matched := m[0] - subMatch := matched[1] - assert.True(t, strings.HasPrefix(subMatch, "#/definitions/"), - "expected $ref to be inlined, got: %s", matched[0]) - } - } -} - -func docAndOpts(t *testing.T, fixturePath string) ([]byte, *ExpandOptions) { - doc, err := jsonDoc(fixturePath) - require.NoError(t, err) - - specPath, _ := absPath(fixturePath) - - return doc, &ExpandOptions{ - RelativeBase: specPath, + require.NotNil(t, m) + for _, matched := range m { + // matched := m[0] + subMatch := matched[1] + assert.True(t, strings.HasPrefix(subMatch, "#/definitions/"), + "expected $ref to be inlined, got: %s", matched[0]) } } -func expandThisSchemaOrDieTrying(t *testing.T, fixturePath string) string { - doc, opts := docAndOpts(t, fixturePath) - - sch := new(Schema) - err := json.Unmarshal(doc, sch) - require.NoError(t, err) - - require.NotPanics(t, func() { - assert.NoError(t, ExpandSchemaWithBasePath(sch, nil, opts)) - }, "Calling expand schema circular refs, should not panic!") - - bbb, _ := json.MarshalIndent(sch, "", " ") - return string(bbb) -} - -func expandThisOrDieTrying(t *testing.T, fixturePath string) string { - doc, opts := docAndOpts(t, fixturePath) - - spec := new(Swagger) - err := json.Unmarshal(doc, spec) - require.NoError(t, err) - - assert.NotPanics(t, func() { - err = ExpandSpec(spec, opts) - assert.NoError(t, err) - }, "Calling expand spec with circular refs, should not panic!") - - bbb, _ := json.MarshalIndent(spec, "", " ") - return string(bbb) -} - func TestContinueOnErrorExpansion(t *testing.T) { defer log.SetOutput(os.Stdout) log.SetOutput(ioutil.Discard) @@ -758,31 +514,29 @@ func TestContinueOnErrorExpansion(t *testing.T) { Input *Swagger `json:"input"` Expected *Swagger `json:"expected"` }{} - err = json.Unmarshal(missingRefDoc, &testCase) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(missingRefDoc, &testCase)) opts := &ExpandOptions{ ContinueOnError: true, RelativeBase: specPath, } - err = ExpandSpec(testCase.Input, opts) - assert.NoError(t, err) + require.NoError(t, ExpandSpec(testCase.Input, opts)) assert.Equal(t, testCase.Input, testCase.Expected, "Should continue expanding spec when a definition can't be found.") doc, err := jsonDoc("fixtures/expansion/missingItemRef.json") + require.NoError(t, err) + spec := new(Swagger) - err = json.Unmarshal(doc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(doc, spec)) assert.NotPanics(t, func() { - err = ExpandSpec(spec, opts) - assert.NoError(t, err) + require.NoError(t, ExpandSpec(spec, opts)) }, "Array of missing refs should not cause a panic, and continue to expand spec.") } func TestIssue415(t *testing.T) { doc, err := jsonDoc("fixtures/expansion/clickmeter.json") - assert.NoError(t, err) + require.NoError(t, err) specPath, _ := absPath("fixtures/expansion/clickmeter.json") @@ -791,47 +545,44 @@ func TestIssue415(t *testing.T) { } spec := new(Swagger) - err = json.Unmarshal(doc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(doc, spec)) assert.NotPanics(t, func() { - err = ExpandSpec(spec, opts) - assert.NoError(t, err) + require.NoError(t, ExpandSpec(spec, opts)) }, "Calling expand spec with response schemas that have circular refs, should not panic!") } func TestCircularSpecExpansion(t *testing.T) { doc, err := jsonDoc("fixtures/expansion/circularSpec.json") - assert.NoError(t, err) + require.NoError(t, err) - specPath, _ := absPath("fixtures/expansion/circularSpec.json") + specPath, err := absPath("fixtures/expansion/circularSpec.json") + require.NoError(t, err) opts := &ExpandOptions{ RelativeBase: specPath, } spec := new(Swagger) - err = json.Unmarshal(doc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(doc, spec)) assert.NotPanics(t, func() { - err = ExpandSpec(spec, opts) - assert.NoError(t, err) + require.NoError(t, ExpandSpec(spec, opts)) }, "Calling expand spec with circular refs, should not panic!") } func TestItemsExpansion(t *testing.T) { carsDoc, err := jsonDoc("fixtures/expansion/schemas2.json") - assert.NoError(t, err) + require.NoError(t, err) - basePath, _ := absPath("fixtures/expansion/schemas2.json") + basePath, err := absPath("fixtures/expansion/schemas2.json") + require.NoError(t, err) spec := new(Swagger) - err = json.Unmarshal(carsDoc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(carsDoc, spec)) resolver, err := defaultSchemaLoader(spec, nil, nil, nil) - assert.NoError(t, err) + require.NoError(t, err) schema := spec.Definitions["car"] oldBrand := schema.Properties["brand"] @@ -839,36 +590,42 @@ func TestItemsExpansion(t *testing.T) { assert.NotEqual(t, spec.Definitions["brand"], oldBrand) _, err = expandSchema(schema, []string{"#/definitions/car"}, resolver, basePath) - assert.NoError(t, err) + require.NoError(t, err) newBrand := schema.Properties["brand"] assert.Empty(t, newBrand.Items.Schema.Ref.String()) assert.Equal(t, spec.Definitions["brand"], *newBrand.Items.Schema) schema = spec.Definitions["truck"] - assert.NotEmpty(t, schema.Items.Schema.Ref.String()) + require.NotEmpty(t, schema.Items.Schema.Ref.String()) s, err := expandSchema(schema, []string{"#/definitions/truck"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) assert.Empty(t, schema.Items.Schema.Ref.String()) assert.Equal(t, spec.Definitions["car"], *schema.Items.Schema) sch := new(Schema) _, err = expandSchema(*sch, []string{""}, resolver, basePath) - assert.NoError(t, err) + require.NoError(t, err) schema = spec.Definitions["batch"] s, err = expandSchema(schema, []string{"#/definitions/batch"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + 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) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) assert.Empty(t, schema.Items.Schemas[0].Items.Schema.Ref.String()) assert.Empty(t, schema.Items.Schemas[1].Items.Schema.Ref.String()) assert.Equal(t, *schema.Items.Schemas[0].Items.Schema, spec.Definitions["brand"]) @@ -876,8 +633,10 @@ func TestItemsExpansion(t *testing.T) { schema = spec.Definitions["allofBoth"] s, err = expandSchema(schema, []string{"#/definitions/allofBoth"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) assert.Empty(t, schema.AllOf[0].Items.Schema.Ref.String()) assert.Empty(t, schema.AllOf[1].Items.Schema.Ref.String()) assert.Equal(t, *schema.AllOf[0].Items.Schema, spec.Definitions["brand"]) @@ -885,8 +644,10 @@ func TestItemsExpansion(t *testing.T) { schema = spec.Definitions["anyofBoth"] s, err = expandSchema(schema, []string{"#/definitions/anyofBoth"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) assert.Empty(t, schema.AnyOf[0].Items.Schema.Ref.String()) assert.Empty(t, schema.AnyOf[1].Items.Schema.Ref.String()) assert.Equal(t, *schema.AnyOf[0].Items.Schema, spec.Definitions["brand"]) @@ -894,8 +655,10 @@ func TestItemsExpansion(t *testing.T) { schema = spec.Definitions["oneofBoth"] s, err = expandSchema(schema, []string{"#/definitions/oneofBoth"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) assert.Empty(t, schema.OneOf[0].Items.Schema.Ref.String()) assert.Empty(t, schema.OneOf[1].Items.Schema.Ref.String()) assert.Equal(t, *schema.OneOf[0].Items.Schema, spec.Definitions["brand"]) @@ -903,45 +666,57 @@ func TestItemsExpansion(t *testing.T) { schema = spec.Definitions["notSomething"] s, err = expandSchema(schema, []string{"#/definitions/notSomething"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + 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) + require.NoError(t, err) + require.NotNil(t, s) + 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) + require.NoError(t, err) + require.NotNil(t, s) + 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) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) prop := schema.PatternProperties["^x-ab"] assert.Empty(t, prop.Items.Schema.Ref.String()) assert.Equal(t, *prop.Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["deps"] s, err = expandSchema(schema, []string{"#/definitions/deps"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) prop2 := schema.Dependencies["something"] assert.Empty(t, prop2.Schema.Items.Schema.Ref.String()) assert.Equal(t, *prop2.Schema.Items.Schema, spec.Definitions["tag"]) schema = spec.Definitions["defined"] s, err = expandSchema(schema, []string{"#/definitions/defined"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) prop = schema.Definitions["something"] assert.Empty(t, prop.Items.Schema.Ref.String()) assert.Equal(t, *prop.Items.Schema, spec.Definitions["tag"]) @@ -949,16 +724,16 @@ func TestItemsExpansion(t *testing.T) { func TestSchemaExpansion(t *testing.T) { carsDoc, err := jsonDoc("fixtures/expansion/schemas1.json") - assert.NoError(t, err) + require.NoError(t, err) - basePath, _ := absPath("fixtures/expansion/schemas1.json") + basePath, err := absPath("fixtures/expansion/schemas1.json") + require.NoError(t, err) spec := new(Swagger) - err = json.Unmarshal(carsDoc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(carsDoc, spec)) resolver, err := defaultSchemaLoader(spec, nil, nil, nil) - assert.NoError(t, err) + require.NoError(t, err) schema := spec.Definitions["car"] oldBrand := schema.Properties["brand"] @@ -966,8 +741,10 @@ func TestSchemaExpansion(t *testing.T) { assert.NotEqual(t, spec.Definitions["brand"], oldBrand) s, err := expandSchema(schema, []string{"#/definitions/car"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) newBrand := schema.Properties["brand"] assert.Empty(t, newBrand.Ref.String()) @@ -977,26 +754,32 @@ func TestSchemaExpansion(t *testing.T) { assert.NotEmpty(t, schema.Ref.String()) s, err = expandSchema(schema, []string{"#/definitions/truck"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) assert.Empty(t, schema.Ref.String()) assert.Equal(t, spec.Definitions["car"], schema) sch := new(Schema) _, err = expandSchema(*sch, []string{""}, resolver, basePath) - assert.NoError(t, err) + require.NoError(t, err) schema = spec.Definitions["batch"] s, err = expandSchema(schema, []string{"#/definitions/batch"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + 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) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) assert.Empty(t, schema.Items.Schemas[0].Ref.String()) assert.Empty(t, schema.Items.Schemas[1].Ref.String()) assert.Equal(t, schema.Items.Schemas[0], spec.Definitions["brand"]) @@ -1004,8 +787,10 @@ func TestSchemaExpansion(t *testing.T) { schema = spec.Definitions["allofBoth"] s, err = expandSchema(schema, []string{"#/definitions/allofBoth"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) assert.Empty(t, schema.AllOf[0].Ref.String()) assert.Empty(t, schema.AllOf[1].Ref.String()) assert.Equal(t, schema.AllOf[0], spec.Definitions["brand"]) @@ -1013,8 +798,10 @@ func TestSchemaExpansion(t *testing.T) { schema = spec.Definitions["anyofBoth"] s, err = expandSchema(schema, []string{"#/definitions/anyofBoth"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) assert.Empty(t, schema.AnyOf[0].Ref.String()) assert.Empty(t, schema.AnyOf[1].Ref.String()) assert.Equal(t, schema.AnyOf[0], spec.Definitions["brand"]) @@ -1022,8 +809,10 @@ func TestSchemaExpansion(t *testing.T) { schema = spec.Definitions["oneofBoth"] s, err = expandSchema(schema, []string{"#/definitions/oneofBoth"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) assert.Empty(t, schema.OneOf[0].Ref.String()) assert.Empty(t, schema.OneOf[1].Ref.String()) assert.Equal(t, schema.OneOf[0], spec.Definitions["brand"]) @@ -1031,75 +820,63 @@ func TestSchemaExpansion(t *testing.T) { schema = spec.Definitions["notSomething"] s, err = expandSchema(schema, []string{"#/definitions/notSomething"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + 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) + require.NoError(t, err) + require.NotNil(t, s) + 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) + require.NoError(t, err) + require.NotNil(t, s) + 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) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) prop := schema.PatternProperties["^x-ab"] assert.Empty(t, prop.Ref.String()) assert.Equal(t, prop, spec.Definitions["tag"]) schema = spec.Definitions["deps"] s, err = expandSchema(schema, []string{"#/definitions/deps"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) prop2 := schema.Dependencies["something"] assert.Empty(t, prop2.Schema.Ref.String()) assert.Equal(t, *prop2.Schema, spec.Definitions["tag"]) schema = spec.Definitions["defined"] s, err = expandSchema(schema, []string{"#/definitions/defined"}, resolver, basePath) + require.NoError(t, err) + require.NotNil(t, s) + schema = *s - assert.NoError(t, err) prop = schema.Definitions["something"] assert.Empty(t, prop.Ref.String()) assert.Equal(t, prop, spec.Definitions["tag"]) } -func TestDefaultResolutionCache(t *testing.T) { - jsonSchema := MustLoadJSONSchemaDraft04() - swaggerSchema := MustLoadSwagger20Schema() - - cache := defaultResolutionCache() - - sch, ok := cache.Get("not there") - assert.False(t, ok) - assert.Nil(t, sch) - - sch, ok = cache.Get("http://swagger.io/v2/schema.json") - assert.True(t, ok) - assert.Equal(t, swaggerSchema, sch) - - sch, ok = cache.Get("http://json-schema.org/draft-04/schema") - assert.True(t, ok) - assert.Equal(t, jsonSchema, sch) - - cache.Set("something", "here") - sch, ok = cache.Get("something") - assert.True(t, ok) - assert.Equal(t, "here", sch) -} - func TestRelativeBaseURI(t *testing.T) { server := httptest.NewServer(http.FileServer(http.Dir("fixtures/remote"))) defer server.Close() @@ -1108,19 +885,17 @@ func TestRelativeBaseURI(t *testing.T) { // resolver, err := defaultSchemaLoader(spec, nil, nil,nil) // assert.NoError(t, err) - err := ExpandSpec(spec, nil) - assert.NoError(t, err) + require.NoError(t, ExpandSpec(spec, nil)) specDoc, err := jsonDoc("fixtures/remote/all-the-things.json") - assert.NoError(t, err) + require.NoError(t, err) opts := &ExpandOptions{ RelativeBase: server.URL + "/all-the-things.json", } spec = new(Swagger) - err = json.Unmarshal(specDoc, spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(specDoc, spec)) pet := spec.Definitions["pet"] errorModel := spec.Definitions["errorModel"] @@ -1132,440 +907,117 @@ func TestRelativeBaseURI(t *testing.T) { anotherPet := spec.Responses["anotherPet"] anotherPet.Ref = MustCreateRef(server.URL + "/" + anotherPet.Ref.String()) - err = ExpandResponse(&anotherPet, opts.RelativeBase) - assert.NoError(t, err) + require.NoError(t, ExpandResponse(&anotherPet, opts.RelativeBase)) spec.Responses["anotherPet"] = anotherPet circularA := spec.Responses["circularA"] circularA.Ref = MustCreateRef(server.URL + "/" + circularA.Ref.String()) - err = ExpandResponse(&circularA, opts.RelativeBase) - assert.NoError(t, err) + require.NoError(t, ExpandResponse(&circularA, opts.RelativeBase)) spec.Responses["circularA"] = circularA - err = ExpandSpec(spec, opts) - assert.NoError(t, err) + require.NoError(t, ExpandSpec(spec, opts)) assert.Equal(t, tagParam, spec.Parameters["query"]) + assert.Equal(t, petResponse, spec.Responses["petResponse"]) - // ICI assert.Equal(t, petResponse, spec.Responses["anotherPet"]) - // ICI - assert.Equal(t, pet, *spec.Responses["petResponse"].Schema) - assert.Equal(t, stringResponse, *spec.Paths.Paths["/"].Get.Responses.Default) + assert.Equal(t, petResponse, spec.Paths.Paths["/pets"].Post.Responses.StatusCodeResponses[200]) assert.Equal(t, petResponse, spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200]) + + assert.Equal(t, pet, *spec.Responses["petResponse"].Schema) assert.Equal(t, pet, *spec.Paths.Paths["/pets"].Get.Responses.StatusCodeResponses[200].Schema.Items.Schema) - assert.Equal(t, errorModel, *spec.Paths.Paths["/pets"].Get.Responses.Default.Schema) assert.Equal(t, pet, spec.Definitions["petInput"].AllOf[0]) + assert.Equal(t, spec.Definitions["petInput"], *spec.Paths.Paths["/pets"].Post.Parameters[0].Schema) - assert.Equal(t, petResponse, spec.Paths.Paths["/pets"].Post.Responses.StatusCodeResponses[200]) + + assert.Equal(t, stringResponse, *spec.Paths.Paths["/"].Get.Responses.Default) + + assert.Equal(t, errorModel, *spec.Paths.Paths["/pets"].Get.Responses.Default.Schema) assert.Equal(t, errorModel, *spec.Paths.Paths["/pets"].Post.Responses.Default.Schema) + pi := spec.Paths.Paths["/pets/{id}"] assert.Equal(t, idParam, pi.Get.Parameters[0]) assert.Equal(t, petResponse, pi.Get.Responses.StatusCodeResponses[200]) - assert.Equal(t, errorModel, *pi.Get.Responses.Default.Schema) assert.Equal(t, idParam, pi.Delete.Parameters[0]) - assert.Equal(t, errorModel, *pi.Delete.Responses.Default.Schema) -} - -func resolutionContextServer() *httptest.Server { - var servedAt string - server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - // fmt.Println("got a request for", req.URL.String()) - if req.URL.Path == "/resolution.json" { - - b, _ := ioutil.ReadFile(filepath.Join(specs, "resolution.json")) - var ctnt map[string]interface{} - _ = json.Unmarshal(b, &ctnt) - ctnt["id"] = servedAt - - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(200) - bb, _ := json.Marshal(ctnt) - _, _ = rw.Write(bb) - return - } - if req.URL.Path == "/resolution2.json" { - b, _ := ioutil.ReadFile(filepath.Join(specs, "resolution2.json")) - var ctnt map[string]interface{} - _ = json.Unmarshal(b, &ctnt) - ctnt["id"] = servedAt - - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(200) - bb, _ := json.Marshal(ctnt) - _, _ = rw.Write(bb) - return - } - - if req.URL.Path == "/boolProp.json" { - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(200) - b, _ := json.Marshal(map[string]interface{}{ - "type": "boolean", - }) - _, _ = rw.Write(b) - return - } - - if req.URL.Path == "/deeper/stringProp.json" { - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(200) - b, _ := json.Marshal(map[string]interface{}{ - "type": "string", - }) - _, _ = rw.Write(b) - return - } - - if req.URL.Path == "/deeper/arrayProp.json" { - rw.Header().Set("Content-Type", "application/json") - rw.WriteHeader(200) - b, _ := json.Marshal(map[string]interface{}{ - "type": "array", - "items": map[string]interface{}{ - "type": "file", - }, - }) - _, _ = rw.Write(b) - return - } - - rw.WriteHeader(http.StatusNotFound) - })) - servedAt = server.URL - return server -} - -func TestResolveRemoteRef_RootSame(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() - - rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) - // the filename doesn't matter because ref will eventually point to refed.json - specBase, _ := absPath(filepath.Join(specs, "anyotherfile.json")) - if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { - var result0 Swagger - ref0, _ := NewRef(server.URL + "/refed.json#") - resolver0, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - if assert.NoError(t, resolver0.Resolve(&ref0, &result0, "")) { - assertSpecs(t, result0, *rootDoc) - } - - var result1 Swagger - ref1, _ := NewRef("./refed.json") - resolver1, _ := defaultSchemaLoader(rootDoc, &ExpandOptions{ - RelativeBase: specBase, - }, nil, nil) - if assert.NoError(t, resolver1.Resolve(&ref1, &result1, specBase)) { - assertSpecs(t, result1, *rootDoc) - } - } -} - -func TestResolveRemoteRef_FromFragment(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() - - rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) - - if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { - var tgt Schema - ref, err := NewRef(server.URL + "/refed.json#/definitions/pet") - if assert.NoError(t, err) { - resolver := &schemaLoader{root: rootDoc, cache: defaultResolutionCache(), context: &resolverContext{loadDoc: jsonDoc}} - if assert.NoError(t, resolver.Resolve(&ref, &tgt, "")) { - assert.Equal(t, []string{"id", "name"}, tgt.Required) - } - } - } -} - -func TestResolveRemoteRef_FromInvalidFragment(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() - rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) - if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { - var tgt Schema - ref, err := NewRef(server.URL + "/refed.json#/definitions/NotThere") - if assert.NoError(t, err) { - resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - assert.Error(t, resolver.Resolve(&ref, &tgt, "")) - } - } + assert.Equal(t, errorModel, *pi.Get.Responses.Default.Schema) + assert.Equal(t, errorModel, *pi.Delete.Responses.Default.Schema) } -func TestResolveRemoteRef_WithResolutionContext(t *testing.T) { +func TestExpandRemoteRef_WithResolutionContext(t *testing.T) { server := resolutionContextServer() defer server.Close() var tgt Schema ref, err := NewRef(server.URL + "/resolution.json#/definitions/bool") - if assert.NoError(t, err) { - tgt.Ref = ref - ere := ExpandSchema(&tgt, nil, nil) - assert.NoError(t, ere) - assert.Equal(t, StringOrArray([]string{"boolean"}), tgt.Type) - } + require.NoError(t, err) + tgt.Ref = ref + require.NoError(t, ExpandSchema(&tgt, nil, nil)) + assert.Equal(t, StringOrArray([]string{"boolean"}), tgt.Type) } -func TestResolveRemoteRef_WithNestedResolutionContext(t *testing.T) { +func TestExpandRemoteRef_WithNestedResolutionContext(t *testing.T) { server := resolutionContextServer() defer server.Close() var tgt Schema ref, err := NewRef(server.URL + "/resolution.json#/items") - if assert.NoError(t, err) { - tgt.Ref = ref - ere := ExpandSchema(&tgt, nil, nil) - assert.NoError(t, ere) - assert.Equal(t, StringOrArray([]string{"string"}), tgt.Items.Schema.Type) - } + require.NoError(t, err) + + tgt.Ref = ref + require.NoError(t, ExpandSchema(&tgt, nil, nil)) + assert.Equal(t, StringOrArray([]string{"string"}), tgt.Items.Schema.Type) } /* This next test will have to wait until we do full $ID analysis for every subschema on every file that is referenced */ -/* For now, TestResolveRemoteRef_WithNestedResolutionContext replaces this next test */ -// func TestResolveRemoteRef_WithNestedResolutionContext_WithParentID(t *testing.T) { +/* For now, TestExpandRemoteRef_WithNestedResolutionContext replaces this next test */ +// func TestExpandRemoteRef_WithNestedResolutionContext_WithParentID(t *testing.T) { // server := resolutionContextServer() // defer server.Close() // var tgt Schema // ref, err := NewRef(server.URL + "/resolution.json#/items/items") -// if assert.NoError(t, err) { -// tgt.Ref = ref -// ExpandSchema(&tgt, nil, nil) -// assert.Equal(t, StringOrArray([]string{"string"}), tgt.Type) -// } +// require.NoError(t, err) +// +// tgt.Ref = ref +// require.NoError(t, ExpandSchema(&tgt, nil, nil)) +// assert.Equal(t, StringOrArray([]string{"string"}), tgt.Type) // } -func TestResolveRemoteRef_WithNestedResolutionContextWithFragment(t *testing.T) { +func TestExpandRemoteRef_WithNestedResolutionContextWithFragment(t *testing.T) { server := resolutionContextServer() defer server.Close() var tgt Schema ref, err := NewRef(server.URL + "/resolution2.json#/items") - if assert.NoError(t, err) { - tgt.Ref = ref - ere := ExpandSchema(&tgt, nil, nil) - assert.NoError(t, ere) - assert.Equal(t, StringOrArray([]string{"file"}), tgt.Items.Schema.Type) - } - -} - -/* This next test will have to wait until we do full $ID analysis for every subschema on every file that is referenced */ -/* For now, TestResolveRemoteRef_WithNestedResolutionContext replaces this next test */ -// func TestResolveRemoteRef_WithNestedResolutionContextWithFragment_WithParentID(t *testing.T) { -// server := resolutionContextServer() -// defer server.Close() - -// rootDoc := new(Swagger) -// b, err := ioutil.ReadFile("fixtures/specs/refed.json") -// if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { -// var tgt Schema -// ref, err := NewRef(server.URL + "/resolution2.json#/items/items") -// if assert.NoError(t, err) { -// resolver, _ := defaultSchemaLoader(rootDoc, nil, nil,nil) -// if assert.NoError(t, resolver.Resolve(&ref, &tgt, "")) { -// assert.Equal(t, StringOrArray([]string{"file"}), tgt.Type) -// } -// } -// } -// } - -func TestResolveRemoteRef_ToParameter(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() - - rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) - if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { - var tgt Parameter - ref, err := NewRef(server.URL + "/refed.json#/parameters/idParam") - if assert.NoError(t, err) { - - resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - if assert.NoError(t, resolver.Resolve(&ref, &tgt, "")) { - assert.Equal(t, "id", tgt.Name) - assert.Equal(t, "path", tgt.In) - assert.Equal(t, "ID of pet to fetch", tgt.Description) - assert.True(t, tgt.Required) - assert.Equal(t, "integer", tgt.Type) - assert.Equal(t, "int64", tgt.Format) - } - } - } -} - -func TestResolveRemoteRef_ToPathItem(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() - - rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) - if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { - var tgt PathItem - ref, err := NewRef(server.URL + "/refed.json#/paths/" + jsonpointer.Escape("/pets/{id}")) - if assert.NoError(t, err) { - - resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - if assert.NoError(t, resolver.Resolve(&ref, &tgt, "")) { - assert.Equal(t, rootDoc.Paths.Paths["/pets/{id}"].Get, tgt.Get) - } - } - } -} - -func TestResolveRemoteRef_ToResponse(t *testing.T) { - fileserver := http.FileServer(http.Dir(specs)) - server := httptest.NewServer(fileserver) - defer server.Close() - - rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) - if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { - var tgt Response - ref, err := NewRef(server.URL + "/refed.json#/responses/petResponse") - if assert.NoError(t, err) { - - resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - if assert.NoError(t, resolver.Resolve(&ref, &tgt, "")) { - assert.Equal(t, rootDoc.Responses["petResponse"], tgt) - } - } - } -} - -func TestResolveLocalRef_SameRoot(t *testing.T) { - rootDoc := new(Swagger) - _ = json.Unmarshal(PetStoreJSONMessage, rootDoc) - - result := new(Swagger) - ref, _ := NewRef("#") - resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - err := resolver.Resolve(&ref, result, "") - if assert.NoError(t, err) { - assert.Equal(t, rootDoc, result) - } -} - -func TestResolveLocalRef_FromFragment(t *testing.T) { - rootDoc := new(Swagger) - _ = json.Unmarshal(PetStoreJSONMessage, rootDoc) - - var tgt Schema - ref, err := NewRef("#/definitions/Category") - if assert.NoError(t, err) { - resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - err := resolver.Resolve(&ref, &tgt, "") - if assert.NoError(t, err) { - assert.Equal(t, "Category", tgt.ID) - } - } -} - -func TestResolveLocalRef_FromInvalidFragment(t *testing.T) { - rootDoc := new(Swagger) - _ = json.Unmarshal(PetStoreJSONMessage, rootDoc) - - var tgt Schema - ref, err := NewRef("#/definitions/NotThere") - if assert.NoError(t, err) { - resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - err := resolver.Resolve(&ref, &tgt, "") - assert.Error(t, err) - } -} - -func TestResolveLocalRef_Parameter(t *testing.T) { - rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) - basePath, _ := absPath(filepath.Join(specs, "refed.json")) - if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { - var tgt Parameter - ref, err := NewRef("#/parameters/idParam") - if assert.NoError(t, err) { - resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - if assert.NoError(t, resolver.Resolve(&ref, &tgt, basePath)) { - assert.Equal(t, "id", tgt.Name) - assert.Equal(t, "path", tgt.In) - assert.Equal(t, "ID of pet to fetch", tgt.Description) - assert.True(t, tgt.Required) - assert.Equal(t, "integer", tgt.Type) - assert.Equal(t, "int64", tgt.Format) - } - } - } -} - -func TestResolveLocalRef_PathItem(t *testing.T) { - rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) - basePath, _ := absPath(filepath.Join(specs, "refed.json")) - if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { - var tgt PathItem - ref, err := NewRef("#/paths/" + jsonpointer.Escape("/pets/{id}")) - if assert.NoError(t, err) { - resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - if assert.NoError(t, resolver.Resolve(&ref, &tgt, basePath)) { - assert.Equal(t, rootDoc.Paths.Paths["/pets/{id}"].Get, tgt.Get) - } - } - } -} + require.NoError(t, err) -func TestResolveLocalRef_Response(t *testing.T) { - rootDoc := new(Swagger) - b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) - basePath, _ := absPath(filepath.Join(specs, "refed.json")) - if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { - var tgt Response - ref, err := NewRef("#/responses/petResponse") - if assert.NoError(t, err) { - resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) - if assert.NoError(t, resolver.Resolve(&ref, &tgt, basePath)) { - assert.Equal(t, rootDoc.Responses["petResponse"], tgt) - } - } - } + tgt.Ref = ref + require.NoError(t, ExpandSchema(&tgt, nil, nil)) + assert.Equal(t, StringOrArray([]string{"file"}), tgt.Items.Schema.Type) } -func TestResolveForTransitiveRefs(t *testing.T) { +func TestExpandForTransitiveRefs(t *testing.T) { var spec *Swagger rawSpec, err := ioutil.ReadFile(filepath.Join(specs, "todos.json")) - assert.NoError(t, err) + require.NoError(t, err) basePath, err := absPath(filepath.Join(specs, "todos.json")) - assert.NoError(t, err) + require.NoError(t, err) opts := &ExpandOptions{ RelativeBase: basePath, } - err = json.Unmarshal(rawSpec, &spec) - assert.NoError(t, err) + require.NoError(t, json.Unmarshal(rawSpec, &spec)) - err = ExpandSpec(spec, opts) - assert.NoError(t, err) + require.NoError(t, ExpandSpec(spec, opts)) } -const ( - withoutSchemaID = "removed" - withSchemaID = "schema" -) - func TestExpandSchemaWithRoot(t *testing.T) { root := new(Swagger) - _ = json.Unmarshal(PetStoreJSONMessage, root) + require.NoError(t, json.Unmarshal(PetStoreJSONMessage, root)) // 1. remove ID from root definition origPet := root.Definitions["Pet"] @@ -1581,7 +1033,7 @@ func TestExpandSchemaWithRoot(t *testing.T) { expandRootWithID(t, root, withSchemaID) } -func expandRootWithID(t *testing.T, root *Swagger, testcase string) { +func expandRootWithID(t testing.TB, root *Swagger, testcase string) { t.Logf("case: expanding $ref to schema without ID, with nested $ref with %s ID", testcase) sch := &Schema{ SchemaProps: SchemaProps{ @@ -1590,10 +1042,11 @@ func expandRootWithID(t *testing.T, root *Swagger, testcase string) { } err := ExpandSchema(sch, root, nil) if testcase == withSchemaID { - assert.Errorf(t, err, "expected %s NOT to expand properly because of the ID in the parent schema", sch.Ref.String()) + require.Errorf(t, err, "expected %s NOT to expand properly because of the ID in the parent schema", sch.Ref.String()) } else { - assert.NoErrorf(t, err, "expected %s to expand properly", sch.Ref.String()) + require.NoErrorf(t, err, "expected %s to expand properly", sch.Ref.String()) } + if Debug { bbb, _ := json.MarshalIndent(sch, "", " ") t.Log(string(bbb)) @@ -1605,8 +1058,8 @@ func expandRootWithID(t *testing.T, root *Swagger, testcase string) { Ref: MustCreateRef("#/definitions/Category"), }, } - err = ExpandSchema(sch, root, nil) - assert.NoErrorf(t, err, "expected %s to expand properly", sch.Ref.String()) + require.NoErrorf(t, ExpandSchema(sch, root, nil), "expected %s to expand properly", sch.Ref.String()) + if Debug { bbb, _ := json.MarshalIndent(sch, "", " ") t.Log(string(bbb)) @@ -1619,30 +1072,30 @@ func expandRootWithID(t *testing.T, root *Swagger, testcase string) { } err = ExpandSchema(sch, root, nil) if testcase == withSchemaID { - assert.Errorf(t, err, "expected %s NOT to expand properly because of the ID in the parent schema", sch.Ref.String()) + require.Errorf(t, err, "expected %s NOT to expand properly because of the ID in the parent schema", sch.Ref.String()) } else { - assert.NoErrorf(t, err, "expected %s to expand properly", sch.Ref.String()) + require.NoErrorf(t, err, "expected %s to expand properly", sch.Ref.String()) } + if Debug { bbb, _ := json.MarshalIndent(sch, "", " ") t.Log(string(bbb)) } } -const pathItemsFixture = "fixtures/expansion/pathItems.json" - func TestExpandPathItem(t *testing.T) { spec := new(Swagger) specDoc, err := jsonDoc(pathItemsFixture) - assert.NoError(t, err) - _ = json.Unmarshal(specDoc, spec) - specPath, _ := absPath(pathItemsFixture) + require.NoError(t, err) + + require.NoError(t, json.Unmarshal(specDoc, spec)) + + specPath, err := absPath(pathItemsFixture) + require.NoError(t, err) // ExpandSpec use case - err = ExpandSpec(spec, &ExpandOptions{RelativeBase: specPath}) - if !assert.NoError(t, err) { - t.FailNow() - } + require.NoError(t, ExpandSpec(spec, &ExpandOptions{RelativeBase: specPath})) + jazon, _ := json.MarshalIndent(spec, "", " ") assert.JSONEq(t, `{ "swagger": "2.0", @@ -1673,52 +1126,19 @@ func TestExpandPathItem(t *testing.T) { }`, string(jazon)) } -func TestResolvePathItem(t *testing.T) { - spec := new(Swagger) - specDoc, err := jsonDoc(pathItemsFixture) - assert.NoError(t, err) - _ = json.Unmarshal(specDoc, spec) - specPath, _ := absPath(pathItemsFixture) - - // Resolve use case - pth := spec.Paths.Paths["/todos"] - pathItem, err := ResolvePathItem(spec, pth.Ref, &ExpandOptions{RelativeBase: specPath}) - assert.NoError(t, err) - jazon, _ := json.MarshalIndent(pathItem, "", " ") - assert.JSONEq(t, `{ - "get": { - "responses": { - "200": { - "description": "List Todos", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "404": { - "description": "error" - } - } - } - }`, string(jazon)) -} - -const extraRefFixture = "fixtures/expansion/extraRef.json" - func TestExpandExtraItems(t *testing.T) { spec := new(Swagger) specDoc, err := jsonDoc(extraRefFixture) - assert.NoError(t, err) - _ = json.Unmarshal(specDoc, spec) - specPath, _ := absPath(extraRefFixture) + require.NoError(t, err) + + require.NoError(t, json.Unmarshal(specDoc, spec)) + + specPath, err := absPath(extraRefFixture) + require.NoError(t, err) // ExpandSpec use case: unsupported $refs are not expanded - err = ExpandSpec(spec, &ExpandOptions{RelativeBase: specPath}) - if !assert.NoError(t, err) { - t.FailNow() - } + require.NoError(t, ExpandSpec(spec, &ExpandOptions{RelativeBase: specPath})) + jazon, _ := json.MarshalIndent(spec, "", " ") assert.JSONEq(t, `{ "schemes": [ @@ -1779,35 +1199,6 @@ func TestExpandExtraItems(t *testing.T) { }`, string(jazon)) } -func TestResolveExtraItem(t *testing.T) { - // go-openapi extra goodie: $ref in simple schema Items and Headers - spec := new(Swagger) - specDoc, err := jsonDoc(extraRefFixture) - assert.NoError(t, err) - _ = json.Unmarshal(specDoc, spec) - specPath, _ := absPath(extraRefFixture) - - // Resolve param Items use case: here we explicitly resolve the unsuppord case - parm := spec.Paths.Paths["/employees"].Get.Parameters[0] - parmItem, err := ResolveItems(spec, parm.Items.Ref, &ExpandOptions{RelativeBase: specPath}) - assert.NoError(t, err) - jazon, _ := json.MarshalIndent(parmItem, "", " ") - assert.JSONEq(t, `{ - "type": "integer", - "format": "int32" - }`, string(jazon)) - - // Resolve header Items use case: here we explicitly resolve the unsuppord case - hdr := spec.Paths.Paths["/employees"].Get.Responses.StatusCodeResponses[200].Headers["X-header"] - hdrItem, err := ResolveItems(spec, hdr.Items.Ref, &ExpandOptions{RelativeBase: specPath}) - assert.NoError(t, err) - jazon, _ = json.MarshalIndent(hdrItem, "", " ") - assert.JSONEq(t, `{ - "type": "string", - "format": "uuid" - }`, string(jazon)) -} - func Test_CircularID(t *testing.T) { go func() { err := http.ListenAndServe("localhost:1234", http.FileServer(http.Dir("fixtures/more_circulars/remote"))) @@ -1844,9 +1235,6 @@ func Test_CircularID(t *testing.T) { require.Len(t, refs, 1) } -// PetStoreJSONMessage json raw message for Petstore20 -var PetStoreJSONMessage = json.RawMessage([]byte(PetStore20)) - // PetStore20 json doc for swagger 2.0 pet store const PetStore20 = `{ "swagger": "2.0", diff --git a/go.mod b/go.mod index 02c5f1d..1717d08 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ require ( github.com/go-openapi/jsonreference v0.19.5 github.com/go-openapi/swag v0.19.12 github.com/stretchr/testify v1.6.1 - golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect + golang.org/x/net v0.0.0-20201224014010-6772e930b67b // indirect golang.org/x/text v0.3.4 // indirect gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 75bdf87..730dddb 100644 --- a/go.sum +++ b/go.sum @@ -41,16 +41,13 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= diff --git a/helpers_test.go b/helpers_test.go new file mode 100644 index 0000000..c689a57 --- /dev/null +++ b/helpers_test.go @@ -0,0 +1,132 @@ +package spec + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + "github.com/go-openapi/swag" + "github.com/stretchr/testify/require" +) + +func jsonDoc(path string) (json.RawMessage, error) { + data, err := swag.LoadFromFileOrHTTP(path) + if err != nil { + return nil, err + } + return json.RawMessage(data), nil +} + +func docAndOpts(t testing.TB, fixturePath string) ([]byte, *ExpandOptions) { + doc, err := jsonDoc(fixturePath) + require.NoError(t, err) + + specPath, _ := absPath(fixturePath) + + return doc, &ExpandOptions{ + RelativeBase: specPath, + } +} + +func expandThisSchemaOrDieTrying(t testing.TB, fixturePath string) string { + doc, opts := docAndOpts(t, fixturePath) + + sch := new(Schema) + require.NoError(t, json.Unmarshal(doc, sch)) + + require.NotPanics(t, func() { + require.NoError(t, ExpandSchemaWithBasePath(sch, nil, opts)) + }, "Calling expand schema circular refs, should not panic!") + + bbb, err := json.MarshalIndent(sch, "", " ") + require.NoError(t, err) + + return string(bbb) +} + +func expandThisOrDieTrying(t testing.TB, fixturePath string) string { + doc, opts := docAndOpts(t, fixturePath) + + spec := new(Swagger) + require.NoError(t, json.Unmarshal(doc, spec)) + + require.NotPanics(t, func() { + require.NoError(t, ExpandSpec(spec, opts)) + }, "Calling expand spec with circular refs, should not panic!") + + bbb, err := json.MarshalIndent(spec, "", " ") + require.NoError(t, err) + + return string(bbb) +} + +func resolutionContextServer() *httptest.Server { + var servedAt string + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if req.URL.Path == "/resolution.json" { + + b, _ := ioutil.ReadFile(filepath.Join(specs, "resolution.json")) + var ctnt map[string]interface{} + _ = json.Unmarshal(b, &ctnt) + ctnt["id"] = servedAt + + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(200) + bb, _ := json.Marshal(ctnt) + _, _ = rw.Write(bb) + return + } + if req.URL.Path == "/resolution2.json" { + b, _ := ioutil.ReadFile(filepath.Join(specs, "resolution2.json")) + var ctnt map[string]interface{} + _ = json.Unmarshal(b, &ctnt) + ctnt["id"] = servedAt + + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(200) + bb, _ := json.Marshal(ctnt) + _, _ = rw.Write(bb) + return + } + + if req.URL.Path == "/boolProp.json" { + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(200) + b, _ := json.Marshal(map[string]interface{}{ + "type": "boolean", + }) + _, _ = rw.Write(b) + return + } + + if req.URL.Path == "/deeper/stringProp.json" { + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(200) + b, _ := json.Marshal(map[string]interface{}{ + "type": "string", + }) + _, _ = rw.Write(b) + return + } + + if req.URL.Path == "/deeper/arrayProp.json" { + rw.Header().Set("Content-Type", "application/json") + rw.WriteHeader(200) + b, _ := json.Marshal(map[string]interface{}{ + "type": "array", + "items": map[string]interface{}{ + "type": "file", + }, + }) + _, _ = rw.Write(b) + return + } + + rw.WriteHeader(http.StatusNotFound) + })) + servedAt = server.URL + return server +} diff --git a/normalizer.go b/normalizer.go index 1a8731c..e9010a7 100644 --- a/normalizer.go +++ b/normalizer.go @@ -159,8 +159,6 @@ func normalizeFileRef(ref *Ref, relativeBase string) *Ref { return &r } - debugLog("normalizing %s against %s", ref.String(), relativeBase) - s := normalizePaths(ref.String(), relativeBase) r, _ := NewRef(s) return &r diff --git a/normalizer_test.go b/normalizer_test.go new file mode 100644 index 0000000..ccb1858 --- /dev/null +++ b/normalizer_test.go @@ -0,0 +1,84 @@ +package spec + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +// tests that paths are normalized correctly +func TestNormalizePaths(t *testing.T) { + type testNormalizePathsTestCases []struct { + refPath string + base string + expOutput string + } + + testCases := func() testNormalizePathsTestCases { + testCases := testNormalizePathsTestCases{ + { + // http basePath, absolute refPath + refPath: "http://www.anotherexample.com/another/base/path/swagger.json#/definitions/Pet", + base: "http://www.example.com/base/path/swagger.json", + expOutput: "http://www.anotherexample.com/another/base/path/swagger.json#/definitions/Pet", + }, + { + // http basePath, relative refPath + refPath: "another/base/path/swagger.json#/definitions/Pet", + base: "http://www.example.com/base/path/swagger.json", + expOutput: "http://www.example.com/base/path/another/base/path/swagger.json#/definitions/Pet", + }, + } + if runtime.GOOS == "windows" { + testCases = append(testCases, testNormalizePathsTestCases{ + { + // file basePath, absolute refPath, no fragment + refPath: `C:\another\base\path.json`, + base: `C:\base\path.json`, + expOutput: `c:\another\base\path.json`, + }, + { + // file basePath, absolute refPath + refPath: `C:\another\base\path.json#/definitions/Pet`, + base: `C:\base\path.json`, + expOutput: `c:\another\base\path.json#/definitions/Pet`, + }, + { + // file basePath, relative refPath + refPath: `another\base\path.json#/definitions/Pet`, + base: `C:\base\path.json`, + expOutput: `c:\base\another\base\path.json#/definitions/Pet`, + }, + }...) + return testCases + } + // linux case + testCases = append(testCases, testNormalizePathsTestCases{ + { + // file basePath, absolute refPath, no fragment + refPath: "/another/base/path.json", + base: "/base/path.json", + expOutput: "/another/base/path.json", + }, + { + // file basePath, absolute refPath + refPath: "/another/base/path.json#/definitions/Pet", + base: "/base/path.json", + expOutput: "/another/base/path.json#/definitions/Pet", + }, + { + // file basePath, relative refPath + refPath: "another/base/path.json#/definitions/Pet", + base: "/base/path.json", + expOutput: "/base/another/base/path.json#/definitions/Pet", + }, + }...) + return testCases + }() + + for _, tcase := range testCases { + out := normalizePaths(tcase.refPath, tcase.base) + assert.Equal(t, tcase.expOutput, out) + } +} diff --git a/resolver.go b/resolver.go new file mode 100644 index 0000000..7117050 --- /dev/null +++ b/resolver.go @@ -0,0 +1,131 @@ +package spec + +import ( + "github.com/go-openapi/swag" +) + +func resolveAnyWithBase(root interface{}, ref *Ref, result interface{}, options *ExpandOptions) error { + resolver, err := defaultSchemaLoader(root, options, nil, nil) + if err != nil { + return err + } + + basePath := "" + if options != nil && options.RelativeBase != "" { + basePath, _ = absPath(options.RelativeBase) + } + + if err := resolver.Resolve(ref, result, basePath); err != nil { + return err + } + + return nil +} + +// ResolveRefWithBase resolves a reference against a context root with preservation of base path +func ResolveRefWithBase(root interface{}, ref *Ref, options *ExpandOptions) (*Schema, error) { + result := new(Schema) + err := resolveAnyWithBase(root, ref, result, options) + if err != nil { + return nil, err + } + + return result, nil +} + +// ResolveRef resolves a reference for a schema against a context root +// ref is guaranteed to be in root (no need to go to external files) +// +// ResolveRef is ONLY called from the code generation module +func ResolveRef(root interface{}, ref *Ref) (*Schema, error) { + res, _, err := ref.GetPointer().Get(root) + if err != nil { + return nil, err + } + + switch sch := res.(type) { + case Schema: + return &sch, nil + case *Schema: + return sch, nil + case map[string]interface{}: + newSch := new(Schema) + if err = swag.DynamicJSONToStruct(sch, newSch); err != nil { + return nil, err + } + return newSch, nil + default: + return nil, ErrUnknownTypeForReference + } +} + +// ResolveParameterWithBase resolves a parameter reference against a context root and base path +func ResolveParameterWithBase(root interface{}, ref Ref, options *ExpandOptions) (*Parameter, error) { + result := new(Parameter) + err := resolveAnyWithBase(root, &ref, result, options) + if err != nil { + return nil, err + } + + return result, nil +} + +// ResolveParameter resolves a parameter reference against a context root +func ResolveParameter(root interface{}, ref Ref) (*Parameter, error) { + return ResolveParameterWithBase(root, ref, nil) +} + +// ResolveResponseWithBase resolves response a reference against a context root and base path +func ResolveResponseWithBase(root interface{}, ref Ref, options *ExpandOptions) (*Response, error) { + result := new(Response) + err := resolveAnyWithBase(root, &ref, result, options) + if err != nil { + return nil, err + } + + return result, nil +} + +// ResolveResponse resolves response a reference against a context root +func ResolveResponse(root interface{}, ref Ref) (*Response, error) { + return ResolveResponseWithBase(root, ref, nil) +} + +// ResolvePathItemWithBase resolves response a path item against a context root and base path +func ResolvePathItemWithBase(root interface{}, ref Ref, options *ExpandOptions) (*PathItem, error) { + result := new(PathItem) + err := resolveAnyWithBase(root, &ref, result, options) + if err != nil { + return nil, err + } + + return result, nil +} + +// ResolvePathItem resolves response a path item against a context root and base path +// +// @deprecated: use ResolvePathItemWithBase instead +func ResolvePathItem(root interface{}, ref Ref, options *ExpandOptions) (*PathItem, error) { + return ResolvePathItemWithBase(root, ref, options) +} + +// ResolveItemsWithBase resolves parameter items reference against a context root and base path. +// +// NOTE: stricly speaking, this construct is not supported by Swagger 2.0. +// Similarly, $ref are forbidden in response headers. +func ResolveItemsWithBase(root interface{}, ref Ref, options *ExpandOptions) (*Items, error) { + result := new(Items) + err := resolveAnyWithBase(root, &ref, result, options) + if err != nil { + return nil, err + } + + return result, nil +} + +// ResolveItems resolves parameter items reference against a context root and base path. +// +// @deprecated: use ResolveItemsWithBase instead +func ResolveItems(root interface{}, ref Ref, options *ExpandOptions) (*Items, error) { + return ResolveItemsWithBase(root, ref, options) +} diff --git a/resolver_test.go b/resolver_test.go new file mode 100644 index 0000000..e202d59 --- /dev/null +++ b/resolver_test.go @@ -0,0 +1,451 @@ +package spec + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + "github.com/go-openapi/jsonpointer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResolveRef(t *testing.T) { + var root interface{} + require.NoError(t, json.Unmarshal([]byte(PetStore20), &root)) + + ref, err := NewRef("#/definitions/Category") + require.NoError(t, err) + + sch, err := ResolveRef(root, &ref) + require.NoError(t, err) + + b, err := sch.MarshalJSON() + require.NoError(t, err) + + assert.JSONEq(t, `{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}`, string(b)) + + // WithBase variant + sch, err = ResolveRefWithBase(root, &ref, &ExpandOptions{ + RelativeBase: "/", + }) + require.NoError(t, err) + + b, err = sch.MarshalJSON() + require.NoError(t, err) + + assert.JSONEq(t, `{"id":"Category","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}}}`, string(b)) +} + +func TestResolveResponse(t *testing.T) { + specDoc, err := jsonDoc("fixtures/expansion/all-the-things.json") + require.NoError(t, err) + + spec := new(Swagger) + require.NoError(t, json.Unmarshal(specDoc, spec)) + + // Resolve with root version + resp := spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200] + resp2, err := ResolveResponse(spec, resp.Ref) + require.NoError(t, err) + + // resolve resolves the ref, but dos not expand + jazon, err := json.MarshalIndent(resp2, "", " ") + require.NoError(t, err) + + assert.JSONEq(t, `{ + "$ref": "#/responses/petResponse" + }`, string(jazon)) +} + +func TestResolveResponseWithBase(t *testing.T) { + specDoc, err := jsonDoc(crossFileRefFixture) + require.NoError(t, err) + + spec := new(Swagger) + require.NoError(t, json.Unmarshal(specDoc, spec)) + + // Resolve with root version + resp := spec.Paths.Paths["/"].Get.Responses.StatusCodeResponses[200] + resp2, err := ResolveResponseWithBase(spec, resp.Ref, &ExpandOptions{RelativeBase: crossFileRefFixture}) + require.NoError(t, err) + + // resolve resolves the ref, but dos not expand + jazon, err := json.MarshalIndent(resp2, "", " ") + require.NoError(t, err) + + assert.JSONEq(t, `{ + "$ref": "#/responses/petResponse" + }`, string(jazon)) +} + +func TestResolveParam(t *testing.T) { + specDoc, err := jsonDoc("fixtures/expansion/all-the-things.json") + require.NoError(t, err) + + var spec Swagger + require.NoError(t, json.Unmarshal(specDoc, &spec)) + + param := spec.Paths.Paths["/pets/{id}"].Get.Parameters[0] + par, err := ResolveParameter(spec, param.Ref) + require.NoError(t, err) + + jazon, err := json.MarshalIndent(par, "", " ") + require.NoError(t, err) + + assert.JSONEq(t, `{ + "name": "id", + "in": "path", + "description": "ID of pet to fetch", + "required": true, + "type": "integer", + "format": "int64" + }`, string(jazon)) +} + +func TestResolveParamWithBase(t *testing.T) { + specDoc, err := jsonDoc(crossFileRefFixture) + require.NoError(t, err) + + var spec Swagger + require.NoError(t, json.Unmarshal(specDoc, &spec)) + + param := spec.Paths.Paths["/pets"].Get.Parameters[0] + par, err := ResolveParameterWithBase(spec, param.Ref, &ExpandOptions{RelativeBase: crossFileRefFixture}) + require.NoError(t, err) + + jazon, err := json.MarshalIndent(par, "", " ") + require.NoError(t, err) + + assert.JSONEq(t, `{ +"description":"ID of pet to fetch", +"format":"int64", +"in":"path", +"name":"id", +"required":true, +"type":"integer" +}`, string(jazon)) +} + +func TestResolveRemoteRef_RootSame(t *testing.T) { + fileserver := http.FileServer(http.Dir(specs)) + server := httptest.NewServer(fileserver) + defer server.Close() + + rootDoc := new(Swagger) + b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + + // the filename doesn't matter because ref will eventually point to refed.json + specBase, _ := absPath(filepath.Join(specs, "anyotherfile.json")) + require.NoError(t, json.Unmarshal(b, rootDoc)) + + var result0 Swagger + ref0, _ := NewRef(server.URL + "/refed.json#") + resolver0, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + require.NoError(t, resolver0.Resolve(&ref0, &result0, "")) + assertSpecs(t, result0, *rootDoc) + + var result1 Swagger + ref1, _ := NewRef("./refed.json") + resolver1, _ := defaultSchemaLoader(rootDoc, &ExpandOptions{ + RelativeBase: specBase, + }, nil, nil) + require.NoError(t, resolver1.Resolve(&ref1, &result1, specBase)) + assertSpecs(t, result1, *rootDoc) +} + +func TestResolveRemoteRef_FromFragment(t *testing.T) { + fileserver := http.FileServer(http.Dir(specs)) + server := httptest.NewServer(fileserver) + defer server.Close() + + rootDoc := new(Swagger) + b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, rootDoc)) + + var tgt Schema + ref, err := NewRef(server.URL + "/refed.json#/definitions/pet") + require.NoError(t, err) + + resolver := &schemaLoader{root: rootDoc, cache: defaultResolutionCache(), context: &resolverContext{loadDoc: jsonDoc}} + require.NoError(t, resolver.Resolve(&ref, &tgt, "")) + assert.Equal(t, []string{"id", "name"}, tgt.Required) +} + +func TestResolveRemoteRef_FromInvalidFragment(t *testing.T) { + fileserver := http.FileServer(http.Dir(specs)) + server := httptest.NewServer(fileserver) + defer server.Close() + + rootDoc := new(Swagger) + b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, rootDoc)) + + var tgt Schema + ref, err := NewRef(server.URL + "/refed.json#/definitions/NotThere") + require.NoError(t, err) + + resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + assert.Error(t, resolver.Resolve(&ref, &tgt, "")) +} + +/* This next test will have to wait until we do full $ID analysis for every subschema on every file that is referenced */ +/* For now, TestResolveRemoteRef_WithNestedResolutionContext replaces this next test */ +// func TestResolveRemoteRef_WithNestedResolutionContextWithFragment_WithParentID(t *testing.T) { +// server := resolutionContextServer() +// defer server.Close() + +// rootDoc := new(Swagger) +// b, err := ioutil.ReadFile("fixtures/specs/refed.json") +// if assert.NoError(t, err) && assert.NoError(t, json.Unmarshal(b, rootDoc)) { +// var tgt Schema +// ref, err := NewRef(server.URL + "/resolution2.json#/items/items") +// if assert.NoError(t, err) { +// resolver, _ := defaultSchemaLoader(rootDoc, nil, nil,nil) +// if assert.NoError(t, resolver.Resolve(&ref, &tgt, "")) { +// assert.Equal(t, StringOrArray([]string{"file"}), tgt.Type) +// } +// } +// } +// } + +func TestResolveRemoteRef_ToParameter(t *testing.T) { + fileserver := http.FileServer(http.Dir(specs)) + server := httptest.NewServer(fileserver) + defer server.Close() + + rootDoc := new(Swagger) + b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, rootDoc)) + + var tgt Parameter + ref, err := NewRef(server.URL + "/refed.json#/parameters/idParam") + require.NoError(t, err) + + resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + require.NoError(t, resolver.Resolve(&ref, &tgt, "")) + + assert.Equal(t, "id", tgt.Name) + assert.Equal(t, "path", tgt.In) + assert.Equal(t, "ID of pet to fetch", tgt.Description) + assert.True(t, tgt.Required) + assert.Equal(t, "integer", tgt.Type) + assert.Equal(t, "int64", tgt.Format) +} + +func TestResolveRemoteRef_ToPathItem(t *testing.T) { + fileserver := http.FileServer(http.Dir(specs)) + server := httptest.NewServer(fileserver) + defer server.Close() + + rootDoc := new(Swagger) + b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, rootDoc)) + + var tgt PathItem + ref, err := NewRef(server.URL + "/refed.json#/paths/" + jsonpointer.Escape("/pets/{id}")) + require.NoError(t, err) + + resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + require.NoError(t, resolver.Resolve(&ref, &tgt, "")) + assert.Equal(t, rootDoc.Paths.Paths["/pets/{id}"].Get, tgt.Get) +} + +func TestResolveRemoteRef_ToResponse(t *testing.T) { + fileserver := http.FileServer(http.Dir(specs)) + server := httptest.NewServer(fileserver) + defer server.Close() + + rootDoc := new(Swagger) + b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, rootDoc)) + + var tgt Response + ref, err := NewRef(server.URL + "/refed.json#/responses/petResponse") + require.NoError(t, err) + + resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + require.NoError(t, resolver.Resolve(&ref, &tgt, "")) + assert.Equal(t, rootDoc.Responses["petResponse"], tgt) +} + +func TestResolveLocalRef_SameRoot(t *testing.T) { + rootDoc := new(Swagger) + require.NoError(t, json.Unmarshal(PetStoreJSONMessage, rootDoc)) + + result := new(Swagger) + ref, _ := NewRef("#") + resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + require.NoError(t, resolver.Resolve(&ref, result, "")) + assert.Equal(t, rootDoc, result) +} + +func TestResolveLocalRef_FromFragment(t *testing.T) { + rootDoc := new(Swagger) + require.NoError(t, json.Unmarshal(PetStoreJSONMessage, rootDoc)) + + var tgt Schema + ref, err := NewRef("#/definitions/Category") + require.NoError(t, err) + + resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + require.NoError(t, resolver.Resolve(&ref, &tgt, "")) + assert.Equal(t, "Category", tgt.ID) +} + +func TestResolveLocalRef_FromInvalidFragment(t *testing.T) { + rootDoc := new(Swagger) + require.NoError(t, json.Unmarshal(PetStoreJSONMessage, rootDoc)) + + var tgt Schema + ref, err := NewRef("#/definitions/NotThere") + require.NoError(t, err) + + resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + require.Error(t, resolver.Resolve(&ref, &tgt, "")) +} + +func TestResolveLocalRef_Parameter(t *testing.T) { + rootDoc := new(Swagger) + b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + + basePath, err := absPath(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, rootDoc)) + + var tgt Parameter + ref, err := NewRef("#/parameters/idParam") + require.NoError(t, err) + + resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + require.NoError(t, resolver.Resolve(&ref, &tgt, basePath)) + + assert.Equal(t, "id", tgt.Name) + assert.Equal(t, "path", tgt.In) + assert.Equal(t, "ID of pet to fetch", tgt.Description) + assert.True(t, tgt.Required) + assert.Equal(t, "integer", tgt.Type) + assert.Equal(t, "int64", tgt.Format) +} + +func TestResolveLocalRef_PathItem(t *testing.T) { + rootDoc := new(Swagger) + b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + + basePath, err := absPath(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, rootDoc)) + + var tgt PathItem + ref, err := NewRef("#/paths/" + jsonpointer.Escape("/pets/{id}")) + require.NoError(t, err) + + resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + require.NoError(t, resolver.Resolve(&ref, &tgt, basePath)) + assert.Equal(t, rootDoc.Paths.Paths["/pets/{id}"].Get, tgt.Get) +} + +func TestResolveLocalRef_Response(t *testing.T) { + rootDoc := new(Swagger) + b, err := ioutil.ReadFile(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + + basePath, err := absPath(filepath.Join(specs, "refed.json")) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, rootDoc)) + + var tgt Response + ref, err := NewRef("#/responses/petResponse") + require.NoError(t, err) + + resolver, _ := defaultSchemaLoader(rootDoc, nil, nil, nil) + require.NoError(t, resolver.Resolve(&ref, &tgt, basePath)) + assert.Equal(t, rootDoc.Responses["petResponse"], tgt) +} + +func TestResolvePathItem(t *testing.T) { + spec := new(Swagger) + specDoc, err := jsonDoc(pathItemsFixture) + require.NoError(t, err) + + require.NoError(t, json.Unmarshal(specDoc, spec)) + + specPath, err := absPath(pathItemsFixture) + require.NoError(t, err) + + // Resolve use case + pth := spec.Paths.Paths["/todos"] + pathItem, err := ResolvePathItem(spec, pth.Ref, &ExpandOptions{RelativeBase: specPath}) + require.NoError(t, err) + + jazon, err := json.MarshalIndent(pathItem, "", " ") + require.NoError(t, err) + + assert.JSONEq(t, `{ + "get": { + "responses": { + "200": { + "description": "List Todos", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "404": { + "description": "error" + } + } + } + }`, string(jazon)) +} + +func TestResolveExtraItem(t *testing.T) { + // go-openapi extra goodie: $ref in simple schema Items and Headers + spec := new(Swagger) + specDoc, err := jsonDoc(extraRefFixture) + require.NoError(t, err) + + require.NoError(t, json.Unmarshal(specDoc, spec)) + + specPath, err := absPath(extraRefFixture) + require.NoError(t, err) + + // Resolve param Items use case: here we explicitly resolve the unsuppord case + parm := spec.Paths.Paths["/employees"].Get.Parameters[0] + parmItem, err := ResolveItems(spec, parm.Items.Ref, &ExpandOptions{RelativeBase: specPath}) + require.NoError(t, err) + + jazon, err := json.MarshalIndent(parmItem, "", " ") + require.NoError(t, err) + + assert.JSONEq(t, `{ + "type": "integer", + "format": "int32" + }`, string(jazon)) + + // Resolve header Items use case: here we explicitly resolve the unsuppord case + hdr := spec.Paths.Paths["/employees"].Get.Responses.StatusCodeResponses[200].Headers["X-header"] + hdrItem, err := ResolveItems(spec, hdr.Items.Ref, &ExpandOptions{RelativeBase: specPath}) + require.NoError(t, err) + + jazon, err = json.MarshalIndent(hdrItem, "", " ") + require.NoError(t, err) + + assert.JSONEq(t, `{ + "type": "string", + "format": "uuid" + }`, string(jazon)) +} diff --git a/schema_loader.go b/schema_loader.go index def3b05..1d9e944 100644 --- a/schema_loader.go +++ b/schema_loader.go @@ -106,16 +106,13 @@ func (r *schemaLoader) transitiveResolver(basePath string, ref Ref) (*schemaLoad // traversing multiple documents newOptions := r.options newOptions.RelativeBase = rootURL.String() - debugLog("setting new root: %s", newOptions.RelativeBase) return defaultSchemaLoader(root, newOptions, r.cache, r.context) } func (r *schemaLoader) updateBasePath(transitive *schemaLoader, basePath string) string { if transitive != r { - debugLog("got a new resolver") if transitive.options != nil && transitive.options.RelativeBase != "" { basePath, _ = absPath(transitive.options.RelativeBase) - debugLog("new basePath = %s", basePath) } } return basePath @@ -124,17 +121,19 @@ func (r *schemaLoader) updateBasePath(transitive *schemaLoader, basePath string) func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) error { tgt := reflect.ValueOf(target) if tgt.Kind() != reflect.Ptr { - return fmt.Errorf("resolve ref: target needs to be a pointer") + return ErrResolveRefNeedsAPointer } - refURL := ref.GetURL() - if refURL == nil { + if ref.GetURL() == nil { return nil } - var res interface{} - var data interface{} - var err error + var ( + res interface{} + data interface{} + err error + ) + // Resolve against the root if it isn't nil, and if ref is pointing at the root, or has a fragment only which means // it is pointing somewhere in the root. root := r.root @@ -143,12 +142,11 @@ func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) root, _, _, _ = r.load(baseRef.GetURL()) } } + if (ref.IsRoot() || ref.HasFragmentOnly) && root != nil { data = root } else { baseRef := normalizeFileRef(ref, basePath) - debugLog("current ref is: %s", ref.String()) - debugLog("current ref normalized file: %s", baseRef.String()) data, _, _, err = r.load(baseRef.GetURL()) if err != nil { return err @@ -232,30 +230,32 @@ func (r *schemaLoader) deref(input interface{}, parentRefs []string, basePath st case *PathItem: ref = &refable.Ref default: - return fmt.Errorf("deref: unsupported type %T", input) + return fmt.Errorf("unsupported type: %T: %w", input, ErrDerefUnsupportedType) } curRef := ref.String() - if curRef != "" { - normalizedRef := normalizeFileRef(ref, basePath) - normalizedBasePath := normalizedRef.RemoteURI() + if curRef == "" { + return nil + } - if r.isCircular(normalizedRef, basePath, parentRefs...) { - return nil - } + normalizedRef := normalizeFileRef(ref, basePath) + normalizedBasePath := normalizedRef.RemoteURI() - if err := r.resolveRef(ref, input, basePath); r.shouldStopOnError(err) { - return err - } + if r.isCircular(normalizedRef, basePath, parentRefs...) { + return nil + } - // NOTE(fredbi): removed basePath check => needs more testing - if ref.String() != "" && ref.String() != curRef { - parentRefs = append(parentRefs, normalizedRef.String()) - return r.deref(input, parentRefs, normalizedBasePath) - } + if err := r.resolveRef(ref, input, basePath); r.shouldStopOnError(err) { + return err + } + + if ref.String() == "" || ref.String() == curRef { + // done with rereferencing + return nil } - return nil + parentRefs = append(parentRefs, normalizedRef.String()) + return r.deref(input, parentRefs, normalizedBasePath) } func (r *schemaLoader) shouldStopOnError(err error) bool {