diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 3a7caf2c8..09f1192ab 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -96,7 +96,7 @@ FUNCTIONS func BoolPtr(value bool) *bool BoolPtr is a helper for defining OpenAPI schemas. -func DefaultRefNameResolver(doc *T, ref componentRef) string +func DefaultRefNameResolver(doc *T, ref ComponentRef) string DefaultRefResolver is a default implementation of refNameResolver for the InternalizeRefs function. @@ -150,7 +150,7 @@ func Int64Ptr(value int64) *int64 func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) ReadFromFile is a ReadFromURIFunc which reads local file URIs. -func ReferencesComponentInRootDocument(doc *T, ref componentRef) (string, bool) +func ReferencesComponentInRootDocument(doc *T, ref ComponentRef) (string, bool) ReferencesComponentInRootDocument returns if the given component reference references the same document or element as another component reference in the root document's '#/components/'. If it does, it returns the name @@ -316,6 +316,12 @@ func (m Callbacks) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +type ComponentRef interface { + RefString() string + RefPath() *url.URL + CollectionName() string +} + type Components struct { Extensions map[string]any `json:"-" yaml:"-"` @@ -1227,7 +1233,7 @@ type Ref struct { Ref is specified by OpenAPI/Swagger 3.0 standard. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object -type RefNameResolver func(*T, componentRef) string +type RefNameResolver func(*T, ComponentRef) string RefNameResolver maps a component to an name that is used as it's internalized name. @@ -1990,7 +1996,7 @@ func (doc *T) AddServer(server *Server) func (doc *T) AddServers(servers ...*Server) -func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(*T, componentRef) string) +func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(*T, ComponentRef) string) InternalizeRefs removes all references to external files from the spec and moves them to the components section. diff --git a/.github/sponsors/speakeasy-github-sponsor-dark.svg b/.github/sponsors/speakeasy-github-sponsor-dark.svg deleted file mode 100644 index 7c4eba28a..000000000 --- a/.github/sponsors/speakeasy-github-sponsor-dark.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.github/sponsors/speakeasy-github-sponsor-light.svg b/.github/sponsors/speakeasy-github-sponsor-light.svg deleted file mode 100644 index fd179ec61..000000000 --- a/.github/sponsors/speakeasy-github-sponsor-light.svg +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/README.md b/README.md index 976075c00..0709ae7c5 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,7 @@ Licensed under the [MIT License](./LICENSE). ## Contributors, users and sponsors The project has received pull requests [from many people](https://github.com/getkin/kin-openapi/graphs/contributors). Thanks to everyone! -Be sure to [give back to this project](https://github.com/sponsors/fenollp) like our sponsors: - -

- - - - - Speakeasy logo - - -

