diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 993d2da17..7c950937b 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -4,6 +4,8 @@ Package openapi3 parses and writes OpenAPI 3 specification documents. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md +Code generated by go generate; DO NOT EDIT. + CONSTANTS const ( @@ -170,6 +172,9 @@ func (callback *Callback) Map() (m map[string]*PathItem) func (callback *Callback) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Callback. +func (callback *Callback) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Callback. + func (callback *Callback) Set(key string, value *PathItem) Set adds or replaces key 'key' of 'callback' with 'value'. Note: 'callback' MUST be non-nil @@ -235,6 +240,9 @@ func NewComponents() Components func (components Components) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Components. +func (components Components) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Components. + func (components *Components) UnmarshalJSON(data []byte) error UnmarshalJSON sets Components to a copy of data. @@ -255,6 +263,9 @@ type Contact struct { func (contact Contact) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Contact. +func (contact Contact) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Contact. + func (contact *Contact) UnmarshalJSON(data []byte) error UnmarshalJSON sets Contact to a copy of data. @@ -295,6 +306,9 @@ type Discriminator struct { func (discriminator Discriminator) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Discriminator. +func (discriminator Discriminator) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Discriminator. + func (discriminator *Discriminator) UnmarshalJSON(data []byte) error UnmarshalJSON sets Discriminator to a copy of data. @@ -319,6 +333,9 @@ func NewEncoding() *Encoding func (encoding Encoding) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Encoding. +func (encoding Encoding) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Encoding. + func (encoding *Encoding) SerializationMethod() *SerializationMethod SerializationMethod returns a serialization method of request body. When serialization method is not defined the method returns the default @@ -350,6 +367,9 @@ func NewExample(value interface{}) *Example func (example Example) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Example. +func (example Example) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Example. + func (example *Example) UnmarshalJSON(data []byte) error UnmarshalJSON sets Example to a copy of data. @@ -399,6 +419,9 @@ type ExternalDocs struct { func (e ExternalDocs) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of ExternalDocs. +func (e ExternalDocs) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of ExternalDocs. + func (e *ExternalDocs) UnmarshalJSON(data []byte) error UnmarshalJSON sets ExternalDocs to a copy of data. @@ -487,6 +510,9 @@ type Info struct { func (info Info) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Info. +func (info Info) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Info. + func (info *Info) UnmarshalJSON(data []byte) error UnmarshalJSON sets Info to a copy of data. @@ -505,6 +531,9 @@ type License struct { func (license License) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of License. +func (license License) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of License. + func (license *License) UnmarshalJSON(data []byte) error UnmarshalJSON sets License to a copy of data. @@ -527,6 +556,9 @@ type Link struct { func (link Link) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Link. +func (link Link) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Link. + func (link *Link) UnmarshalJSON(data []byte) error UnmarshalJSON sets Link to a copy of data. @@ -622,6 +654,9 @@ func (mediaType MediaType) JSONLookup(token string) (interface{}, error) func (mediaType MediaType) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of MediaType. +func (mediaType MediaType) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of MediaType. + func (mediaType *MediaType) UnmarshalJSON(data []byte) error UnmarshalJSON sets MediaType to a copy of data. @@ -687,6 +722,9 @@ type OAuthFlow struct { func (flow OAuthFlow) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of OAuthFlow. +func (flow OAuthFlow) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of OAuthFlow. + func (flow *OAuthFlow) UnmarshalJSON(data []byte) error UnmarshalJSON sets OAuthFlow to a copy of data. @@ -708,6 +746,9 @@ type OAuthFlows struct { func (flows OAuthFlows) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of OAuthFlows. +func (flows OAuthFlows) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of OAuthFlows. + func (flows *OAuthFlows) UnmarshalJSON(data []byte) error UnmarshalJSON sets OAuthFlows to a copy of data. @@ -769,6 +810,9 @@ func (operation Operation) JSONLookup(token string) (interface{}, error) func (operation Operation) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Operation. +func (operation Operation) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Operation. + func (operation *Operation) UnmarshalJSON(data []byte) error UnmarshalJSON sets Operation to a copy of data. @@ -811,6 +855,9 @@ func (parameter Parameter) JSONLookup(token string) (interface{}, error) func (parameter Parameter) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Parameter. +func (parameter Parameter) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Parameter. + func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error) SerializationMethod returns a parameter's serialization method. When a parameter's serialization method is not defined the method returns the @@ -901,6 +948,9 @@ func (pathItem *PathItem) GetOperation(method string) *Operation func (pathItem PathItem) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of PathItem. +func (pathItem PathItem) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of PathItem. + func (pathItem *PathItem) Operations() map[string]*Operation func (pathItem *PathItem) SetOperation(method string, operation *Operation) @@ -963,8 +1013,8 @@ func (paths *Paths) Map() (m map[string]*PathItem) func (paths *Paths) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Paths. -func (paths *Paths) MarshalYAML() (any, error) - Support YAML Marshaler interface for gopkg.in/yaml +func (paths *Paths) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Paths. func (paths *Paths) Set(key string, value *PathItem) Set adds or replaces key 'key' of 'paths' with 'value'. Note: 'paths' MUST @@ -1031,6 +1081,9 @@ func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType func (requestBody RequestBody) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of RequestBody. +func (requestBody RequestBody) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of RequestBody. + func (requestBody *RequestBody) UnmarshalJSON(data []byte) error UnmarshalJSON sets RequestBody to a copy of data. @@ -1097,6 +1150,9 @@ func NewResponse() *Response func (response Response) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Response. +func (response Response) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Response. + func (response *Response) UnmarshalJSON(data []byte) error UnmarshalJSON sets Response to a copy of data. @@ -1177,8 +1233,8 @@ func (responses *Responses) Map() (m map[string]*ResponseRef) func (responses *Responses) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Responses. -func (responses *Responses) MarshalYAML() (any, error) - Support YAML Marshaler interface for gopkg.in/yaml +func (responses *Responses) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Responses. func (responses *Responses) Set(key string, value *ResponseRef) Set adds or replaces key 'key' of 'responses' with 'value'. Note: @@ -1307,6 +1363,9 @@ func (schema Schema) JSONLookup(token string) (interface{}, error) func (schema Schema) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Schema. +func (schema Schema) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Schema. + func (schema *Schema) NewRef() *SchemaRef func (schema *Schema) PermitsNull() bool @@ -1536,6 +1595,9 @@ func NewSecurityScheme() *SecurityScheme func (ss SecurityScheme) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of SecurityScheme. +func (ss SecurityScheme) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of SecurityScheme. + func (ss *SecurityScheme) UnmarshalJSON(data []byte) error UnmarshalJSON sets SecurityScheme to a copy of data. @@ -1611,6 +1673,9 @@ func (server *Server) BasePath() (string, error) func (server Server) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Server. +func (server Server) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Server. + func (server Server) MatchRawURL(input string) ([]string, string, bool) func (server Server) ParameterNames() ([]string, error) @@ -1634,6 +1699,9 @@ type ServerVariable struct { func (serverVariable ServerVariable) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of ServerVariable. +func (serverVariable ServerVariable) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of ServerVariable. + func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error UnmarshalJSON sets ServerVariable to a copy of data. @@ -1699,6 +1767,9 @@ func (doc *T) JSONLookup(token string) (interface{}, error) func (doc *T) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of T. +func (doc *T) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of T. + func (doc *T) UnmarshalJSON(data []byte) error UnmarshalJSON sets T to a copy of data. @@ -1719,6 +1790,9 @@ type Tag struct { func (t Tag) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Tag. +func (t Tag) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of Tag. + func (t *Tag) UnmarshalJSON(data []byte) error UnmarshalJSON sets Tag to a copy of data. @@ -1813,6 +1887,9 @@ type XML struct { func (xml XML) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of XML. +func (xml XML) MarshalYAML() (interface{}, error) + MarshalYAML returns the YAML encoding of XML. + func (xml *XML) UnmarshalJSON(data []byte) error UnmarshalJSON sets XML to a copy of data. diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt index 0fd7027c8..43540c416 100644 --- a/.github/docs/openapi3filter.txt +++ b/.github/docs/openapi3filter.txt @@ -182,7 +182,7 @@ type ErrCode int occur during validation. These may be used to write an appropriate response in ErrFunc. -type ErrFunc func(w http.ResponseWriter, status int, code ErrCode, err error) +type ErrFunc func(ctx context.Context, w http.ResponseWriter, status int, code ErrCode, err error) ErrFunc handles errors that may occur during validation. type ErrorEncoder func(ctx context.Context, err error, w http.ResponseWriter) @@ -198,7 +198,7 @@ type Headerer interface { Headerer, the provided headers will be applied to the response writer, after the Content-Type is set. -type LogFunc func(message string, err error) +type LogFunc func(ctx context.Context, message string, err error) LogFunc handles log messages that may occur during validation. type Options struct { diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index a09546a40..000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [ "master" ] - pull_request: - branches: [ "master" ] - schedule: - - cron: "4 8 * * 4" - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ go ] - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - queries: +security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 - with: - category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index acb3a0f0d..fa32c1b59 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -52,7 +52,7 @@ jobs: - uses: actions/checkout@v2 - - run: ./refs.sh | tee openapi3/refs.go + - run: go generate openapi3/refsgenerator.go - run: git --no-pager diff --exit-code - run: ./maps.sh diff --git a/README.md b/README.md index dfd1781ad..aea156c0d 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Be sure to [give back to this project](https://github.com/sponsors/fenollp) like

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" * [github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3" * [github.com/danielgtaylor/apisprout](https://github.com/danielgtaylor/apisprout) - "Lightweight, blazing fast, cross-platform OpenAPI 3 mock server with validation" * [github.com/deepmap/oapi-codegen](https://github.com/deepmap/oapi-codegen) - "Generate Go client and server boilerplate from OpenAPI 3 specifications" @@ -277,6 +278,9 @@ This will change the schema validation errors to return only the `Reason` field, ## CHANGELOG: Sub-v1 breaking API changes +### v0.125.0 +* The `openapi3filter.ErrFunc` and `openapi3filter.LogFunc` func types now take the validated request's context as first argument. + ### v0.122.0 * `Paths` field of `openapi3.T` is now a pointer * `Responses` field of `openapi3.Operation` is now a pointer diff --git a/maps.sh b/maps.sh index 0f7e14ca5..7e335b01c 100755 --- a/maps.sh +++ b/maps.sh @@ -156,8 +156,8 @@ EOF maplike_UnMarsh() { cat <>"$maplike" -// MarshalJSON returns the JSON encoding of ${type#'*'}. -func (${name} ${type}) MarshalJSON() ([]byte, error) { +// MarshalYAML returns the YAML encoding of ${type#'*'}. +func (${name} ${type}) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, ${name}.Len()+len(${name}.Extensions)) for k, v := range ${name}.Extensions { m[k] = v @@ -165,7 +165,16 @@ func (${name} ${type}) MarshalJSON() ([]byte, error) { for k, v := range ${name}.Map() { m[k] = v } - return json.Marshal(m) + return m, nil +} + +// MarshalJSON returns the JSON encoding of ${type#'*'}. +func (${name} ${type}) MarshalJSON() ([]byte, error) { + ${name}Yaml, err := ${name}.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(${name}Yaml) } // UnmarshalJSON sets ${type#'*'} to a copy of data. diff --git a/openapi2/marsh.go b/openapi2/marsh.go index b15f3b82c..5aa162a72 100644 --- a/openapi2/marsh.go +++ b/openapi2/marsh.go @@ -17,10 +17,18 @@ func unmarshalError(jsonUnmarshalErr error) error { } func unmarshal(data []byte, v interface{}) error { + var jsonErr, yamlErr error + // See https://github.com/getkin/kin-openapi/issues/680 - if err := json.Unmarshal(data, v); err != nil { - // UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys - return yaml.Unmarshal(data, v) + if jsonErr = json.Unmarshal(data, v); jsonErr == nil { + return nil + } + + // UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys + if yamlErr = yaml.Unmarshal(data, v); yamlErr == nil { + return nil } - return nil + + // If both unmarshaling attempts fail, return a new error that includes both errors + return fmt.Errorf("failed to unmarshal data: json error: %v, yaml error: %v", jsonErr, yamlErr) } diff --git a/openapi3/components.go b/openapi3/components.go index 656ea1936..c46663273 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -43,6 +43,15 @@ func NewComponents() Components { // MarshalJSON returns the JSON encoding of Components. func (components Components) MarshalJSON() ([]byte, error) { + x, err := components.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Components. +func (components Components) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 9+len(components.Extensions)) for k, v := range components.Extensions { m[k] = v @@ -74,7 +83,7 @@ func (components Components) MarshalJSON() ([]byte, error) { if x := components.Callbacks; len(x) != 0 { m["callbacks"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Components to a copy of data. diff --git a/openapi3/contact.go b/openapi3/contact.go index e60d2818a..7b707ce39 100644 --- a/openapi3/contact.go +++ b/openapi3/contact.go @@ -17,6 +17,15 @@ type Contact struct { // MarshalJSON returns the JSON encoding of Contact. func (contact Contact) MarshalJSON() ([]byte, error) { + x, err := contact.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Contact. +func (contact Contact) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 3+len(contact.Extensions)) for k, v := range contact.Extensions { m[k] = v @@ -30,7 +39,7 @@ func (contact Contact) MarshalJSON() ([]byte, error) { if x := contact.Email; x != "" { m["email"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Contact to a copy of data. diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index abb480741..ae36f416d 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -16,6 +16,15 @@ type Discriminator struct { // MarshalJSON returns the JSON encoding of Discriminator. func (discriminator Discriminator) MarshalJSON() ([]byte, error) { + x, err := discriminator.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Discriminator. +func (discriminator Discriminator) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 2+len(discriminator.Extensions)) for k, v := range discriminator.Extensions { m[k] = v @@ -24,7 +33,7 @@ func (discriminator Discriminator) MarshalJSON() ([]byte, error) { if x := discriminator.Mapping; len(x) != 0 { m["mapping"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Discriminator to a copy of data. diff --git a/openapi3/encoding.go b/openapi3/encoding.go index 8e810279c..57d20ee3d 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -41,6 +41,15 @@ func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding { // MarshalJSON returns the JSON encoding of Encoding. func (encoding Encoding) MarshalJSON() ([]byte, error) { + x, err := encoding.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Encoding. +func (encoding Encoding) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 5+len(encoding.Extensions)) for k, v := range encoding.Extensions { m[k] = v @@ -60,7 +69,7 @@ func (encoding Encoding) MarshalJSON() ([]byte, error) { if x := encoding.AllowReserved; x { m["allowReserved"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Encoding to a copy of data. diff --git a/openapi3/example.go b/openapi3/example.go index 44e71d827..a1a5a2b35 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -23,6 +23,15 @@ func NewExample(value interface{}) *Example { // MarshalJSON returns the JSON encoding of Example. func (example Example) MarshalJSON() ([]byte, error) { + x, err := example.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Example. +func (example Example) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 4+len(example.Extensions)) for k, v := range example.Extensions { m[k] = v @@ -39,7 +48,7 @@ func (example Example) MarshalJSON() ([]byte, error) { if x := example.ExternalValue; x != "" { m["externalValue"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Example to a copy of data. diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index 7190be4b0..40e9f3db0 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -19,6 +19,15 @@ type ExternalDocs struct { // MarshalJSON returns the JSON encoding of ExternalDocs. func (e ExternalDocs) MarshalJSON() ([]byte, error) { + x, err := e.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of ExternalDocs. +func (e ExternalDocs) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 2+len(e.Extensions)) for k, v := range e.Extensions { m[k] = v @@ -29,7 +38,7 @@ func (e ExternalDocs) MarshalJSON() ([]byte, error) { if x := e.URL; x != "" { m["url"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets ExternalDocs to a copy of data. diff --git a/openapi3/info.go b/openapi3/info.go index ffcd3b0e3..51707f852 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -21,6 +21,15 @@ type Info struct { // MarshalJSON returns the JSON encoding of Info. func (info Info) MarshalJSON() ([]byte, error) { + x, err := info.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Info. +func (info Info) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 6+len(info.Extensions)) for k, v := range info.Extensions { m[k] = v @@ -39,7 +48,7 @@ func (info Info) MarshalJSON() ([]byte, error) { m["license"] = x } m["version"] = info.Version - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Info to a copy of data. diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index e313e5535..191c14f7e 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -351,7 +351,10 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso ops.Ref = "" for _, param := range ops.Parameters { - doc.addParameterToSpec(param, refNameResolver, pathIsExternal) + isExternal := doc.addParameterToSpec(param, refNameResolver, pathIsExternal) + if param.Value != nil { + doc.derefParameter(*param.Value, refNameResolver, pathIsExternal || isExternal) + } } for _, op := range ops.Operations() { diff --git a/openapi3/internalize_refs_test.go b/openapi3/internalize_refs_test.go index b5ceb6905..90e73f234 100644 --- a/openapi3/internalize_refs_test.go +++ b/openapi3/internalize_refs_test.go @@ -23,6 +23,7 @@ func TestInternalizeRefs(t *testing.T) { {"testdata/spec.yaml"}, {"testdata/callbacks.yml"}, {"testdata/issue831/testref.internalizepath.openapi.yml"}, + {"testdata/issue959/openapi.yml"}, } for _, test := range tests { diff --git a/openapi3/license.go b/openapi3/license.go index 3d2d2f06d..e845ed832 100644 --- a/openapi3/license.go +++ b/openapi3/license.go @@ -17,6 +17,15 @@ type License struct { // MarshalJSON returns the JSON encoding of License. func (license License) MarshalJSON() ([]byte, error) { + x, err := license.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of License. +func (license License) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 2+len(license.Extensions)) for k, v := range license.Extensions { m[k] = v @@ -25,7 +34,7 @@ func (license License) MarshalJSON() ([]byte, error) { if x := license.URL; x != "" { m["url"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets License to a copy of data. diff --git a/openapi3/link.go b/openapi3/link.go index 23a8df41b..961566538 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -22,6 +22,15 @@ type Link struct { // MarshalJSON returns the JSON encoding of Link. func (link Link) MarshalJSON() ([]byte, error) { + x, err := link.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Link. +func (link Link) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 6+len(link.Extensions)) for k, v := range link.Extensions { m[k] = v @@ -46,7 +55,7 @@ func (link Link) MarshalJSON() ([]byte, error) { m["requestBody"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Link to a copy of data. diff --git a/openapi3/maplike.go b/openapi3/maplike.go index b27cbf6c5..8829b8db5 100644 --- a/openapi3/maplike.go +++ b/openapi3/maplike.go @@ -76,8 +76,8 @@ func (responses Responses) JSONLookup(token string) (interface{}, error) { } } -// MarshalJSON returns the JSON encoding of Responses. -func (responses *Responses) MarshalJSON() ([]byte, error) { +// MarshalYAML returns the YAML encoding of Responses. +func (responses *Responses) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, responses.Len()+len(responses.Extensions)) for k, v := range responses.Extensions { m[k] = v @@ -85,7 +85,16 @@ func (responses *Responses) MarshalJSON() ([]byte, error) { for k, v := range responses.Map() { m[k] = v } - return json.Marshal(m) + return m, nil +} + +// MarshalJSON returns the JSON encoding of Responses. +func (responses *Responses) MarshalJSON() ([]byte, error) { + responsesYaml, err := responses.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(responsesYaml) } // UnmarshalJSON sets Responses to a copy of data. @@ -195,8 +204,8 @@ func (callback Callback) JSONLookup(token string) (interface{}, error) { } } -// MarshalJSON returns the JSON encoding of Callback. -func (callback *Callback) MarshalJSON() ([]byte, error) { +// MarshalYAML returns the YAML encoding of Callback. +func (callback *Callback) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, callback.Len()+len(callback.Extensions)) for k, v := range callback.Extensions { m[k] = v @@ -204,7 +213,16 @@ func (callback *Callback) MarshalJSON() ([]byte, error) { for k, v := range callback.Map() { m[k] = v } - return json.Marshal(m) + return m, nil +} + +// MarshalJSON returns the JSON encoding of Callback. +func (callback *Callback) MarshalJSON() ([]byte, error) { + callbackYaml, err := callback.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(callbackYaml) } // UnmarshalJSON sets Callback to a copy of data. @@ -314,8 +332,8 @@ func (paths Paths) JSONLookup(token string) (interface{}, error) { } } -// MarshalJSON returns the JSON encoding of Paths. -func (paths *Paths) MarshalJSON() ([]byte, error) { +// MarshalYAML returns the YAML encoding of Paths. +func (paths *Paths) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, paths.Len()+len(paths.Extensions)) for k, v := range paths.Extensions { m[k] = v @@ -323,7 +341,16 @@ func (paths *Paths) MarshalJSON() ([]byte, error) { for k, v := range paths.Map() { m[k] = v } - return json.Marshal(m) + return m, nil +} + +// MarshalJSON returns the JSON encoding of Paths. +func (paths *Paths) MarshalJSON() ([]byte, error) { + pathsYaml, err := paths.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(pathsYaml) } // UnmarshalJSON sets Paths to a copy of data. diff --git a/openapi3/marsh.go b/openapi3/marsh.go index 18036ae78..9be7bb44c 100644 --- a/openapi3/marsh.go +++ b/openapi3/marsh.go @@ -17,10 +17,18 @@ func unmarshalError(jsonUnmarshalErr error) error { } func unmarshal(data []byte, v interface{}) error { + var jsonErr, yamlErr error + // See https://github.com/getkin/kin-openapi/issues/680 - if err := json.Unmarshal(data, v); err != nil { - // UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys - return yaml.Unmarshal(data, v) + if jsonErr = json.Unmarshal(data, v); jsonErr == nil { + return nil + } + + // UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys + if yamlErr = yaml.Unmarshal(data, v); yamlErr == nil { + return nil } - return nil + + // If both unmarshaling attempts fail, return a new error that includes both errors + return fmt.Errorf("failed to unmarshal data: json error: %v, yaml error: %v", jsonErr, yamlErr) } diff --git a/openapi3/media_type.go b/openapi3/media_type.go index e043a7c95..02de1dbc5 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -65,6 +65,15 @@ func (mediaType *MediaType) WithEncoding(name string, enc *Encoding) *MediaType // MarshalJSON returns the JSON encoding of MediaType. func (mediaType MediaType) MarshalJSON() ([]byte, error) { + x, err := mediaType.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of MediaType. +func (mediaType MediaType) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 4+len(mediaType.Extensions)) for k, v := range mediaType.Extensions { m[k] = v @@ -81,7 +90,7 @@ func (mediaType MediaType) MarshalJSON() ([]byte, error) { if x := mediaType.Encoding; len(x) != 0 { m["encoding"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets MediaType to a copy of data. diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 04df3505b..04bac8ff7 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -55,6 +55,15 @@ func (doc *T) JSONLookup(token string) (interface{}, error) { // MarshalJSON returns the JSON encoding of T. func (doc *T) MarshalJSON() ([]byte, error) { + x, err := doc.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of T. +func (doc *T) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 4+len(doc.Extensions)) for k, v := range doc.Extensions { m[k] = v @@ -77,7 +86,7 @@ func (doc *T) MarshalJSON() ([]byte, error) { if x := doc.ExternalDocs; x != nil { m["externalDocs"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets T to a copy of data. diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index 989294823..3b68c1693 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -82,18 +82,9 @@ func TestRefsYAML(t *testing.T) { require.NoError(t, err) dataB, err := yaml.Marshal(docB) require.NoError(t, err) - eqYAML(t, data, specYAML) - eqYAML(t, data, dataA) - eqYAML(t, data, dataB) -} - -func eqYAML(t *testing.T, expected, actual []byte) { - var e, a interface{} - err := yaml.Unmarshal(expected, &e) - require.NoError(t, err) - err = yaml.Unmarshal(actual, &a) - require.NoError(t, err) - require.Equal(t, e, a) + require.YAMLEq(t, string(data), string(specYAML)) + require.YAMLEq(t, string(data), string(dataA)) + require.YAMLEq(t, string(data), string(dataB)) } var specYAML = []byte(` diff --git a/openapi3/operation.go b/openapi3/operation.go index d859a437c..41e3c9b99 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -58,6 +58,15 @@ func NewOperation() *Operation { // MarshalJSON returns the JSON encoding of Operation. func (operation Operation) MarshalJSON() ([]byte, error) { + x, err := operation.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Operation. +func (operation Operation) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 12+len(operation.Extensions)) for k, v := range operation.Extensions { m[k] = v @@ -96,7 +105,7 @@ func (operation Operation) MarshalJSON() ([]byte, error) { if x := operation.ExternalDocs; x != nil { m["externalDocs"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Operation to a copy of data. diff --git a/openapi3/parameter.go b/openapi3/parameter.go index f5a157de2..a13c1121b 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -150,6 +150,15 @@ func (parameter *Parameter) WithSchema(value *Schema) *Parameter { // MarshalJSON returns the JSON encoding of Parameter. func (parameter Parameter) MarshalJSON() ([]byte, error) { + x, err := parameter.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Parameter. +func (parameter Parameter) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 13+len(parameter.Extensions)) for k, v := range parameter.Extensions { m[k] = v @@ -195,7 +204,7 @@ func (parameter Parameter) MarshalJSON() ([]byte, error) { m["content"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Parameter to a copy of data. diff --git a/openapi3/path_item.go b/openapi3/path_item.go index e5dd0fb63..0a6493a1f 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -31,8 +31,17 @@ type PathItem struct { // MarshalJSON returns the JSON encoding of PathItem. func (pathItem PathItem) MarshalJSON() ([]byte, error) { + x, err := pathItem.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of PathItem. +func (pathItem PathItem) MarshalYAML() (interface{}, error) { if ref := pathItem.Ref; ref != "" { - return json.Marshal(Ref{Ref: ref}) + return Ref{Ref: ref}, nil } m := make(map[string]interface{}, 13+len(pathItem.Extensions)) @@ -78,7 +87,7 @@ func (pathItem PathItem) MarshalJSON() ([]byte, error) { if x := pathItem.Parameters; len(x) != 0 { m["parameters"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets PathItem to a copy of data. diff --git a/openapi3/paths.go b/openapi3/paths.go index daafe71cc..ac4f58bbb 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -218,21 +218,6 @@ func (paths *Paths) validateUniqueOperationIDs() error { return nil } -// Support YAML Marshaler interface for gopkg.in/yaml -func (paths *Paths) MarshalYAML() (any, error) { - res := make(map[string]any, len(paths.Extensions)+len(paths.m)) - - for k, v := range paths.Extensions { - res[k] = v - } - - for k, v := range paths.m { - res[k] = v - } - - return res, nil -} - func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) { if strings.IndexByte(path, '{') < 0 { return path, 0, nil diff --git a/openapi3/ref.go b/openapi3/ref.go index a937de4a5..07060731f 100644 --- a/openapi3/ref.go +++ b/openapi3/ref.go @@ -1,5 +1,7 @@ package openapi3 +//go:generate go run refsgenerator.go + // 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 Ref struct { diff --git a/openapi3/refs.go b/openapi3/refs.go index a7e1e3680..087e5abfe 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -1,3 +1,4 @@ +// Code generated by go generate; DO NOT EDIT. package openapi3 import ( @@ -27,15 +28,16 @@ func (x CallbackRef) MarshalYAML() (interface{}, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } - return x.Value, nil + return x.Value.MarshalYAML() } // MarshalJSON returns the JSON encoding of CallbackRef. func (x CallbackRef) MarshalJSON() ([]byte, error) { - if ref := x.Ref; ref != "" { - return json.Marshal(Ref{Ref: ref}) + y, err := x.MarshalYAML() + if err != nil { + return nil, err } - return json.Marshal(x.Value) + return json.Marshal(y) } // UnmarshalJSON sets CallbackRef to a copy of data. @@ -105,15 +107,16 @@ func (x ExampleRef) MarshalYAML() (interface{}, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } - return x.Value, nil + return x.Value.MarshalYAML() } // MarshalJSON returns the JSON encoding of ExampleRef. func (x ExampleRef) MarshalJSON() ([]byte, error) { - if ref := x.Ref; ref != "" { - return json.Marshal(Ref{Ref: ref}) + y, err := x.MarshalYAML() + if err != nil { + return nil, err } - return x.Value.MarshalJSON() + return json.Marshal(y) } // UnmarshalJSON sets ExampleRef to a copy of data. @@ -183,15 +186,16 @@ func (x HeaderRef) MarshalYAML() (interface{}, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } - return x.Value, nil + return x.Value.MarshalYAML() } // MarshalJSON returns the JSON encoding of HeaderRef. func (x HeaderRef) MarshalJSON() ([]byte, error) { - if ref := x.Ref; ref != "" { - return json.Marshal(Ref{Ref: ref}) + y, err := x.MarshalYAML() + if err != nil { + return nil, err } - return x.Value.MarshalJSON() + return json.Marshal(y) } // UnmarshalJSON sets HeaderRef to a copy of data. @@ -261,15 +265,16 @@ func (x LinkRef) MarshalYAML() (interface{}, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } - return x.Value, nil + return x.Value.MarshalYAML() } // MarshalJSON returns the JSON encoding of LinkRef. func (x LinkRef) MarshalJSON() ([]byte, error) { - if ref := x.Ref; ref != "" { - return json.Marshal(Ref{Ref: ref}) + y, err := x.MarshalYAML() + if err != nil { + return nil, err } - return x.Value.MarshalJSON() + return json.Marshal(y) } // UnmarshalJSON sets LinkRef to a copy of data. @@ -339,15 +344,16 @@ func (x ParameterRef) MarshalYAML() (interface{}, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } - return x.Value, nil + return x.Value.MarshalYAML() } // MarshalJSON returns the JSON encoding of ParameterRef. func (x ParameterRef) MarshalJSON() ([]byte, error) { - if ref := x.Ref; ref != "" { - return json.Marshal(Ref{Ref: ref}) + y, err := x.MarshalYAML() + if err != nil { + return nil, err } - return x.Value.MarshalJSON() + return json.Marshal(y) } // UnmarshalJSON sets ParameterRef to a copy of data. @@ -417,15 +423,16 @@ func (x RequestBodyRef) MarshalYAML() (interface{}, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } - return x.Value, nil + return x.Value.MarshalYAML() } // MarshalJSON returns the JSON encoding of RequestBodyRef. func (x RequestBodyRef) MarshalJSON() ([]byte, error) { - if ref := x.Ref; ref != "" { - return json.Marshal(Ref{Ref: ref}) + y, err := x.MarshalYAML() + if err != nil { + return nil, err } - return x.Value.MarshalJSON() + return json.Marshal(y) } // UnmarshalJSON sets RequestBodyRef to a copy of data. @@ -495,15 +502,16 @@ func (x ResponseRef) MarshalYAML() (interface{}, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } - return x.Value, nil + return x.Value.MarshalYAML() } // MarshalJSON returns the JSON encoding of ResponseRef. func (x ResponseRef) MarshalJSON() ([]byte, error) { - if ref := x.Ref; ref != "" { - return json.Marshal(Ref{Ref: ref}) + y, err := x.MarshalYAML() + if err != nil { + return nil, err } - return x.Value.MarshalJSON() + return json.Marshal(y) } // UnmarshalJSON sets ResponseRef to a copy of data. @@ -573,15 +581,16 @@ func (x SchemaRef) MarshalYAML() (interface{}, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } - return x.Value, nil + return x.Value.MarshalYAML() } // MarshalJSON returns the JSON encoding of SchemaRef. func (x SchemaRef) MarshalJSON() ([]byte, error) { - if ref := x.Ref; ref != "" { - return json.Marshal(Ref{Ref: ref}) + y, err := x.MarshalYAML() + if err != nil { + return nil, err } - return x.Value.MarshalJSON() + return json.Marshal(y) } // UnmarshalJSON sets SchemaRef to a copy of data. @@ -651,15 +660,16 @@ func (x SecuritySchemeRef) MarshalYAML() (interface{}, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } - return x.Value, nil + return x.Value.MarshalYAML() } // MarshalJSON returns the JSON encoding of SecuritySchemeRef. func (x SecuritySchemeRef) MarshalJSON() ([]byte, error) { - if ref := x.Ref; ref != "" { - return json.Marshal(Ref{Ref: ref}) + y, err := x.MarshalYAML() + if err != nil { + return nil, err } - return x.Value.MarshalJSON() + return json.Marshal(y) } // UnmarshalJSON sets SecuritySchemeRef to a copy of data. diff --git a/openapi3/refs.tmpl b/openapi3/refs.tmpl new file mode 100644 index 000000000..638d6469d --- /dev/null +++ b/openapi3/refs.tmpl @@ -0,0 +1,92 @@ +// Code generated by go generate; DO NOT EDIT. +package {{ .Package }} + +import ( + "context" + "encoding/json" + "fmt" + "sort" + + "github.com/go-openapi/jsonpointer" + "github.com/perimeterx/marshmallow" +) +{{ range $type := .Types }} +// {{ $type }}Ref represents either a {{ $type }} or a $ref to a {{ $type }}. +// When serializing and both fields are set, Ref is preferred over Value. +type {{ $type }}Ref struct { + Ref string + Value *{{ $type }} + extra []string +} + +var _ jsonpointer.JSONPointable = (*{{ $type }}Ref)(nil) + +func (x *{{ $type }}Ref) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } + +// MarshalYAML returns the YAML encoding of {{ $type }}Ref. +func (x {{ $type }}Ref) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value.MarshalYAML() +} + +// MarshalJSON returns the JSON encoding of {{ $type }}Ref. +func (x {{ $type }}Ref) MarshalJSON() ([]byte, error) { + y, err := x.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(y) +} + +// UnmarshalJSON sets {{ $type }}Ref to a copy of data. +func (x *{{ $type }}Ref) UnmarshalJSON(data []byte) error { + var refOnly Ref + if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { + x.Ref = refOnly.Ref + if len(extra) != 0 { + x.extra = make([]string, 0, len(extra)) + for key := range extra { + x.extra = append(x.extra, key) + } + sort.Strings(x.extra) + } + return nil + } + return json.Unmarshal(data, &x.Value) +} + +// Validate returns an error if {{ $type }}Ref does not comply with the OpenAPI spec. +func (x *{{ $type }}Ref) Validate(ctx context.Context, opts ...ValidationOption) error { + ctx = WithValidationOptions(ctx, opts...) + if extra := x.extra; len(extra) != 0 { + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for _, ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) + } + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + } + if v := x.Value; v != nil { + return v.Validate(ctx) + } + return foundUnresolvedRef(x.Ref) +} + +// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (x *{{ $type }}Ref) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return x.Ref, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) + return ptr, err +} +{{ end -}} diff --git a/openapi3/refsgenerator.go b/openapi3/refsgenerator.go new file mode 100644 index 000000000..5bddfe258 --- /dev/null +++ b/openapi3/refsgenerator.go @@ -0,0 +1,49 @@ +//go:build ignore +// +build ignore + +// The program generates refs.go, invoke `go generate ./...` to run. +package main + +import ( + _ "embed" + "os" + "text/template" +) + +//go:embed refs.tmpl +var tmplData string + +func main() { + file, err := os.Create("refs.go") + if err != nil { + panic(err) + } + + defer func() { + if err := file.Close(); err != nil { + panic(err) + } + }() + + packageTemplate := template.Must(template.New("openapi3-refs").Parse(tmplData)) + + if err := packageTemplate.Execute(file, struct { + Package string + Types []string + }{ + Package: os.Getenv("GOPACKAGE"), // set by the go:generate directive + Types: []string{ + "Callback", + "Example", + "Header", + "Link", + "Parameter", + "RequestBody", + "Response", + "Schema", + "SecurityScheme", + }, + }); err != nil { + panic(err) + } +} diff --git a/openapi3/request_body.go b/openapi3/request_body.go index acd2d0e8c..7b8d35399 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -75,6 +75,15 @@ func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType { // MarshalJSON returns the JSON encoding of RequestBody. func (requestBody RequestBody) MarshalJSON() ([]byte, error) { + x, err := requestBody.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of RequestBody. +func (requestBody RequestBody) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 3+len(requestBody.Extensions)) for k, v := range requestBody.Extensions { m[k] = v @@ -88,7 +97,7 @@ func (requestBody RequestBody) MarshalJSON() ([]byte, error) { if x := requestBody.Content; true { m["content"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets RequestBody to a copy of data. diff --git a/openapi3/response.go b/openapi3/response.go index f69c237b3..6209b5810 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -98,21 +98,6 @@ func (responses *Responses) Validate(ctx context.Context, opts ...ValidationOpti return validateExtensions(ctx, responses.Extensions) } -// Support YAML Marshaler interface for gopkg.in/yaml -func (responses *Responses) MarshalYAML() (any, error) { - res := make(map[string]any, len(responses.Extensions)+len(responses.m)) - - for k, v := range responses.Extensions { - res[k] = v - } - - for k, v := range responses.m { - res[k] = v - } - - return res, nil -} - // Response is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#response-object type Response struct { @@ -150,6 +135,15 @@ func (response *Response) WithJSONSchemaRef(schema *SchemaRef) *Response { // MarshalJSON returns the JSON encoding of Response. func (response Response) MarshalJSON() ([]byte, error) { + x, err := response.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Response. +func (response Response) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 4+len(response.Extensions)) for k, v := range response.Extensions { m[k] = v @@ -166,7 +160,7 @@ func (response Response) MarshalJSON() ([]byte, error) { if x := response.Links; len(x) != 0 { m["links"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Response to a copy of data. diff --git a/openapi3/schema.go b/openapi3/schema.go index ae28afef7..8bacf729d 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -222,23 +222,18 @@ func (addProps AdditionalProperties) MarshalYAML() (interface{}, error) { return false, nil } if x := addProps.Schema; x != nil { - return x.Value, nil + return x.MarshalYAML() } return nil, nil } // MarshalJSON returns the JSON encoding of AdditionalProperties. func (addProps AdditionalProperties) MarshalJSON() ([]byte, error) { - if x := addProps.Has; x != nil { - if *x { - return []byte("true"), nil - } - return []byte("false"), nil - } - if x := addProps.Schema; x != nil { - return json.Marshal(x) + x, err := addProps.MarshalYAML() + if err != nil { + return nil, err } - return nil, nil + return json.Marshal(x) } // UnmarshalJSON sets AdditionalProperties to a copy of data. @@ -275,6 +270,16 @@ func NewSchema() *Schema { // MarshalJSON returns the JSON encoding of Schema. func (schema Schema) MarshalJSON() ([]byte, error) { + m, err := schema.MarshalYAML() + if err != nil { + return nil, err + } + + return json.Marshal(m) +} + +// MarshalYAML returns the YAML encoding of Schema. +func (schema Schema) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 36+len(schema.Extensions)) for k, v := range schema.Extensions { m[k] = v @@ -401,7 +406,7 @@ func (schema Schema) MarshalJSON() ([]byte, error) { m["discriminator"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Schema to a copy of data. @@ -2119,14 +2124,16 @@ type SchemaError struct { var _ interface{ Unwrap() error } = SchemaError{} func markSchemaErrorKey(err error, key string) error { - var me multiErrorForOneOf - - if errors.As(err, &me) { - err = me.Unwrap() - } if v, ok := err.(*SchemaError); ok { v.reversePath = append(v.reversePath, key) + if v.Origin != nil { + if unwrapped := errors.Unwrap(v.Origin); unwrapped != nil { + if me, ok := unwrapped.(multiErrorForOneOf); ok { + _ = markSchemaErrorKey(MultiError(me), key) + } + } + } return v } if v, ok := err.(MultiError); ok { diff --git a/openapi3/schema_issue940_test.go b/openapi3/schema_issue940_test.go new file mode 100644 index 000000000..95d3d7869 --- /dev/null +++ b/openapi3/schema_issue940_test.go @@ -0,0 +1,72 @@ +package openapi3 + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOneOfErrorPreserved(t *testing.T) { + + SchemaErrorDetailsDisabled = true + defer func() { SchemaErrorDetailsDisabled = false }() + + // language=json + raw := ` +{ + "foo": [ "bar" ] +} +` + + // language=json + schema := ` +{ + "type": "object", + "properties": { + "foo": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "string" + } + ] + } + } +} +` + + s := NewSchema() + err := s.UnmarshalJSON([]byte(schema)) + require.NoError(t, err) + err = s.Validate(context.Background()) + require.NoError(t, err) + + obj := make(map[string]interface{}) + err = json.Unmarshal([]byte(raw), &obj) + require.NoError(t, err) + + err = s.VisitJSON(obj, MultiErrors()) + require.Error(t, err) + + var multiError MultiError + ok := errors.As(err, &multiError) + require.True(t, ok) + var schemaErr *SchemaError + ok = errors.As(multiError[0], &schemaErr) + require.True(t, ok) + + require.Equal(t, "oneOf", schemaErr.SchemaField) + require.Equal(t, `value doesn't match any schema from "oneOf"`, schemaErr.Reason) + require.Equal(t, `Error at "/foo": doesn't match schema due to: value must be a number Or value must be a string`, schemaErr.Error()) + + var me multiErrorForOneOf + ok = errors.As(err, &me) + require.True(t, ok) + require.Equal(t, `Error at "/foo": value must be a number`, me[0].Error()) + require.Equal(t, `Error at "/foo": value must be a string`, me[1].Error()) +} diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index c07bfb619..7b6662c4d 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -52,6 +52,15 @@ func NewJWTSecurityScheme() *SecurityScheme { // MarshalJSON returns the JSON encoding of SecurityScheme. func (ss SecurityScheme) MarshalJSON() ([]byte, error) { + x, err := ss.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of SecurityScheme. +func (ss SecurityScheme) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 8+len(ss.Extensions)) for k, v := range ss.Extensions { m[k] = v @@ -80,7 +89,7 @@ func (ss SecurityScheme) MarshalJSON() ([]byte, error) { if x := ss.OpenIdConnectUrl; x != "" { m["openIdConnectUrl"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets SecurityScheme to a copy of data. @@ -225,6 +234,15 @@ const ( // MarshalJSON returns the JSON encoding of OAuthFlows. func (flows OAuthFlows) MarshalJSON() ([]byte, error) { + x, err := flows.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of OAuthFlows. +func (flows OAuthFlows) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 4+len(flows.Extensions)) for k, v := range flows.Extensions { m[k] = v @@ -241,7 +259,7 @@ func (flows OAuthFlows) MarshalJSON() ([]byte, error) { if x := flows.AuthorizationCode; x != nil { m["authorizationCode"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets OAuthFlows to a copy of data. @@ -307,6 +325,15 @@ type OAuthFlow struct { // MarshalJSON returns the JSON encoding of OAuthFlow. func (flow OAuthFlow) MarshalJSON() ([]byte, error) { + x, err := flow.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of OAuthFlow. +func (flow OAuthFlow) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 4+len(flow.Extensions)) for k, v := range flow.Extensions { m[k] = v @@ -321,7 +348,7 @@ func (flow OAuthFlow) MarshalJSON() ([]byte, error) { m["refreshUrl"] = x } m["scopes"] = flow.Scopes - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets OAuthFlow to a copy of data. diff --git a/openapi3/server.go b/openapi3/server.go index 04e233d51..85b716fc9 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -84,6 +84,15 @@ func (server *Server) BasePath() (string, error) { // MarshalJSON returns the JSON encoding of Server. func (server Server) MarshalJSON() ([]byte, error) { + x, err := server.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Server. +func (server Server) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 3+len(server.Extensions)) for k, v := range server.Extensions { m[k] = v @@ -95,7 +104,7 @@ func (server Server) MarshalJSON() ([]byte, error) { if x := server.Variables; len(x) != 0 { m["variables"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Server to a copy of data. @@ -234,6 +243,15 @@ type ServerVariable struct { // MarshalJSON returns the JSON encoding of ServerVariable. func (serverVariable ServerVariable) MarshalJSON() ([]byte, error) { + x, err := serverVariable.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of ServerVariable. +func (serverVariable ServerVariable) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 4+len(serverVariable.Extensions)) for k, v := range serverVariable.Extensions { m[k] = v @@ -247,7 +265,7 @@ func (serverVariable ServerVariable) MarshalJSON() ([]byte, error) { if x := serverVariable.Description; x != "" { m["description"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets ServerVariable to a copy of data. diff --git a/openapi3/tag.go b/openapi3/tag.go index eea6462f5..8dc996724 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -42,6 +42,15 @@ type Tag struct { // MarshalJSON returns the JSON encoding of Tag. func (t Tag) MarshalJSON() ([]byte, error) { + x, err := t.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of Tag. +func (t Tag) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 3+len(t.Extensions)) for k, v := range t.Extensions { m[k] = v @@ -55,7 +64,7 @@ func (t Tag) MarshalJSON() ([]byte, error) { if x := t.ExternalDocs; x != nil { m["externalDocs"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets Tag to a copy of data. diff --git a/openapi3/testdata/issue241.yml b/openapi3/testdata/issue241.yml index 07609c1d8..1bcd73dce 100644 --- a/openapi3/testdata/issue241.yml +++ b/openapi3/testdata/issue241.yml @@ -1,15 +1,15 @@ -openapi: 3.0.3 components: schemas: FooBar: - type: object properties: type_url: type: string value: - type: string format: byte + type: string + type: object info: title: sample version: version not set +openapi: 3.0.3 paths: {} diff --git a/openapi3/testdata/issue959/components.yml b/openapi3/testdata/issue959/components.yml new file mode 100644 index 000000000..c7095e90d --- /dev/null +++ b/openapi3/testdata/issue959/components.yml @@ -0,0 +1,4 @@ +components: + schemas: + External1: + type: string \ No newline at end of file diff --git a/openapi3/testdata/issue959/openapi.yml b/openapi3/testdata/issue959/openapi.yml new file mode 100644 index 000000000..de5bbfe48 --- /dev/null +++ b/openapi3/testdata/issue959/openapi.yml @@ -0,0 +1,16 @@ +openapi: 3.0.0 +info: + title: foo + version: 0.0.0 +paths: + /{external1}: + parameters: + - in: path + name: external1 + required: true + schema: + $ref: './components.yml#/components/schemas/External1' + get: + responses: + '204': + description: No content \ No newline at end of file diff --git a/openapi3/testdata/issue959/openapi.yml.internalized.yml b/openapi3/testdata/issue959/openapi.yml.internalized.yml new file mode 100644 index 000000000..3cbe674d6 --- /dev/null +++ b/openapi3/testdata/issue959/openapi.yml.internalized.yml @@ -0,0 +1,35 @@ +{ + "components": { + "schemas": { + "External1": { + "type": "string" + } + } + }, + "info": { + "title": "foo", + "version": "0.0.0" + }, + "openapi": "3.0.0", + "paths": { + "/{external1}": { + "get": { + "responses": { + "204": { + "description": "No content" + } + } + }, + "parameters": [ + { + "in": "path", + "name": "external1", + "required": true, + "schema": { + "$ref": "#/components/schemas/External1" + } + } + ] + } + } +} \ No newline at end of file diff --git a/openapi3/xml.go b/openapi3/xml.go index 604b607dc..e69c1fa6d 100644 --- a/openapi3/xml.go +++ b/openapi3/xml.go @@ -19,6 +19,15 @@ type XML struct { // MarshalJSON returns the JSON encoding of XML. func (xml XML) MarshalJSON() ([]byte, error) { + x, err := xml.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +// MarshalYAML returns the YAML encoding of XML. +func (xml XML) MarshalYAML() (interface{}, error) { m := make(map[string]interface{}, 5+len(xml.Extensions)) for k, v := range xml.Extensions { m[k] = v @@ -38,7 +47,7 @@ func (xml XML) MarshalJSON() ([]byte, error) { if x := xml.Wrapped; x { m["wrapped"] = x } - return json.Marshal(m) + return m, nil } // UnmarshalJSON sets XML to a copy of data. diff --git a/openapi3filter/middleware.go b/openapi3filter/middleware.go index 0009d61c4..d20889ed9 100644 --- a/openapi3filter/middleware.go +++ b/openapi3filter/middleware.go @@ -2,6 +2,7 @@ package openapi3filter import ( "bytes" + "context" "io" "log" "net/http" @@ -19,10 +20,10 @@ type Validator struct { } // ErrFunc handles errors that may occur during validation. -type ErrFunc func(w http.ResponseWriter, status int, code ErrCode, err error) +type ErrFunc func(ctx context.Context, w http.ResponseWriter, status int, code ErrCode, err error) // LogFunc handles log messages that may occur during validation. -type LogFunc func(message string, err error) +type LogFunc func(ctx context.Context, message string, err error) // ErrCode is used for classification of different types of errors that may // occur during validation. These may be used to write an appropriate response @@ -61,10 +62,10 @@ func (e ErrCode) responseText() string { func NewValidator(router routers.Router, options ...ValidatorOption) *Validator { v := &Validator{ router: router, - errFunc: func(w http.ResponseWriter, status int, code ErrCode, _ error) { + errFunc: func(_ context.Context, w http.ResponseWriter, status int, code ErrCode, _ error) { http.Error(w, code.responseText(), status) }, - logFunc: func(message string, err error) { + logFunc: func(_ context.Context, message string, err error) { log.Printf("%s: %v", message, err) }, } @@ -117,10 +118,11 @@ func ValidationOptions(options Options) ValidatorOption { // request and response validation. func (v *Validator) Middleware(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() route, pathParams, err := v.router.FindRoute(r) if err != nil { - v.logFunc("validation error: failed to find route for "+r.URL.String(), err) - v.errFunc(w, http.StatusNotFound, ErrCodeCannotFindRoute, err) + v.logFunc(ctx, "validation error: failed to find route for "+r.URL.String(), err) + v.errFunc(ctx, w, http.StatusNotFound, ErrCodeCannotFindRoute, err) return } requestValidationInput := &RequestValidationInput{ @@ -129,9 +131,9 @@ func (v *Validator) Middleware(h http.Handler) http.Handler { Route: route, Options: &v.options, } - if err = ValidateRequest(r.Context(), requestValidationInput); err != nil { - v.logFunc("invalid request", err) - v.errFunc(w, http.StatusBadRequest, ErrCodeRequestInvalid, err) + if err = ValidateRequest(ctx, requestValidationInput); err != nil { + v.logFunc(ctx, "invalid request", err) + v.errFunc(ctx, w, http.StatusBadRequest, ErrCodeRequestInvalid, err) return } @@ -144,22 +146,22 @@ func (v *Validator) Middleware(h http.Handler) http.Handler { h.ServeHTTP(wr, r) - if err = ValidateResponse(r.Context(), &ResponseValidationInput{ + if err = ValidateResponse(ctx, &ResponseValidationInput{ RequestValidationInput: requestValidationInput, Status: wr.statusCode(), Header: wr.Header(), Body: io.NopCloser(bytes.NewBuffer(wr.bodyContents())), Options: &v.options, }); err != nil { - v.logFunc("invalid response", err) + v.logFunc(ctx, "invalid response", err) if v.strict { - v.errFunc(w, http.StatusInternalServerError, ErrCodeResponseInvalid, err) + v.errFunc(ctx, w, http.StatusInternalServerError, ErrCodeResponseInvalid, err) } return } if err = wr.flushBodyContents(); err != nil { - v.logFunc("failed to write response", err) + v.logFunc(ctx, "failed to write response", err) } }) } diff --git a/openapi3filter/middleware_test.go b/openapi3filter/middleware_test.go index 1260ac54c..137228cd1 100644 --- a/openapi3filter/middleware_test.go +++ b/openapi3filter/middleware_test.go @@ -2,6 +2,7 @@ package openapi3filter_test import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -489,7 +490,7 @@ paths: // testing a service against its spec in development and CI. In production, // availability may be more important than strictness. v := openapi3filter.NewValidator(router, openapi3filter.Strict(true), - openapi3filter.OnErr(func(w http.ResponseWriter, status int, code openapi3filter.ErrCode, err error) { + openapi3filter.OnErr(func(_ context.Context, w http.ResponseWriter, status int, code openapi3filter.ErrCode, err error) { // Customize validation error responses to use JSON w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 72e9d86d9..882d2d34d 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -650,6 +650,10 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization propsFn = func(params url.Values) (map[string]string, error) { props := make(map[string]string) for key, values := range params { + if !regexp.MustCompile(fmt.Sprintf(`^%s\[`, regexp.QuoteMeta(param))).MatchString(key) { + continue + } + matches := regexp.MustCompile(`\[(.*?)\]`).FindAllStringSubmatch(key, -1) switch l := len(matches); { case l == 0: @@ -1562,7 +1566,7 @@ func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Sch func csvBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { r := csv.NewReader(body) - var content string + var sb strings.Builder for { record, err := r.Read() if err == io.EOF { @@ -1572,8 +1576,9 @@ func csvBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaR return nil, err } - content += strings.Join(record, ",") + "\n" + sb.WriteString(strings.Join(record, ",")) + sb.WriteString("\n") } - return content, nil + return sb.String(), nil } diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index f24b24549..662636b46 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -998,6 +998,17 @@ func TestDecodeParameter(t *testing.T) { query: "anotherparam=bar", want: map[string]interface{}(nil), }, + { + name: "deepObject explode nested object - extraneous deep object param ignored", + param: &openapi3.Parameter{ + Name: "param", In: "query", Style: "deepObject", Explode: explode, + Schema: objectOf( + "obj", objectOf("nestedObjOne", stringSchema, "nestedObjTwo", stringSchema), + ), + }, + query: "anotherparam[obj][nestedObjOne]=one&anotherparam[obj][nestedObjTwo]=two", + want: map[string]interface{}(nil), + }, { name: "deepObject explode nested object - bad array item type", param: &openapi3.Parameter{ diff --git a/refs.sh b/refs.sh deleted file mode 100755 index 9ece26eaa..000000000 --- a/refs.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/bin/bash -eux -set -o pipefail - -types=() -types+=("Callback") -types+=("Example") -types+=("Header") -types+=("Link") -types+=("Parameter") -types+=("RequestBody") -types+=("Response") -types+=("Schema") -types+=("SecurityScheme") - -cat <