diff --git a/.gitignore b/.gitignore index a5bf17cb5..31ab03fb6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,3 @@ # IntelliJ / GoLand .idea - diff --git a/openapi3/callback.go b/openapi3/callback.go index 334233104..b216d1a9d 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -13,7 +13,7 @@ var _ jsonpointer.JSONPointable = (*Callbacks)(nil) func (c Callbacks) JSONLookup(token string) (interface{}, error) { ref, ok := c[token] - if ref == nil || ok == false { + if ref == nil || !ok { return nil, fmt.Errorf("object has no field %q", token) } diff --git a/openapi3/examples.go b/openapi3/examples.go index 5f6255bf3..98f79b884 100644 --- a/openapi3/examples.go +++ b/openapi3/examples.go @@ -13,7 +13,7 @@ var _ jsonpointer.JSONPointable = (*Examples)(nil) func (e Examples) JSONLookup(token string) (interface{}, error) { ref, ok := e[token] - if ref == nil || ok == false { + if ref == nil || !ok { return nil, fmt.Errorf("object has no field %q", token) } diff --git a/openapi3/header.go b/openapi3/header.go index 3adb2ea5a..bab30a412 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -14,7 +14,7 @@ var _ jsonpointer.JSONPointable = (*Headers)(nil) func (h Headers) JSONLookup(token string) (interface{}, error) { ref, ok := h[token] - if ref == nil || ok == false { + if ref == nil || !ok { return nil, fmt.Errorf("object has no field %q", token) } diff --git a/openapi3/issue344_test.go b/openapi3/issue344_test.go new file mode 100644 index 000000000..8a53394c5 --- /dev/null +++ b/openapi3/issue344_test.go @@ -0,0 +1,20 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue344(t *testing.T) { + sl := NewSwaggerLoader() + sl.IsExternalRefsAllowed = true + + doc, err := sl.LoadSwaggerFromFile("testdata/spec.yaml") + require.NoError(t, err) + + err = doc.Validate(sl.Context) + require.NoError(t, err) + + require.Equal(t, "string", doc.Components.Schemas["Test"].Value.Properties["test"].Value.Properties["name"].Value.Type) +} diff --git a/openapi3/schema.go b/openapi3/schema.go index 9c3103a27..22cb84210 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -1266,7 +1266,7 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ Value: value, Schema: schema, SchemaField: "uniqueItems", - Reason: fmt.Sprintf("duplicate items found"), + Reason: "duplicate items found", } if !settings.multiError { return err diff --git a/openapi3/swagger_loader.go b/openapi3/swagger_loader.go index 8ae8d0406..2cb951e53 100644 --- a/openapi3/swagger_loader.go +++ b/openapi3/swagger_loader.go @@ -63,6 +63,11 @@ func (swaggerLoader *SwaggerLoader) LoadSwaggerFromURI(location *url.URL) (*Swag return swaggerLoader.loadSwaggerFromURIInternal(location) } +// LoadSwaggerFromFile loads a spec from a local file path +func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger, error) { + return swaggerLoader.LoadSwaggerFromURI(&url.URL{Path: path}) +} + func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL) (*Swagger, error) { data, err := swaggerLoader.readURL(location) if err != nil { @@ -71,35 +76,41 @@ func (swaggerLoader *SwaggerLoader) loadSwaggerFromURIInternal(location *url.URL return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, location) } -// loadSingleElementFromURI reads the data from ref and unmarshals to the passed element. -func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element json.Unmarshaler) error { +func (swaggerLoader *SwaggerLoader) allowsExternalRefs(ref string) (err error) { if !swaggerLoader.IsExternalRefsAllowed { - return fmt.Errorf("encountered non-allowed external reference: %q", ref) + err = fmt.Errorf("encountered disallowed external reference: %q", ref) + } + return +} + +// loadSingleElementFromURI reads the data from ref and unmarshals to the passed element. +func (swaggerLoader *SwaggerLoader) loadSingleElementFromURI(ref string, rootPath *url.URL, element json.Unmarshaler) (*url.URL, error) { + if err := swaggerLoader.allowsExternalRefs(ref); err != nil { + return nil, err } parsedURL, err := url.Parse(ref) if err != nil { - return err + return nil, err } - - if parsedURL.Fragment != "" { - return errors.New("references to files which contain more than one element definition are not supported") + if fragment := parsedURL.Fragment; fragment != "" { + return nil, fmt.Errorf("unexpected ref fragment %q", fragment) } resolvedPath, err := resolvePath(rootPath, parsedURL) if err != nil { - return fmt.Errorf("could not resolve path: %v", err) + return nil, fmt.Errorf("could not resolve path: %v", err) } data, err := swaggerLoader.readURL(resolvedPath) if err != nil { - return err + return nil, err } if err := yaml.Unmarshal(data, element); err != nil { - return err + return nil, err } - return nil + return resolvedPath, nil } func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { @@ -121,28 +132,9 @@ func (swaggerLoader *SwaggerLoader) readURL(location *url.URL) ([]byte, error) { return ioutil.ReadFile(location.Path) } -// LoadSwaggerFromFile loads a spec from a local file path -func (swaggerLoader *SwaggerLoader) LoadSwaggerFromFile(path string) (*Swagger, error) { - swaggerLoader.resetVisitedPathItemRefs() - return swaggerLoader.loadSwaggerFromFileInternal(path) -} - -func (swaggerLoader *SwaggerLoader) loadSwaggerFromFileInternal(path string) (*Swagger, error) { - pathAsURL := &url.URL{Path: path} - data, err := swaggerLoader.readURL(pathAsURL) - if err != nil { - return nil, err - } - return swaggerLoader.loadSwaggerFromDataWithPathInternal(data, pathAsURL) -} - // LoadSwaggerFromData loads a spec from a byte array func (swaggerLoader *SwaggerLoader) LoadSwaggerFromData(data []byte) (*Swagger, error) { swaggerLoader.resetVisitedPathItemRefs() - return swaggerLoader.loadSwaggerFromDataInternal(data) -} - -func (swaggerLoader *SwaggerLoader) loadSwaggerFromDataInternal(data []byte) (*Swagger, error) { doc := &Swagger{} if err := yaml.Unmarshal(data, doc); err != nil { return nil, err @@ -239,15 +231,11 @@ func (swaggerLoader *SwaggerLoader) ResolveRefsIn(swagger *Swagger, path *url.UR return } -func copyURL(basePath *url.URL) (*url.URL, error) { - return url.Parse(basePath.String()) -} - func join(basePath *url.URL, relativePath *url.URL) (*url.URL, error) { if basePath == nil { return relativePath, nil } - newPath, err := copyURL(basePath) + newPath, err := url.Parse(basePath.String()) if err != nil { return nil, fmt.Errorf("cannot copy path: %q", basePath.String()) } @@ -292,18 +280,34 @@ func (swaggerLoader *SwaggerLoader) resolveComponent( return nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref) } - var cursor interface{} - cursor = swagger - for _, pathPart := range strings.Split(fragment[1:], "/") { - pathPart = unescapeRefString(pathPart) + drill := func(cursor interface{}) (interface{}, error) { + for _, pathPart := range strings.Split(fragment[1:], "/") { + pathPart = unescapeRefString(pathPart) - if cursor, err = drillIntoSwaggerField(cursor, pathPart); err != nil { - e := failedToResolveRefFragmentPart(ref, pathPart) - return nil, fmt.Errorf("%s: %s", e.Error(), err.Error()) + if cursor, err = drillIntoSwaggerField(cursor, pathPart); err != nil { + e := failedToResolveRefFragmentPart(ref, pathPart) + return nil, fmt.Errorf("%s: %s", e.Error(), err.Error()) + } + if cursor == nil { + return nil, failedToResolveRefFragmentPart(ref, pathPart) + } + } + return cursor, nil + } + var cursor interface{} + if cursor, err = drill(swagger); err != nil { + var err2 error + data, err2 := swaggerLoader.readURL(path) + if err2 != nil { + return nil, err } - if cursor == nil { - return nil, failedToResolveRefFragmentPart(ref, pathPart) + if err2 = yaml.Unmarshal(data, &cursor); err2 != nil { + return nil, err + } + if cursor, err2 = drill(cursor); err2 != nil || cursor == nil { + return nil, err } + err = nil } switch { @@ -388,33 +392,34 @@ func drillIntoSwaggerField(cursor interface{}, fieldName string) (interface{}, e } func (swaggerLoader *SwaggerLoader) resolveRefSwagger(swagger *Swagger, ref string, path *url.URL) (*Swagger, string, *url.URL, error) { - componentPath := path - if !strings.HasPrefix(ref, "#") { - if !swaggerLoader.IsExternalRefsAllowed { - return nil, "", nil, fmt.Errorf("encountered non-allowed external reference: %q", ref) - } - parsedURL, err := url.Parse(ref) - if err != nil { - return nil, "", nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) - } - fragment := parsedURL.Fragment - parsedURL.Fragment = "" + if ref != "" && ref[0] == '#' { + return swagger, ref, path, nil + } - resolvedPath, err := resolvePath(path, parsedURL) - if err != nil { - return nil, "", nil, fmt.Errorf("error resolving path: %v", err) - } + if err := swaggerLoader.allowsExternalRefs(ref); err != nil { + return nil, "", nil, err + } - if swagger, err = swaggerLoader.loadSwaggerFromURIInternal(resolvedPath); err != nil { - return nil, "", nil, fmt.Errorf("error resolving reference %q: %v", ref, err) - } - ref = "#" + fragment - componentPath = resolvedPath + parsedURL, err := url.Parse(ref) + if err != nil { + return nil, "", nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL) + } + fragment := parsedURL.Fragment + parsedURL.Fragment = "" + + var resolvedPath *url.URL + if resolvedPath, err = resolvePath(path, parsedURL); err != nil { + return nil, "", nil, fmt.Errorf("error resolving path: %v", err) + } + + if swagger, err = swaggerLoader.loadSwaggerFromURIInternal(resolvedPath); err != nil { + return nil, "", nil, fmt.Errorf("error resolving reference %q: %v", ref, err) } - return swagger, ref, componentPath, nil + + return swagger, "#" + fragment, resolvedPath, nil } -func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component *HeaderRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedHeader == nil { swaggerLoader.visitedHeader = make(map[*Header]struct{}) @@ -425,17 +430,15 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component swaggerLoader.visitedHeader[component.Value] = struct{}{} } - const prefix = "#/components/headers/" if component == nil { return errors.New("invalid header: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var header Header - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &header); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &header); err != nil { return err } - component.Value = &header } else { var resolved HeaderRef @@ -453,6 +456,7 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component if value == nil { return nil } + if schema := value.Schema; schema != nil { if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { return err @@ -461,7 +465,7 @@ func (swaggerLoader *SwaggerLoader) resolveHeaderRef(swagger *Swagger, component return nil } -func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, component *ParameterRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedParameter == nil { swaggerLoader.visitedParameter = make(map[*Parameter]struct{}) @@ -472,7 +476,6 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon swaggerLoader.visitedParameter[component.Value] = struct{}{} } - const prefix = "#/components/parameters/" if component == nil { return errors.New("invalid parameter: value MUST be an object") } @@ -480,7 +483,7 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon if ref != "" { if isSingleRefElement(ref) { var param Parameter - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil { return err } component.Value = ¶m @@ -501,30 +504,25 @@ func (swaggerLoader *SwaggerLoader) resolveParameterRef(swagger *Swagger, compon return nil } - refDocumentPath, err := referencedDocumentPath(documentPath, ref) - if err != nil { - return err - } - if value.Content != nil && value.Schema != nil { return errors.New("cannot contain both schema and content in a parameter") } for _, contentType := range value.Content { if schema := contentType.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { return err } } } if schema := value.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { return err } } return nil } -func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, component *RequestBodyRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, component *RequestBodyRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedRequestBody == nil { swaggerLoader.visitedRequestBody = make(map[*RequestBody]struct{}) @@ -535,17 +533,15 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp swaggerLoader.visitedRequestBody[component.Value] = struct{}{} } - const prefix = "#/components/requestBodies/" if component == nil { return errors.New("invalid requestBody: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var requestBody RequestBody - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil { return err } - component.Value = &requestBody } else { var resolved RequestBodyRef @@ -563,6 +559,7 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp if value == nil { return nil } + for _, contentType := range value.Content { for name, example := range contentType.Examples { if err := swaggerLoader.resolveExampleRef(swagger, example, documentPath); err != nil { @@ -579,7 +576,7 @@ func (swaggerLoader *SwaggerLoader) resolveRequestBodyRef(swagger *Swagger, comp return nil } -func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, component *ResponseRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedResponse == nil { swaggerLoader.visitedResponse = make(map[*Response]struct{}) @@ -590,7 +587,6 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone swaggerLoader.visitedResponse[component.Value] = struct{}{} } - const prefix = "#/components/responses/" if component == nil { return errors.New("invalid response: value MUST be an object") } @@ -598,7 +594,7 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone if ref != "" { if isSingleRefElement(ref) { var resp Response - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil { return err } component.Value = &resp @@ -614,17 +610,13 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone component.Value = resolved.Value } } - refDocumentPath, err := referencedDocumentPath(documentPath, ref) - if err != nil { - return err - } - value := component.Value if value == nil { return nil } + for _, header := range value.Headers { - if err := swaggerLoader.resolveHeaderRef(swagger, header, refDocumentPath); err != nil { + if err := swaggerLoader.resolveHeaderRef(swagger, header, documentPath); err != nil { return err } } @@ -633,27 +625,27 @@ func (swaggerLoader *SwaggerLoader) resolveResponseRef(swagger *Swagger, compone continue } for name, example := range contentType.Examples { - if err := swaggerLoader.resolveExampleRef(swagger, example, refDocumentPath); err != nil { + if err := swaggerLoader.resolveExampleRef(swagger, example, documentPath); err != nil { return err } contentType.Examples[name] = example } if schema := contentType.Schema; schema != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, schema, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, schema, documentPath); err != nil { return err } contentType.Schema = schema } } for _, link := range value.Links { - if err := swaggerLoader.resolveLinkRef(swagger, link, refDocumentPath); err != nil { + if err := swaggerLoader.resolveLinkRef(swagger, link, documentPath); err != nil { return err } } return nil } -func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component *SchemaRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedSchema == nil { swaggerLoader.visitedSchema = make(map[*Schema]struct{}) @@ -664,7 +656,6 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component swaggerLoader.visitedSchema[component.Value] = struct{}{} } - const prefix = "#/components/schemas/" if component == nil { return errors.New("invalid schema: value MUST be an object") } @@ -672,7 +663,7 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component if ref != "" { if isSingleRefElement(ref) { var schema Schema - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil { return err } component.Value = &schema @@ -688,12 +679,6 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component component.Value = resolved.Value } } - - refDocumentPath, err := referencedDocumentPath(documentPath, ref) - if err != nil { - return err - } - value := component.Value if value == nil { return nil @@ -701,45 +686,44 @@ func (swaggerLoader *SwaggerLoader) resolveSchemaRef(swagger *Swagger, component // ResolveRefs referred schemas if v := value.Items; v != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } for _, v := range value.Properties { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } if v := value.AdditionalProperties; v != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } if v := value.Not; v != nil { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } for _, v := range value.AllOf { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } for _, v := range value.AnyOf { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } for _, v := range value.OneOf { - if err := swaggerLoader.resolveSchemaRef(swagger, v, refDocumentPath); err != nil { + if err := swaggerLoader.resolveSchemaRef(swagger, v, documentPath); err != nil { return err } } - return nil } -func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, component *SecuritySchemeRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, component *SecuritySchemeRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedSecurityScheme == nil { swaggerLoader.visitedSecurityScheme = make(map[*SecurityScheme]struct{}) @@ -750,17 +734,15 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c swaggerLoader.visitedSecurityScheme[component.Value] = struct{}{} } - const prefix = "#/components/securitySchemes/" if component == nil { return errors.New("invalid securityScheme: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var scheme SecurityScheme - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { return err } - component.Value = &scheme } else { var resolved SecuritySchemeRef @@ -777,7 +759,7 @@ func (swaggerLoader *SwaggerLoader) resolveSecuritySchemeRef(swagger *Swagger, c return nil } -func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, component *ExampleRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, component *ExampleRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedExample == nil { swaggerLoader.visitedExample = make(map[*Example]struct{}) @@ -788,17 +770,15 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen swaggerLoader.visitedExample[component.Value] = struct{}{} } - const prefix = "#/components/examples/" if component == nil { return errors.New("invalid example: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var example Example - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { return err } - component.Value = &example } else { var resolved ExampleRef @@ -815,7 +795,7 @@ func (swaggerLoader *SwaggerLoader) resolveExampleRef(swagger *Swagger, componen return nil } -func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, documentPath *url.URL) error { +func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component *LinkRef, documentPath *url.URL) (err error) { if component != nil && component.Value != nil { if swaggerLoader.visitedLink == nil { swaggerLoader.visitedLink = make(map[*Link]struct{}) @@ -826,17 +806,15 @@ func (swaggerLoader *SwaggerLoader) resolveLinkRef(swagger *Swagger, component * swaggerLoader.visitedLink[component.Value] = struct{}{} } - const prefix = "#/components/links/" if component == nil { return errors.New("invalid link: value MUST be an object") } if ref := component.Ref; ref != "" { if isSingleRefElement(ref) { var link Link - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { return err } - component.Value = &link } else { var resolved LinkRef @@ -864,7 +842,6 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo } swaggerLoader.visitedPathItemRefs[key] = struct{}{} - const prefix = "#/paths/" if pathItem == nil { return errors.New("invalid path item: value MUST be an object") } @@ -872,7 +849,7 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo if ref != "" { if isSingleRefElement(ref) { var p PathItem - if err := swaggerLoader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { + if documentPath, err = swaggerLoader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { return err } *pathItem = p @@ -881,11 +858,11 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo return } - if !strings.HasPrefix(ref, prefix) { - err = fmt.Errorf("expected prefix %q in URI %q", prefix, ref) - return + rest := strings.TrimPrefix(ref, "#/paths/") + if rest == ref { + return fmt.Errorf(`expected prefix "#/paths/" in URI %q`, ref) } - id := unescapeRefString(ref[len(prefix):]) + id := unescapeRefString(rest) definitions := swagger.Paths if definitions == nil { @@ -900,55 +877,31 @@ func (swaggerLoader *SwaggerLoader) resolvePathItemRef(swagger *Swagger, entrypo } } - refDocumentPath, err := referencedDocumentPath(documentPath, ref) - if err != nil { - return err - } - for _, parameter := range pathItem.Parameters { - if err = swaggerLoader.resolveParameterRef(swagger, parameter, refDocumentPath); err != nil { + if err = swaggerLoader.resolveParameterRef(swagger, parameter, documentPath); err != nil { return } } for _, operation := range pathItem.Operations() { for _, parameter := range operation.Parameters { - if err = swaggerLoader.resolveParameterRef(swagger, parameter, refDocumentPath); err != nil { + if err = swaggerLoader.resolveParameterRef(swagger, parameter, documentPath); err != nil { return } } if requestBody := operation.RequestBody; requestBody != nil { - if err = swaggerLoader.resolveRequestBodyRef(swagger, requestBody, refDocumentPath); err != nil { + if err = swaggerLoader.resolveRequestBodyRef(swagger, requestBody, documentPath); err != nil { return } } for _, response := range operation.Responses { - if err = swaggerLoader.resolveResponseRef(swagger, response, refDocumentPath); err != nil { + if err = swaggerLoader.resolveResponseRef(swagger, response, documentPath); err != nil { return } } } - - return nil + return } func unescapeRefString(ref string) string { return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1) } - -func referencedDocumentPath(documentPath *url.URL, ref string) (*url.URL, error) { - if documentPath == nil { - return nil, nil - } - - newDocumentPath, err := copyURL(documentPath) - if err != nil { - return nil, err - } - refPath, err := url.Parse(ref) - if err != nil { - return nil, err - } - newDocumentPath.Path = path.Join(path.Dir(newDocumentPath.Path), path.Dir(refPath.Path)) + "/" - - return newDocumentPath, nil -} diff --git a/openapi3/swagger_loader_referenced_document_path_test.go b/openapi3/swagger_loader_referenced_document_path_test.go deleted file mode 100644 index de219c369..000000000 --- a/openapi3/swagger_loader_referenced_document_path_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package openapi3 - -import ( - "net/url" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestReferencedDocumentPath(t *testing.T) { - httpURL, err := url.Parse("http://example.com/path/to/schemas/test1.yaml") - require.NoError(t, err) - - fileURL, err := url.Parse("path/to/schemas/test1.yaml") - require.NoError(t, err) - - refEmpty := "" - refNoComponent := "moreschemas/test2.yaml" - refWithComponent := "moreschemas/test2.yaml#/components/schemas/someobject" - - for _, test := range []struct { - path *url.URL - ref, expected string - }{ - { - path: httpURL, - ref: refEmpty, - expected: "http://example.com/path/to/schemas/", - }, - { - path: httpURL, - ref: refNoComponent, - expected: "http://example.com/path/to/schemas/moreschemas/", - }, - { - path: httpURL, - ref: refWithComponent, - expected: "http://example.com/path/to/schemas/moreschemas/", - }, - { - path: fileURL, - ref: refEmpty, - expected: "path/to/schemas/", - }, - { - path: fileURL, - ref: refNoComponent, - expected: "path/to/schemas/moreschemas/", - }, - { - path: fileURL, - ref: refWithComponent, - expected: "path/to/schemas/moreschemas/", - }, - } { - result, err := referencedDocumentPath(test.path, test.ref) - require.NoError(t, err) - require.Equal(t, test.expected, result.String()) - } -} diff --git a/openapi3/swagger_loader_relative_refs_test.go b/openapi3/swagger_loader_relative_refs_test.go index 8f074680a..cb5ab3f05 100644 --- a/openapi3/swagger_loader_relative_refs_test.go +++ b/openapi3/swagger_loader_relative_refs_test.go @@ -814,25 +814,25 @@ func TestLoadSpecWithRelativeDocumentRefs(t *testing.T) { const relativeSchemaDocsRefTemplate = ` openapi: 3.0.0 -info: +info: title: "" version: "1.0" paths: {} -components: - schemas: - TestSchema: +components: + schemas: + TestSchema: $ref: relativeDocs/CustomTestSchema.yml ` const relativeResponseDocsRefTemplate = ` openapi: 3.0.0 -info: +info: title: "" version: "1.0" paths: {} -components: - responses: - TestResponse: +components: + responses: + TestResponse: $ref: relativeDocs/CustomTestResponse.yml ` @@ -844,7 +844,7 @@ info: paths: {} components: parameters: - TestParameter: + TestParameter: $ref: relativeDocs/CustomTestParameter.yml ` @@ -921,6 +921,8 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { // check header require.Equal(t, "header", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Rate-Limit-Reset"].Value.Description) + require.Equal(t, "header1", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Another"].Value.Description) + require.Equal(t, "header2", nestedDirPath.Patch.Responses["200"].Value.Headers["X-And-Another"].Value.Description) // check request body require.Equal(t, "example request", nestedDirPath.Patch.RequestBody.Value.Description) @@ -939,6 +941,8 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { // check header require.Equal(t, "header", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Rate-Limit-Reset"].Value.Description) + require.Equal(t, "header1", nestedDirPath.Patch.Responses["200"].Value.Headers["X-Another"].Value.Description) + require.Equal(t, "header2", nestedDirPath.Patch.Responses["200"].Value.Headers["X-And-Another"].Value.Description) // check request body require.Equal(t, "example request", moreNestedDirPath.Patch.RequestBody.Value.Description) diff --git a/openapi3/testdata/ext.json b/openapi3/testdata/ext.json new file mode 100644 index 000000000..df227e62e --- /dev/null +++ b/openapi3/testdata/ext.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "a": { + "type": "string" + }, + "b": { + "type": "object", + "description": "I use a local reference.", + "properties": { + "name": { + "$ref": "#/definitions/a" + } + } + } + } +} diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml index 9d12ac352..e5cf2b15b 100644 --- a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml +++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader.yml @@ -1 +1 @@ -description: header \ No newline at end of file +description: header diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml new file mode 100644 index 000000000..4a5d8f994 --- /dev/null +++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1.yml @@ -0,0 +1 @@ +description: header1 diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml new file mode 100644 index 000000000..532e79203 --- /dev/null +++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader1bis.yml @@ -0,0 +1,2 @@ +header: + description: header1 diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml new file mode 100644 index 000000000..71536c564 --- /dev/null +++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2.yml @@ -0,0 +1 @@ +description: header2 diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml new file mode 100644 index 000000000..c14ae4710 --- /dev/null +++ b/openapi3/testdata/relativeDocsUseDocumentPath/CustomTestHeader2bis.yml @@ -0,0 +1,2 @@ +header: + description: header2 diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml index 45046c421..6e608808c 100644 --- a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml +++ b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/CustomTestPath.yml @@ -10,6 +10,10 @@ patch: headers: X-Rate-Limit-Reset: $ref: "../../../CustomTestHeader.yml" + X-Another: + $ref: ../../../CustomTestHeader1.yml + X-And-Another: + $ref: ../../../CustomTestHeader2.yml content: application/json: schema: diff --git a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml index 647852fc5..35c5ccf51 100644 --- a/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml +++ b/openapi3/testdata/relativeDocsUseDocumentPath/openapi/paths/nesteddir/morenested/CustomTestPath.yml @@ -10,6 +10,10 @@ patch: headers: X-Rate-Limit-Reset: $ref: "../../../../CustomTestHeader.yml" + X-Another: + $ref: '../../../../CustomTestHeader1bis.yml#/header' + X-And-Another: + $ref: '../../../../CustomTestHeader2bis.yml#/header' content: application/json: schema: diff --git a/openapi3/testdata/spec.yaml b/openapi3/testdata/spec.yaml new file mode 100644 index 000000000..781312f8e --- /dev/null +++ b/openapi3/testdata/spec.yaml @@ -0,0 +1,14 @@ +openapi: 3.0.1 +info: + version: 1.0.0 + title: Some Swagger + license: + name: MIT +paths: {} +components: + schemas: + Test: + type: object + properties: + test: + $ref: 'ext.json#/definitions/b' diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go index 47f9cd9f0..707b22d4a 100644 --- a/openapi3filter/validation_error_encoder.go +++ b/openapi3filter/validation_error_encoder.go @@ -152,7 +152,7 @@ func convertSchemaError(e *RequestError, innerErr *openapi3.SchemaError) *Valida strings.Join(enums, ", ")) value := fmt.Sprintf("%v", innerErr.Value) if e.Parameter != nil && - (e.Parameter.Explode == nil || *e.Parameter.Explode == true) && + (e.Parameter.Explode == nil || *e.Parameter.Explode) && (e.Parameter.Style == "" || e.Parameter.Style == "form") && strings.Contains(value, ",") { parts := strings.Split(value, ",") diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 1422017a5..a975b2a7a 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -25,7 +25,7 @@ func TestExportedNonTagged(t *testing.T) { type Bla struct { A string Another string `json:"another"` - yetAnother string + yetAnother string // unused because unexported EvenAYaml string `yaml:"even_a_yaml"` } diff --git a/routers/gorillamux/router.go b/routers/gorillamux/router.go index 6bdda1a17..896f5fa47 100644 --- a/routers/gorillamux/router.go +++ b/routers/gorillamux/router.go @@ -135,9 +135,7 @@ func orderedPaths(paths map[string]*openapi3.PathItem) []string { for c := 0; c <= max; c++ { if ps, ok := vars[c]; ok { sort.Strings(ps) - for _, p := range ps { - ordered = append(ordered, p) - } + ordered = append(ordered, ps...) } } return ordered