+Please, [give back to this project](https://github.com/sponsors/fenollp) by becoming a sponsor. Here's some projects that depend on _kin-openapi_: * [github.com/a-h/rest](https://github.com/a-h/rest) - "Generate OpenAPI 3.0 specifications from Go code without annotations or magic comments" @@ -303,6 +293,9 @@ for _, path := range doc.Paths.InMatchingOrder() { ## CHANGELOG: Sub-v1 breaking API changes +### v0.127.0 +* Downgraded `github.com/gorilla/mux` dep from `1.8.1` to `1.8.0`. + ### v0.126.0 * `openapi3.CircularReferenceError` and `openapi3.CircularReferenceCounter` are removed. `openapi3.Loader` now implements reference backtracking, so any kind of circular references should be properly resolved. * `InternalizeRefs` now takes a refNameResolver that has access to `openapi3.T` and more properties of the reference needing resolving. diff --git a/go.mod b/go.mod index 10abe40b4..11bc6d02b 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( github.com/go-openapi/jsonpointer v0.21.0 - github.com/gorilla/mux v1.8.1 + github.com/gorilla/mux v1.8.0 github.com/invopop/yaml v0.3.1 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/perimeterx/marshmallow v1.1.5 diff --git a/go.sum b/go.sum index fc84c3190..6b91d0dc9 100644 --- a/go.sum +++ b/go.sum @@ -5,8 +5,8 @@ github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kO github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= diff --git a/openapi2conv/issue979_test.go b/openapi2conv/issue979_test.go new file mode 100644 index 000000000..8d87902c9 --- /dev/null +++ b/openapi2conv/issue979_test.go @@ -0,0 +1,68 @@ +package openapi2conv + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi2" + "github.com/getkin/kin-openapi/openapi3" +) + +func TestIssue979(t *testing.T) { + v2 := []byte(` +{ + "basePath": "/v2", + "host": "test.example.com", + "info": { + "title": "MyAPI", + "version": "0.1", + "x-info": "info extension" + }, + "paths": { + "/foo": { + "get": { + "operationId": "getFoo", + "produces": [ + "application/pdf", + "application/json" + ], + "responses": { + "200": { + "description": "returns all information", + "schema": { + "type": "file" + } + }, + "default": { + "description": "OK" + } + }, + "summary": "get foo" + } + } + }, + "schemes": [ + "http" + ], + "swagger": "2.0" +} +`) + + var doc2 openapi2.T + err := json.Unmarshal(v2, &doc2) + require.NoError(t, err) + + doc3, err := ToV3(&doc2) + require.NoError(t, err) + err = doc3.Validate(context.Background()) + require.NoError(t, err) + + require.Equal(t, &openapi3.Types{"string"}, doc3.Paths.Value("/foo").Get.Responses.Value("200").Value.Content.Get("application/json").Schema.Value.Type) + require.Equal(t, "binary", doc3.Paths.Value("/foo").Get.Responses.Value("200").Value.Content.Get("application/json").Schema.Value.Format) + + require.Equal(t, &openapi3.Types{"string"}, doc3.Paths.Value("/foo").Get.Responses.Value("200").Value.Content.Get("application/pdf").Schema.Value.Type) + require.Equal(t, "binary", doc3.Paths.Value("/foo").Get.Responses.Value("200").Value.Content.Get("application/pdf").Schema.Value.Format) +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 2e22f1394..0a622f4f7 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -473,6 +473,9 @@ func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { if schema.Value.Items != nil { schema.Value.Items = ToV3SchemaRef(schema.Value.Items) } + if schema.Value.Type.Is("file") { + schema.Value.Format, schema.Value.Type = "binary", &openapi3.Types{"string"} + } for k, v := range schema.Value.Properties { schema.Value.Properties[k] = ToV3SchemaRef(v) } diff --git a/openapi3/helpers.go b/openapi3/helpers.go index cb1ed3a9f..d50b3d847 100644 --- a/openapi3/helpers.go +++ b/openapi3/helpers.go @@ -71,7 +71,7 @@ func copyURI(u *url.URL) *url.URL { return &c } -type componentRef interface { +type ComponentRef interface { RefString() string RefPath() *url.URL CollectionName() string @@ -88,7 +88,7 @@ type componentRef interface { // /schema/other.yaml $ref: ../records.yaml // // The records.yaml reference in the 2 latter refers to the same document. -func refersToSameDocument(o1 componentRef, o2 componentRef) bool { +func refersToSameDocument(o1 ComponentRef, o2 ComponentRef) bool { if o1 == nil || o2 == nil { return false } @@ -107,7 +107,7 @@ func refersToSameDocument(o1 componentRef, o2 componentRef) bool { // referencesRootDocument returns if the $ref points to the root document of the OpenAPI spec. // // If the document has no location, perhaps loaded from data in memory, it always returns false. -func referencesRootDocument(doc *T, ref componentRef) bool { +func referencesRootDocument(doc *T, ref ComponentRef) bool { if doc.url == nil || ref == nil || ref.RefPath() == nil { return false } @@ -171,7 +171,7 @@ func referenceURIMatch(u1 *url.URL, u2 *url.URL) bool { // This would also return... // // #/components/schemas/Record -func ReferencesComponentInRootDocument(doc *T, ref componentRef) (string, bool) { +func ReferencesComponentInRootDocument(doc *T, ref ComponentRef) (string, bool) { if ref == nil || ref.RefString() == "" { return "", false } @@ -197,19 +197,19 @@ func ReferencesComponentInRootDocument(doc *T, ref componentRef) (string, bool) panic(err) // unreachable } - var components map[string]componentRef + var components map[string]ComponentRef - componentRefType := reflect.TypeOf(new(componentRef)).Elem() + componentRefType := reflect.TypeOf(new(ComponentRef)).Elem() if t := reflect.TypeOf(collection); t.Kind() == reflect.Map && t.Key().Kind() == reflect.String && t.Elem().AssignableTo(componentRefType) { v := reflect.ValueOf(collection) - components = make(map[string]componentRef, v.Len()) + components = make(map[string]ComponentRef, v.Len()) for _, key := range v.MapKeys() { strct := v.MapIndex(key) // Type assertion safe, already checked via reflection above. - components[key.Interface().(string)] = strct.Interface().(componentRef) + components[key.Interface().(string)] = strct.Interface().(ComponentRef) } } else { return "", false diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index b4742864c..01f5dad88 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -10,7 +10,7 @@ import ( // // The function should avoid name collisions (i.e. be a injective mapping). // It must only contain characters valid for fixed field names: [IdentifierRegExp]. -type RefNameResolver func(*T, componentRef) string +type RefNameResolver func(*T, ComponentRef) string // DefaultRefResolver is a default implementation of refNameResolver for the // InternalizeRefs function. @@ -27,7 +27,7 @@ type RefNameResolver func(*T, componentRef) string // // This is an injective mapping over a "reasonable" amount of the possible openapi // spec domain space but is not perfect. There might be edge cases. -func DefaultRefNameResolver(doc *T, ref componentRef) string { +func DefaultRefNameResolver(doc *T, ref ComponentRef) string { if ref.RefString() == "" || ref.RefPath() == nil { panic("unable to resolve reference to name") } @@ -490,7 +490,7 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso // Example: // // doc.InternalizeRefs(context.Background(), nil) -func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(*T, componentRef) string) { +func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(*T, ComponentRef) string) { doc.resetVisited() if refNameResolver == nil { diff --git a/openapi3/issue499_test.go b/openapi3/issue499_test.go new file mode 100644 index 000000000..dfbafa9dc --- /dev/null +++ b/openapi3/issue499_test.go @@ -0,0 +1,14 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue499(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + _, err := loader.LoadFromFile("testdata/issue499/main.yml") + require.NoError(t, err) +} diff --git a/openapi3/issue961_test.go b/openapi3/issue961_test.go new file mode 100644 index 000000000..5dcbe9735 --- /dev/null +++ b/openapi3/issue961_test.go @@ -0,0 +1,14 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue961(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + _, err := loader.LoadFromFile("./testdata/issue961/main.yml") + require.NoError(t, err) +} diff --git a/openapi3/loader.go b/openapi3/loader.go index 4f2766a0f..31c340761 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -288,6 +288,31 @@ func resolvePathWithRef(ref string, rootPath *url.URL) (*url.URL, error) { return resolvedPath, nil } +func (loader *Loader) resolveRefPath(ref string, path *url.URL) (*url.URL, error) { + if ref != "" && ref[0] == '#' { + path = copyURI(path) + // Resolving internal refs of a doc loaded from memory + // has no path, so just set the Fragment. + if path == nil { + path = new(url.URL) + } + + path.Fragment = ref + return path, nil + } + + if err := loader.allowsExternalRefs(ref); err != nil { + return nil, err + } + + resolvedPath, err := resolvePathWithRef(ref, path) + if err != nil { + return nil, err + } + + return resolvedPath, nil +} + func isSingleRefElement(ref string) bool { return !strings.Contains(ref, "#") } @@ -325,7 +350,7 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv componentPath *url.URL, err error, ) { - if componentDoc, ref, componentPath, err = loader.resolveRef(doc, ref, path); err != nil { + if componentDoc, ref, componentPath, err = loader.resolveRefAndDocument(doc, ref, path); err != nil { return nil, nil, err } @@ -406,21 +431,25 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv err = nil } - setComponent := func(target any) { - if componentPath != nil { - if i, ok := target.(interface { - setRefPath(*url.URL) - }); ok { - copy := *componentPath - copy.Fragment = parsedURL.Fragment - i.setRefPath(©) + setPathRef := func(target any) { + if i, ok := target.(interface { + setRefPath(*url.URL) + }); ok { + pathRef := copyURI(componentPath) + // Resolving internal refs of a doc loaded from memory + // has no path, so just set the Fragment. + if pathRef == nil { + pathRef = new(url.URL) } + pathRef.Fragment = fragment + + i.setRefPath(pathRef) } } switch { case reflect.TypeOf(cursor) == reflect.TypeOf(resolved): - setComponent(cursor) + setPathRef(cursor) reflect.ValueOf(resolved).Elem().Set(reflect.ValueOf(cursor).Elem()) return componentDoc, componentPath, nil @@ -435,7 +464,7 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv return err } - setComponent(expect) + setPathRef(expect) return nil } if err := codec(cursor, resolved); err != nil { @@ -531,12 +560,12 @@ func drillIntoField(cursor any, fieldName string) (any, error) { } } -func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { +func (loader *Loader) resolveRefAndDocument(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) { if ref != "" && ref[0] == '#' { return doc, ref, path, nil } - fragment, resolvedPath, err := loader.resolveRefPath(ref, path) + fragment, resolvedPath, err := loader.resolveRef(ref, path) if err != nil { return nil, "", nil, err } @@ -548,23 +577,15 @@ func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, return doc, fragment, resolvedPath, nil } -func (loader *Loader) resolveRefPath(ref string, path *url.URL) (string, *url.URL, error) { - if ref != "" && ref[0] == '#' { - return ref, path, nil - } - - if err := loader.allowsExternalRefs(ref); err != nil { - return "", nil, err - } - - resolvedPath, err := resolvePathWithRef(ref, path) +func (loader *Loader) resolveRef(ref string, path *url.URL) (string, *url.URL, error) { + resolvedPathRef, err := loader.resolveRefPath(ref, path) if err != nil { return "", nil, err } - fragment := "#" + resolvedPath.Fragment - resolvedPath.Fragment = "" - return fragment, resolvedPath, nil + fragment := "#" + resolvedPathRef.Fragment + resolvedPathRef.Fragment = "" + return fragment, resolvedPathRef, nil } var ( @@ -591,8 +612,8 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Header) - _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) - component.setRefPath(refDocPath) + refPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refPath) }) { return nil } @@ -645,8 +666,8 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Parameter) - _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) - component.setRefPath(refDocPath) + refPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refPath) }) { return nil } @@ -710,8 +731,8 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*RequestBody) - _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) - component.setRefPath(refDocPath) + refPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refPath) }) { return nil } @@ -777,8 +798,8 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Response) - _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) - component.setRefPath(refDocPath) + refPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refPath) }) { return nil } @@ -857,8 +878,8 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Schema) - _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) - component.setRefPath(refDocPath) + refPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refPath) }) { return nil } @@ -943,8 +964,8 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*SecurityScheme) - _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) - component.setRefPath(refDocPath) + refPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refPath) }) { return nil } @@ -983,8 +1004,8 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Example) - _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) - component.setRefPath(refDocPath) + refPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refPath) }) { return nil } @@ -1027,8 +1048,8 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Callback) - _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) - component.setRefPath(refDocPath) + refPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refPath) }) { return nil } @@ -1083,8 +1104,8 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u } if !loader.shouldVisitRef(ref, func(value any) { component.Value = value.(*Link) - _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) - component.setRefPath(refDocPath) + refPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refPath) }) { return nil } diff --git a/openapi3/refs.go b/openapi3/refs.go index 846dc55a0..d337b0e3d 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -24,7 +24,7 @@ type CallbackRef struct { Value *Callback extra []string - refPath url.URL + refPath *url.URL } var _ jsonpointer.JSONPointable = (*CallbackRef)(nil) @@ -38,17 +38,16 @@ func (x *CallbackRef) RefString() string { return x.Ref } func (x *CallbackRef) CollectionName() string { return "callbacks" } // RefPath returns the path of the $ref relative to the root document. -func (x *CallbackRef) RefPath() *url.URL { return &x.refPath } +func (x *CallbackRef) RefPath() *url.URL { return copyURI(x.refPath) } func (x *CallbackRef) setRefPath(u *url.URL) { - // Do not set to null or override a path already set. - // References can be loaded multiple times not all with access - // to the correct path info. - if u == nil || x.refPath != (url.URL{}) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { return } - x.refPath = *u + x.refPath = copyURI(u) } // MarshalYAML returns the YAML encoding of CallbackRef. @@ -161,7 +160,7 @@ type ExampleRef struct { Value *Example extra []string - refPath url.URL + refPath *url.URL } var _ jsonpointer.JSONPointable = (*ExampleRef)(nil) @@ -175,17 +174,16 @@ func (x *ExampleRef) RefString() string { return x.Ref } func (x *ExampleRef) CollectionName() string { return "examples" } // RefPath returns the path of the $ref relative to the root document. -func (x *ExampleRef) RefPath() *url.URL { return &x.refPath } +func (x *ExampleRef) RefPath() *url.URL { return copyURI(x.refPath) } func (x *ExampleRef) setRefPath(u *url.URL) { - // Do not set to null or override a path already set. - // References can be loaded multiple times not all with access - // to the correct path info. - if u == nil || x.refPath != (url.URL{}) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { return } - x.refPath = *u + x.refPath = copyURI(u) } // MarshalYAML returns the YAML encoding of ExampleRef. @@ -298,7 +296,7 @@ type HeaderRef struct { Value *Header extra []string - refPath url.URL + refPath *url.URL } var _ jsonpointer.JSONPointable = (*HeaderRef)(nil) @@ -312,17 +310,16 @@ func (x *HeaderRef) RefString() string { return x.Ref } func (x *HeaderRef) CollectionName() string { return "headers" } // RefPath returns the path of the $ref relative to the root document. -func (x *HeaderRef) RefPath() *url.URL { return &x.refPath } +func (x *HeaderRef) RefPath() *url.URL { return copyURI(x.refPath) } func (x *HeaderRef) setRefPath(u *url.URL) { - // Do not set to null or override a path already set. - // References can be loaded multiple times not all with access - // to the correct path info. - if u == nil || x.refPath != (url.URL{}) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { return } - x.refPath = *u + x.refPath = copyURI(u) } // MarshalYAML returns the YAML encoding of HeaderRef. @@ -435,7 +432,7 @@ type LinkRef struct { Value *Link extra []string - refPath url.URL + refPath *url.URL } var _ jsonpointer.JSONPointable = (*LinkRef)(nil) @@ -449,17 +446,16 @@ func (x *LinkRef) RefString() string { return x.Ref } func (x *LinkRef) CollectionName() string { return "links" } // RefPath returns the path of the $ref relative to the root document. -func (x *LinkRef) RefPath() *url.URL { return &x.refPath } +func (x *LinkRef) RefPath() *url.URL { return copyURI(x.refPath) } func (x *LinkRef) setRefPath(u *url.URL) { - // Do not set to null or override a path already set. - // References can be loaded multiple times not all with access - // to the correct path info. - if u == nil || x.refPath != (url.URL{}) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { return } - x.refPath = *u + x.refPath = copyURI(u) } // MarshalYAML returns the YAML encoding of LinkRef. @@ -572,7 +568,7 @@ type ParameterRef struct { Value *Parameter extra []string - refPath url.URL + refPath *url.URL } var _ jsonpointer.JSONPointable = (*ParameterRef)(nil) @@ -586,17 +582,16 @@ func (x *ParameterRef) RefString() string { return x.Ref } func (x *ParameterRef) CollectionName() string { return "parameters" } // RefPath returns the path of the $ref relative to the root document. -func (x *ParameterRef) RefPath() *url.URL { return &x.refPath } +func (x *ParameterRef) RefPath() *url.URL { return copyURI(x.refPath) } func (x *ParameterRef) setRefPath(u *url.URL) { - // Do not set to null or override a path already set. - // References can be loaded multiple times not all with access - // to the correct path info. - if u == nil || x.refPath != (url.URL{}) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { return } - x.refPath = *u + x.refPath = copyURI(u) } // MarshalYAML returns the YAML encoding of ParameterRef. @@ -709,7 +704,7 @@ type RequestBodyRef struct { Value *RequestBody extra []string - refPath url.URL + refPath *url.URL } var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) @@ -723,17 +718,16 @@ func (x *RequestBodyRef) RefString() string { return x.Ref } func (x *RequestBodyRef) CollectionName() string { return "requestBodies" } // RefPath returns the path of the $ref relative to the root document. -func (x *RequestBodyRef) RefPath() *url.URL { return &x.refPath } +func (x *RequestBodyRef) RefPath() *url.URL { return copyURI(x.refPath) } func (x *RequestBodyRef) setRefPath(u *url.URL) { - // Do not set to null or override a path already set. - // References can be loaded multiple times not all with access - // to the correct path info. - if u == nil || x.refPath != (url.URL{}) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { return } - x.refPath = *u + x.refPath = copyURI(u) } // MarshalYAML returns the YAML encoding of RequestBodyRef. @@ -846,7 +840,7 @@ type ResponseRef struct { Value *Response extra []string - refPath url.URL + refPath *url.URL } var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) @@ -860,17 +854,16 @@ func (x *ResponseRef) RefString() string { return x.Ref } func (x *ResponseRef) CollectionName() string { return "responses" } // RefPath returns the path of the $ref relative to the root document. -func (x *ResponseRef) RefPath() *url.URL { return &x.refPath } +func (x *ResponseRef) RefPath() *url.URL { return copyURI(x.refPath) } func (x *ResponseRef) setRefPath(u *url.URL) { - // Do not set to null or override a path already set. - // References can be loaded multiple times not all with access - // to the correct path info. - if u == nil || x.refPath != (url.URL{}) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { return } - x.refPath = *u + x.refPath = copyURI(u) } // MarshalYAML returns the YAML encoding of ResponseRef. @@ -983,7 +976,7 @@ type SchemaRef struct { Value *Schema extra []string - refPath url.URL + refPath *url.URL } var _ jsonpointer.JSONPointable = (*SchemaRef)(nil) @@ -997,17 +990,16 @@ func (x *SchemaRef) RefString() string { return x.Ref } func (x *SchemaRef) CollectionName() string { return "schemas" } // RefPath returns the path of the $ref relative to the root document. -func (x *SchemaRef) RefPath() *url.URL { return &x.refPath } +func (x *SchemaRef) RefPath() *url.URL { return copyURI(x.refPath) } func (x *SchemaRef) setRefPath(u *url.URL) { - // Do not set to null or override a path already set. - // References can be loaded multiple times not all with access - // to the correct path info. - if u == nil || x.refPath != (url.URL{}) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { return } - x.refPath = *u + x.refPath = copyURI(u) } // MarshalYAML returns the YAML encoding of SchemaRef. @@ -1120,7 +1112,7 @@ type SecuritySchemeRef struct { Value *SecurityScheme extra []string - refPath url.URL + refPath *url.URL } var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil) @@ -1134,17 +1126,16 @@ func (x *SecuritySchemeRef) RefString() string { return x.Ref } func (x *SecuritySchemeRef) CollectionName() string { return "securitySchemes" } // RefPath returns the path of the $ref relative to the root document. -func (x *SecuritySchemeRef) RefPath() *url.URL { return &x.refPath } +func (x *SecuritySchemeRef) RefPath() *url.URL { return copyURI(x.refPath) } func (x *SecuritySchemeRef) setRefPath(u *url.URL) { - // Do not set to null or override a path already set. - // References can be loaded multiple times not all with access - // to the correct path info. - if u == nil || x.refPath != (url.URL{}) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { return } - x.refPath = *u + x.refPath = copyURI(u) } // MarshalYAML returns the YAML encoding of SecuritySchemeRef. diff --git a/openapi3/refs.tmpl b/openapi3/refs.tmpl index f9ed1e6e9..a3f5bdab7 100644 --- a/openapi3/refs.tmpl +++ b/openapi3/refs.tmpl @@ -24,7 +24,7 @@ type {{ $type.Name }}Ref struct { Value *{{ $type.Name }} extra []string - refPath url.URL + refPath *url.URL } var _ jsonpointer.JSONPointable = (*{{ $type.Name }}Ref)(nil) @@ -38,17 +38,16 @@ func (x *{{ $type.Name }}Ref) RefString() string { return x.Ref } func (x *{{ $type.Name }}Ref) CollectionName() string { return "{{ $type.CollectionName }}" } // RefPath returns the path of the $ref relative to the root document. -func (x *{{ $type.Name }}Ref) RefPath() *url.URL { return &x.refPath } +func (x *{{ $type.Name }}Ref) RefPath() *url.URL { return copyURI(x.refPath) } func (x *{{ $type.Name }}Ref) setRefPath(u *url.URL) { - // Do not set to null or override a path already set. - // References can be loaded multiple times not all with access - // to the correct path info. - if u == nil || x.refPath != (url.URL{}) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { return } - x.refPath = *u + x.refPath = copyURI(u) } // MarshalYAML returns the YAML encoding of {{ $type.Name }}Ref. diff --git a/openapi3/testdata/issue499/foo.yml b/openapi3/testdata/issue499/foo.yml new file mode 100644 index 000000000..2d8bb0e0f --- /dev/null +++ b/openapi3/testdata/issue499/foo.yml @@ -0,0 +1 @@ +type: string \ No newline at end of file diff --git a/openapi3/testdata/issue499/main.yml b/openapi3/testdata/issue499/main.yml new file mode 100644 index 000000000..da1a97256 --- /dev/null +++ b/openapi3/testdata/issue499/main.yml @@ -0,0 +1,24 @@ +openapi: 3.0.0 +info: + title: 'spec' + version: 1.2.3 + +paths: + /foo: + get: + summary: get foo + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: "#/components/schemas/Foo" + +components: + schemas: + Foo: + type: object + properties: + id: + $ref: "./foo.yml" \ No newline at end of file diff --git a/openapi3/testdata/issue961/config_param.yml b/openapi3/testdata/issue961/config_param.yml new file mode 100644 index 000000000..aa861ac09 --- /dev/null +++ b/openapi3/testdata/issue961/config_param.yml @@ -0,0 +1,41 @@ +oneOf: + - title: "text" + description: | + type "text": **text** is a simple string. + type: object + required: + - type + properties: + default: + type: string + position: + type: number + format: int32 + minimum: 0 + description: | + Position of the parameter in the output. + name: + type: string + description: "name of the parameter as used in the API" + - title: "table" + description: | + type "table" + type: object + required: + - type + properties: + default: + type: string + position: + type: number + format: int32 + minimum: 0 + description: | + Position of the parameter in the output. + name: + type: string + description: "name of the parameter as used in the API" + fields: + type: array + items: + $ref: '#' diff --git a/openapi3/testdata/issue961/main.yml b/openapi3/testdata/issue961/main.yml new file mode 100644 index 000000000..4d090c16c --- /dev/null +++ b/openapi3/testdata/issue961/main.yml @@ -0,0 +1,4 @@ +components: + schemas: + configParam: + $ref: './config_param.yml' \ No newline at end of file diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index c509def3e..e151c9ee7 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -320,7 +320,11 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho switch { case schema.Value.Type.Is("array"): decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { - return dec.DecodeArray(param, sm, schema) + res, b, e := dec.DecodeArray(param, sm, schema) + if len(res) == 0 { + return nil, b, e + } + return res, b, e } case schema.Value.Type.Is("object"): decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 6e456d2aa..53763901d 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -878,6 +878,22 @@ func TestDecodeParameter(t *testing.T) { want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, + { + name: "no param, for arrays", + param: &openapi3.Parameter{Name: "something", In: "query", Schema: stringArraySchema}, + query: "", + want: nil, + found: false, + err: nil, + }, + { + name: "missing param, for arrays", + param: &openapi3.Parameter{Name: "something", In: "query", Schema: stringArraySchema}, + query: "foo=bar", + want: nil, + found: false, + err: nil, + }, { name: "deepObject explode additionalProperties with object properties - missing index on nested array", param: &openapi3.Parameter{ diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index f21060a99..296403c9e 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "sort" "github.com/getkin/kin-openapi/openapi3" @@ -103,6 +104,28 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er return } +// appendToQueryValues adds to query parameters each value in the provided slice +func appendToQueryValues[T any](q url.Values, parameterName string, v []T) { + for _, i := range v { + q.Add(parameterName, fmt.Sprintf("%v", i)) + } +} + +// populateDefaultQueryParameters populates default values inside query parameters, while ensuring types are respected +func populateDefaultQueryParameters(q url.Values, parameterName string, value any) { + switch t := value.(type) { + case []string: + appendToQueryValues(q, parameterName, t) + case []float64: + appendToQueryValues(q, parameterName, t) + case []int: + appendToQueryValues(q, parameterName, t) + default: + q.Add(parameterName, fmt.Sprintf("%v", value)) + } + +} + // ValidateParameter validates a parameter's value by JSON schema. // The function returns RequestError with a ParseError cause when unable to parse a value. // The function returns RequestError with ErrInvalidRequired cause when a value of a required parameter is not defined. @@ -156,7 +179,7 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param // Next check `parameter.Required && !found` will catch this. case openapi3.ParameterInQuery: q := req.URL.Query() - q.Add(parameter.Name, fmt.Sprintf("%v", value)) + populateDefaultQueryParameters(q, parameter.Name, value) req.URL.RawQuery = q.Encode() case openapi3.ParameterInHeader: req.Header.Add(parameter.Name, fmt.Sprintf("%v", value)) diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go index 984af4c21..505e5cb98 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -431,8 +431,6 @@ func TestValidateQueryParams(t *testing.T) { }, }, }, - // - // } for _, tc := range testCases { @@ -569,3 +567,94 @@ paths: }) require.Error(t, err) } + +var ( + StringArraySchemaWithDefault = &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"array"}, + Items: stringSchema, + Default: []string{"A", "B", "C"}, + }, + } + FloatArraySchemaWithDefault = &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: &openapi3.Types{"array"}, + Items: numberSchema, + Default: []float64{1.5, 2.5, 3.5}, + }, + } +) + +func TestValidateRequestDefault(t *testing.T) { + type testCase struct { + name string + param *openapi3.Parameter + query string + wantQuery map[string][]string + wantHeader map[string]any + } + + testCases := []testCase{ + { + name: "String Array In Query", + param: &openapi3.Parameter{ + Name: "param", In: "query", Style: "form", Explode: explode, + Schema: StringArraySchemaWithDefault, + }, + wantQuery: map[string][]string{ + "param": { + "A", + "B", + "C", + }, + }, + }, + { + name: "Float Array In Query", + param: &openapi3.Parameter{ + Name: "param", In: "query", Style: "form", Explode: explode, + Schema: FloatArraySchemaWithDefault, + }, + wantQuery: map[string][]string{ + "param": { + "1.5", + "2.5", + "3.5", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + info := &openapi3.Info{ + Title: "MyAPI", + Version: "0.1", + } + doc := &openapi3.T{OpenAPI: "3.0.0", Info: info, Paths: openapi3.NewPaths()} + op := &openapi3.Operation{ + OperationID: "test", + Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, + Responses: openapi3.NewResponses(), + } + doc.AddOperation("/test", http.MethodGet, op) + err := doc.Validate(context.Background()) + require.NoError(t, err) + router, err := legacyrouter.NewRouter(doc) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodGet, "http://test.org/test?"+tc.query, nil) + route, pathParams, err := router.FindRoute(req) + require.NoError(t, err) + + input := &RequestValidationInput{Request: req, PathParams: pathParams, Route: route} + + err = ValidateParameter(context.Background(), input, tc.param) + require.NoError(t, err) + + for k, v := range tc.wantQuery { + require.Equal(t, v, input.Request.URL.Query()[k]) + } + }) + } +} diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 9eae7ccbb..78e6901dc 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -195,14 +195,16 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type return nil, &CycleError{} } } - - if cap(parents) == 0 { + isRoot := cap(parents) == 0 + if isRoot { parents = make([]*theTypeInfo, 0, 4) } parents = append(parents, typeInfo) + isNullable := false for t.Kind() == reflect.Ptr { t = t.Elem() + isNullable = !isRoot } if strings.HasSuffix(t.Name(), "Ref") { @@ -231,6 +233,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type } schema := &openapi3.Schema{} + schema.Nullable = isNullable switch t.Kind() { case reflect.Func, reflect.Chan: diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 8263983a7..4d9c1e208 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -108,11 +108,13 @@ func ExampleGenerator_SchemaRefs() { // "json": {}, // "map": { // "additionalProperties": { + // "nullable": true, // "type": "string" // }, // "type": "object" // }, // "ptr": { + // "nullable": true, // "type": "string" // }, // "slice": { @@ -178,6 +180,7 @@ func ExampleThrowErrorOnCycle() { // schemaRef: { // "properties": { // "a": { + // "nullable": true, // "properties": { // "b": { // "$ref": "#/components/schemas/CyclicType0" @@ -192,6 +195,7 @@ func ExampleThrowErrorOnCycle() { // "CyclicType0": { // "properties": { // "a": { + // "nullable": true, // "properties": { // "b": { // "$ref": "#/components/schemas/CyclicType0" diff --git a/openapi3gen/simple_test.go b/openapi3gen/simple_test.go index 99e94ae12..11b3b26a4 100644 --- a/openapi3gen/simple_test.go +++ b/openapi3gen/simple_test.go @@ -70,11 +70,13 @@ func Example() { // "json": {}, // "map": { // "additionalProperties": { + // "nullable": true, // "type": "string" // }, // "type": "object" // }, // "ptr": { + // "nullable": true, // "type": "string" // }, // "slice": {