From 2ed340d5c6b86a83551a2e6fa24cea6fb449a2c1 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 3 Jan 2023 18:00:58 +0100 Subject: [PATCH] Disallow unexpected fields in validation and drop `jsoninfo` package (#728) Fixes https://github.com/getkin/kin-openapi/issues/513 Fixes https://github.com/getkin/kin-openapi/issues/37 --- .github/workflows/go.yml | 21 +- .github/workflows/shellcheck.yml | 18 + README.md | 3 + go.mod | 1 + go.sum | 13 +- jsoninfo/doc.go | 2 - jsoninfo/marshal.go | 162 ----- jsoninfo/marshal_ref.go | 30 - jsoninfo/marshal_test.go | 190 ------ jsoninfo/strict_struct.go | 6 - jsoninfo/type_info.go | 68 -- jsoninfo/unmarshal.go | 121 ---- jsoninfo/unmarshal_test.go | 156 ----- jsoninfo/unsupported_properties_error.go | 42 -- openapi2/header.go | 15 + openapi2/openapi2.go | 313 +++------ openapi2/operation.go | 91 +++ openapi2/parameter.go | 176 +++++ openapi2/path_item.go | 150 +++++ openapi2/response.go | 60 ++ openapi2/security_scheme.go | 87 +++ openapi2conv/openapi2_conv.go | 272 ++++---- openapi3/components.go | 60 +- openapi3/discriminator.go | 34 +- openapi3/encoding.go | 44 +- openapi3/example.go | 46 +- openapi3/example_validation_test.go | 5 +- openapi3/extension.go | 40 +- openapi3/extension_test.go | 125 ---- openapi3/external_docs.go | 35 +- openapi3/header.go | 54 +- openapi3/info.go | 109 +++- openapi3/internalize_refs.go | 84 +-- openapi3/issue341_test.go | 2 +- openapi3/issue376_test.go | 143 +++-- openapi3/issue513_test.go | 173 +++++ openapi3/link.go | 53 +- ...oad_cicular_ref_with_external_file_test.go | 4 +- openapi3/loader.go | 106 ++- openapi3/loader_paths_test.go | 1 - openapi3/loader_test.go | 11 +- openapi3/media_type.go | 80 ++- openapi3/openapi3.go | 58 +- openapi3/openapi3_test.go | 16 +- openapi3/operation.go | 72 ++- openapi3/parameter.go | 82 ++- openapi3/path_item.go | 82 ++- openapi3/ref.go | 7 + openapi3/refs.go | 603 +++++++++++++----- openapi3/request_body.go | 40 +- openapi3/response.go | 41 +- openapi3/schema.go | 342 ++++++++-- openapi3/schema_test.go | 8 +- openapi3/security_requirements.go | 2 +- openapi3/security_scheme.go | 137 +++- openapi3/server.go | 73 ++- openapi3/tag.go | 37 +- openapi3/validation_options.go | 13 + openapi3/xml.go | 46 +- openapi3filter/issue707_test.go | 39 +- openapi3filter/req_resp_decoder.go | 22 +- openapi3filter/validate_request.go | 9 +- openapi3filter/validation_test.go | 6 +- {jsoninfo => openapi3gen}/field_info.go | 22 +- openapi3gen/openapi3gen.go | 15 +- openapi3gen/openapi3gen_test.go | 2 +- openapi3gen/type_info.go | 54 ++ refs.sh | 125 ++++ routers/gorillamux/router_test.go | 14 +- 69 files changed, 3174 insertions(+), 1999 deletions(-) create mode 100644 .github/workflows/shellcheck.yml delete mode 100644 jsoninfo/doc.go delete mode 100644 jsoninfo/marshal.go delete mode 100644 jsoninfo/marshal_ref.go delete mode 100644 jsoninfo/marshal_test.go delete mode 100644 jsoninfo/strict_struct.go delete mode 100644 jsoninfo/type_info.go delete mode 100644 jsoninfo/unmarshal.go delete mode 100644 jsoninfo/unmarshal_test.go delete mode 100644 jsoninfo/unsupported_properties_error.go create mode 100644 openapi2/header.go create mode 100644 openapi2/operation.go create mode 100644 openapi2/parameter.go create mode 100644 openapi2/path_item.go create mode 100644 openapi2/response.go create mode 100644 openapi2/security_scheme.go delete mode 100644 openapi3/extension_test.go create mode 100644 openapi3/issue513_test.go create mode 100644 openapi3/ref.go rename {jsoninfo => openapi3gen}/field_info.go (78%) create mode 100644 openapi3gen/type_info.go create mode 100755 refs.sh diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e1648fcc7..dab35cb89 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -52,6 +52,11 @@ jobs: - uses: actions/checkout@v2 + - name: Check codegen + run: | + ./refs.sh | tee openapi3/refs.go + git --no-pager diff --exit-code + - run: go mod download && go mod tidy && go mod verify - run: git --no-pager diff --exit-code @@ -102,22 +107,32 @@ jobs: run: | [[ "$(git grep -F yaml. -- openapi3/ | grep -v _test.go | wc -l)" = 1 ]] + - if: runner.os == 'Linux' + name: Ensure non-pointer MarshalJSON + run: | + ! git grep -InE 'func.+[*].+[)].MarshalJSON[(][)]' + - if: runner.os == 'Linux' name: Missing specification object link to definition run: | [[ 31 -eq $(git grep -InE '^// See https:.+OpenAPI-Specification.+3[.]0[.]3[.]md#.+bject$' openapi3/*.go | grep -v _test.go | grep -v doc.go | wc -l) ]] - if: runner.os == 'Linux' - name: Style around ExtensionProps embedding + name: Missing validation of unknown fields in extensions + run: | + [[ $(git grep -InF 'return validateExtensions' -- openapi3 | wc -l) -eq $(git grep -InE '^\s+Extensions.+`' -- openapi3 | wc -l) ]] + + - if: runner.os == 'Linux' + name: Style around Extensions embedding run: | - ! ag -B2 -A2 'type.[A-Z].+struct..\n.+ExtensionProps\n[^\n]' openapi3/*.go + ! ag -B2 -A2 'type.[A-Z].+struct..\n.+Extensions\n[^\n]' openapi3/*.go - if: runner.os == 'Linux' name: Ensure all exported fields are mentioned in Validate() impls run: | for ty in $TYPES; do # Ensure definition - if ! ag 'type.[A-Z].+struct..\n.+ExtensionProps' openapi3/*.go | grep -F "type $ty struct"; then + if ! ag 'type.[A-Z].+struct..\n.+Extensions' openapi3/*.go | grep -F "type $ty struct"; then echo "OAI type $ty is not defined" && exit 1 fi diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 000000000..e1f8d1242 --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,18 @@ +name: ShellCheck + +on: + push: + pull_request: + +jobs: + shellcheck: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Run shellcheck + uses: ludeeus/action-shellcheck@1.1.0 + with: + check_together: 'yes' + severity: error diff --git a/README.md b/README.md index 850b11b8a..27d6699cf 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,9 @@ func arrayUniqueItemsChecker(items []interface{}) bool { ### v0.113.0 * The string format `email` has been removed by default. To use it please call `openapi3.DefineStringFormat("email", openapi3.FormatOfStringForEmail)`. +* Field `openapi3.T.Components` is now a pointer. +* Fields `openapi3.Schema.AdditionalProperties` and `openapi3.Schema.AdditionalPropertiesAllowed` are replaced by `openapi3.Schema.AdditionalProperties.Schema` and `openapi3.Schema.AdditionalProperties.Has` respectively. +* Type `openapi3.ExtensionProps` is now just `map[string]interface{}` and extensions are accessible through the `Extensions` field. ### v0.112.0 * `(openapi3.ValidationOptions).ExamplesValidationDisabled` has been unexported. diff --git a/go.mod b/go.mod index 942b3195c..12a2f1af7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/invopop/yaml v0.1.0 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 + github.com/perimeterx/marshmallow v1.1.4 github.com/stretchr/testify v1.8.1 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 4982bc738..4d05787e4 100644 --- a/go.sum +++ b/go.sum @@ -5,20 +5,27 @@ github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUe github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 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.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/perimeterx/marshmallow v1.1.4 h1:pZLDH9RjlLGGorbXhcaQLhfuV0pFMNfPO55FuFkxqLw= +github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -29,6 +36,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/jsoninfo/doc.go b/jsoninfo/doc.go deleted file mode 100644 index e59ec2c34..000000000 --- a/jsoninfo/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package jsoninfo provides information and functions for marshalling/unmarshalling JSON. -package jsoninfo diff --git a/jsoninfo/marshal.go b/jsoninfo/marshal.go deleted file mode 100644 index f2abf7c00..000000000 --- a/jsoninfo/marshal.go +++ /dev/null @@ -1,162 +0,0 @@ -package jsoninfo - -import ( - "encoding/json" - "fmt" - "reflect" -) - -// MarshalStrictStruct function: -// - Marshals struct fields, ignoring MarshalJSON() and fields without 'json' tag. -// - Correctly handles StrictStruct semantics. -func MarshalStrictStruct(value StrictStruct) ([]byte, error) { - encoder := NewObjectEncoder() - if err := value.EncodeWith(encoder, value); err != nil { - return nil, err - } - return encoder.Bytes() -} - -type ObjectEncoder struct { - result map[string]json.RawMessage -} - -func NewObjectEncoder() *ObjectEncoder { - return &ObjectEncoder{ - result: make(map[string]json.RawMessage), - } -} - -// Bytes returns the result of encoding. -func (encoder *ObjectEncoder) Bytes() ([]byte, error) { - return json.Marshal(encoder.result) -} - -// EncodeExtension adds a key/value to the current JSON object. -func (encoder *ObjectEncoder) EncodeExtension(key string, value interface{}) error { - data, err := json.Marshal(value) - if err != nil { - return err - } - encoder.result[key] = data - return nil -} - -// EncodeExtensionMap adds all properties to the result. -func (encoder *ObjectEncoder) EncodeExtensionMap(value map[string]json.RawMessage) error { - if value != nil { - result := encoder.result - for k, v := range value { - result[k] = v - } - } - return nil -} - -func (encoder *ObjectEncoder) EncodeStructFieldsAndExtensions(value interface{}) error { - reflection := reflect.ValueOf(value) - - // Follow "encoding/json" semantics - if reflection.Kind() != reflect.Ptr { - // Panic because this is a clear programming error - panic(fmt.Errorf("value %s is not a pointer", reflection.Type().String())) - } - if reflection.IsNil() { - // Panic because this is a clear programming error - panic(fmt.Errorf("value %s is nil", reflection.Type().String())) - } - - // Take the element - reflection = reflection.Elem() - - // Obtain typeInfo - typeInfo := GetTypeInfo(reflection.Type()) - - // Declare result - result := encoder.result - - // Supported fields -iteration: - for _, field := range typeInfo.Fields { - // Fields without JSON tag are ignored - if !field.HasJSONTag { - continue - } - - // Marshal - fieldValue := reflection.FieldByIndex(field.Index) - if v, ok := fieldValue.Interface().(json.Marshaler); ok { - if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() { - if field.JSONOmitEmpty { - continue iteration - } - result[field.JSONName] = []byte("null") - continue - } - fieldData, err := v.MarshalJSON() - if err != nil { - return err - } - result[field.JSONName] = fieldData - continue - } - switch fieldValue.Kind() { - case reflect.Ptr, reflect.Interface: - if fieldValue.IsNil() { - if field.JSONOmitEmpty { - continue iteration - } - result[field.JSONName] = []byte("null") - continue - } - case reflect.Struct: - case reflect.Map: - if field.JSONOmitEmpty && (fieldValue.IsNil() || fieldValue.Len() == 0) { - continue iteration - } - case reflect.Slice: - if field.JSONOmitEmpty && fieldValue.Len() == 0 { - continue iteration - } - case reflect.Bool: - x := fieldValue.Bool() - if field.JSONOmitEmpty && !x { - continue iteration - } - s := "false" - if x { - s = "true" - } - result[field.JSONName] = []byte(s) - continue iteration - case reflect.Int64, reflect.Int, reflect.Int32: - if field.JSONOmitEmpty && fieldValue.Int() == 0 { - continue iteration - } - case reflect.Uint64, reflect.Uint, reflect.Uint32: - if field.JSONOmitEmpty && fieldValue.Uint() == 0 { - continue iteration - } - case reflect.Float64: - if field.JSONOmitEmpty && fieldValue.Float() == 0.0 { - continue iteration - } - case reflect.String: - if field.JSONOmitEmpty && len(fieldValue.String()) == 0 { - continue iteration - } - default: - panic(fmt.Errorf("field %q has unsupported type %s", field.JSONName, field.Type.String())) - } - - // No special treament is needed - // Use plain old "encoding/json".Marshal - fieldData, err := json.Marshal(fieldValue.Addr().Interface()) - if err != nil { - return err - } - result[field.JSONName] = fieldData - } - - return nil -} diff --git a/jsoninfo/marshal_ref.go b/jsoninfo/marshal_ref.go deleted file mode 100644 index 29575e9e9..000000000 --- a/jsoninfo/marshal_ref.go +++ /dev/null @@ -1,30 +0,0 @@ -package jsoninfo - -import ( - "encoding/json" -) - -func MarshalRef(value string, otherwise interface{}) ([]byte, error) { - if value != "" { - return json.Marshal(&refProps{ - Ref: value, - }) - } - return json.Marshal(otherwise) -} - -func UnmarshalRef(data []byte, destRef *string, destOtherwise interface{}) error { - refProps := &refProps{} - if err := json.Unmarshal(data, refProps); err == nil { - ref := refProps.Ref - if ref != "" { - *destRef = ref - return nil - } - } - return json.Unmarshal(data, destOtherwise) -} - -type refProps struct { - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` -} diff --git a/jsoninfo/marshal_test.go b/jsoninfo/marshal_test.go deleted file mode 100644 index 10551542d..000000000 --- a/jsoninfo/marshal_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package jsoninfo_test - -import ( - "encoding/json" - "testing" - "time" - - "github.com/getkin/kin-openapi/jsoninfo" - "github.com/getkin/kin-openapi/openapi3" -) - -type Simple struct { - openapi3.ExtensionProps - Bool bool `json:"bool"` - Int int `json:"int"` - Int64 int64 `json:"int64"` - Float64 float64 `json:"float64"` - Time time.Time `json:"time"` - String string `json:"string"` - Bytes []byte `json:"bytes"` -} - -type SimpleOmitEmpty struct { - openapi3.ExtensionProps - Bool bool `json:"bool,omitempty"` - Int int `json:"int,omitempty"` - Int64 int64 `json:"int64,omitempty"` - Float64 float64 `json:"float64,omitempty"` - Time time.Time `json:"time,omitempty"` - String string `json:"string,omitempty"` - Bytes []byte `json:"bytes,omitempty"` -} - -type SimplePtrOmitEmpty struct { - openapi3.ExtensionProps - Bool *bool `json:"bool,omitempty"` - Int *int `json:"int,omitempty"` - Int64 *int64 `json:"int64,omitempty"` - Float64 *float64 `json:"float64,omitempty"` - Time *time.Time `json:"time,omitempty"` - String *string `json:"string,omitempty"` - Bytes *[]byte `json:"bytes,omitempty"` -} - -type OriginalNameType struct { - openapi3.ExtensionProps - Field string `json:",omitempty"` -} - -type RootType struct { - openapi3.ExtensionProps - EmbeddedType0 - EmbeddedType1 -} - -type EmbeddedType0 struct { - openapi3.ExtensionProps - Field0 string `json:"embedded0,omitempty"` -} - -type EmbeddedType1 struct { - openapi3.ExtensionProps - Field1 string `json:"embedded1,omitempty"` -} - -// Example describes expected outcome of: -// -// 1.Marshal JSON -// 2.Unmarshal value -// 3.Marshal value -type Example struct { - NoMarshal bool - NoUnmarshal bool - Value jsoninfo.StrictStruct - JSON interface{} -} - -var Examples = []Example{ - // Primitives - { - Value: &SimpleOmitEmpty{}, - JSON: Object{ - "time": time.Unix(0, 0), - }, - }, - { - Value: &SimpleOmitEmpty{}, - JSON: Object{ - "bool": true, - "int": 42, - "int64": 42, - "float64": 3.14, - "string": "abc", - "bytes": []byte{1, 2, 3}, - "time": time.Unix(1, 0), - }, - }, - - // Pointers - { - Value: &SimplePtrOmitEmpty{}, - JSON: Object{}, - }, - { - Value: &SimplePtrOmitEmpty{}, - JSON: Object{ - "bool": true, - "int": 42, - "int64": 42, - "float64": 3.14, - "string": "abc", - "bytes": []byte{1, 2, 3}, - "time": time.Unix(1, 0), - }, - }, - - // JSON tag "fieldName" - { - Value: &Simple{}, - JSON: Object{ - "bool": false, - "int": 0, - "int64": 0, - "float64": 0, - "string": "", - "bytes": []byte{}, - "time": time.Unix(0, 0), - }, - }, - - // JSON tag ",omitempty" - { - Value: &OriginalNameType{}, - JSON: Object{ - "Field": "abc", - }, - }, - - // Embedding - { - Value: &RootType{}, - JSON: Object{}, - }, - { - Value: &RootType{}, - JSON: Object{ - "embedded0": "0", - "embedded1": "1", - "x-other": "abc", - }, - }, -} - -type Object map[string]interface{} - -func TestExtensions(t *testing.T) { - for _, example := range Examples { - // Define JSON that will be unmarshalled - expectedData, err := json.Marshal(example.JSON) - if err != nil { - panic(err) - } - expected := string(expectedData) - - // Define value that will marshalled - x := example.Value - - // Unmarshal - if !example.NoUnmarshal { - t.Logf("Unmarshalling %T", x) - if err := jsoninfo.UnmarshalStrictStruct(expectedData, x); err != nil { - t.Fatalf("Error unmarshalling %T: %v", x, err) - } - t.Logf("Marshalling %T", x) - } - - // Marshal - if !example.NoMarshal { - data, err := jsoninfo.MarshalStrictStruct(x) - if err != nil { - t.Fatalf("Error marshalling: %v", err) - } - actually := string(data) - - if actually != expected { - t.Fatalf("Error!\nExpected: %s\nActually: %s", expected, actually) - } - } - } -} diff --git a/jsoninfo/strict_struct.go b/jsoninfo/strict_struct.go deleted file mode 100644 index 6b4d83977..000000000 --- a/jsoninfo/strict_struct.go +++ /dev/null @@ -1,6 +0,0 @@ -package jsoninfo - -type StrictStruct interface { - EncodeWith(encoder *ObjectEncoder, value interface{}) error - DecodeWith(decoder *ObjectDecoder, value interface{}) error -} diff --git a/jsoninfo/type_info.go b/jsoninfo/type_info.go deleted file mode 100644 index 3dbb8d5d6..000000000 --- a/jsoninfo/type_info.go +++ /dev/null @@ -1,68 +0,0 @@ -package jsoninfo - -import ( - "reflect" - "sort" - "sync" -) - -var ( - typeInfos = map[reflect.Type]*TypeInfo{} - typeInfosMutex sync.RWMutex -) - -// TypeInfo contains information about JSON serialization of a type -type TypeInfo struct { - Type reflect.Type - Fields []FieldInfo -} - -func GetTypeInfoForValue(value interface{}) *TypeInfo { - return GetTypeInfo(reflect.TypeOf(value)) -} - -// GetTypeInfo returns TypeInfo for the given type. -func GetTypeInfo(t reflect.Type) *TypeInfo { - for t.Kind() == reflect.Ptr { - t = t.Elem() - } - typeInfosMutex.RLock() - typeInfo, exists := typeInfos[t] - typeInfosMutex.RUnlock() - if exists { - return typeInfo - } - if t.Kind() != reflect.Struct { - typeInfo = &TypeInfo{ - Type: t, - } - } else { - // Allocate - typeInfo = &TypeInfo{ - Type: t, - Fields: make([]FieldInfo, 0, 16), - } - - // Add fields - typeInfo.Fields = AppendFields(nil, nil, t) - - // Sort fields - sort.Sort(sortableFieldInfos(typeInfo.Fields)) - } - - // Publish - typeInfosMutex.Lock() - typeInfos[t] = typeInfo - typeInfosMutex.Unlock() - return typeInfo -} - -// FieldNames returns all field names -func (typeInfo *TypeInfo) FieldNames() []string { - fields := typeInfo.Fields - names := make([]string, 0, len(fields)) - for _, field := range fields { - names = append(names, field.JSONName) - } - return names -} diff --git a/jsoninfo/unmarshal.go b/jsoninfo/unmarshal.go deleted file mode 100644 index 16886ad83..000000000 --- a/jsoninfo/unmarshal.go +++ /dev/null @@ -1,121 +0,0 @@ -package jsoninfo - -import ( - "encoding/json" - "fmt" - "reflect" -) - -// UnmarshalStrictStruct function: -// - Unmarshals struct fields, ignoring UnmarshalJSON(...) and fields without 'json' tag. -// - Correctly handles StrictStruct -func UnmarshalStrictStruct(data []byte, value StrictStruct) error { - decoder, err := NewObjectDecoder(data) - if err != nil { - return err - } - return value.DecodeWith(decoder, value) -} - -type ObjectDecoder struct { - Data []byte - remainingFields map[string]json.RawMessage -} - -func NewObjectDecoder(data []byte) (*ObjectDecoder, error) { - var remainingFields map[string]json.RawMessage - if err := json.Unmarshal(data, &remainingFields); err != nil { - return nil, fmt.Errorf("failed to unmarshal extension properties: %w (%s)", err, data) - } - return &ObjectDecoder{ - Data: data, - remainingFields: remainingFields, - }, nil -} - -// DecodeExtensionMap returns all properties that were not decoded previously. -func (decoder *ObjectDecoder) DecodeExtensionMap() map[string]json.RawMessage { - return decoder.remainingFields -} - -func (decoder *ObjectDecoder) DecodeStructFieldsAndExtensions(value interface{}) error { - reflection := reflect.ValueOf(value) - if reflection.Kind() != reflect.Ptr { - panic(fmt.Errorf("value %T is not a pointer", value)) - } - if reflection.IsNil() { - panic(fmt.Errorf("value %T is nil", value)) - } - reflection = reflection.Elem() - for (reflection.Kind() == reflect.Interface || reflection.Kind() == reflect.Ptr) && !reflection.IsNil() { - reflection = reflection.Elem() - } - reflectionType := reflection.Type() - if reflectionType.Kind() != reflect.Struct { - panic(fmt.Errorf("value %T is not a struct", value)) - } - typeInfo := GetTypeInfo(reflectionType) - - // Supported fields - fields := typeInfo.Fields - remainingFields := decoder.remainingFields - for fieldIndex, field := range fields { - // Fields without JSON tag are ignored - if !field.HasJSONTag { - continue - } - - // Get data - fieldData, exists := remainingFields[field.JSONName] - if !exists { - continue - } - - // Unmarshal - if field.TypeIsUnmarshaller { - fieldType := field.Type - isPtr := false - if fieldType.Kind() == reflect.Ptr { - fieldType = fieldType.Elem() - isPtr = true - } - fieldValue := reflect.New(fieldType) - if err := fieldValue.Interface().(json.Unmarshaler).UnmarshalJSON(fieldData); err != nil { - if field.MultipleFields { - i := fieldIndex + 1 - if i < len(fields) && fields[i].JSONName == field.JSONName { - continue - } - } - return fmt.Errorf("failed to unmarshal property %q (%s): %w", - field.JSONName, fieldValue.Type().String(), err) - } - if !isPtr { - fieldValue = fieldValue.Elem() - } - reflection.FieldByIndex(field.Index).Set(fieldValue) - - // Remove the field from remaining fields - delete(remainingFields, field.JSONName) - } else { - fieldPtr := reflection.FieldByIndex(field.Index) - if fieldPtr.Kind() != reflect.Ptr || fieldPtr.IsNil() { - fieldPtr = fieldPtr.Addr() - } - if err := json.Unmarshal(fieldData, fieldPtr.Interface()); err != nil { - if field.MultipleFields { - i := fieldIndex + 1 - if i < len(fields) && fields[i].JSONName == field.JSONName { - continue - } - } - return fmt.Errorf("failed to unmarshal property %q (%s): %w", - field.JSONName, fieldPtr.Type().String(), err) - } - - // Remove the field from remaining fields - delete(remainingFields, field.JSONName) - } - } - return nil -} diff --git a/jsoninfo/unmarshal_test.go b/jsoninfo/unmarshal_test.go deleted file mode 100644 index dd25f04b6..000000000 --- a/jsoninfo/unmarshal_test.go +++ /dev/null @@ -1,156 +0,0 @@ -package jsoninfo - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewObjectDecoder(t *testing.T) { - data := []byte(` - { - "field1": 1, - "field2": 2 - } -`) - t.Run("test new object decoder", func(t *testing.T) { - decoder, err := NewObjectDecoder(data) - require.NoError(t, err) - require.NotNil(t, decoder) - require.Equal(t, data, decoder.Data) - require.Equal(t, 2, len(decoder.DecodeExtensionMap())) - }) -} - -type mockStrictStruct struct { - EncodeWithFn func(encoder *ObjectEncoder, value interface{}) error - DecodeWithFn func(decoder *ObjectDecoder, value interface{}) error -} - -func (m *mockStrictStruct) EncodeWith(encoder *ObjectEncoder, value interface{}) error { - return m.EncodeWithFn(encoder, value) -} - -func (m *mockStrictStruct) DecodeWith(decoder *ObjectDecoder, value interface{}) error { - return m.DecodeWithFn(decoder, value) -} - -func TestUnmarshalStrictStruct(t *testing.T) { - data := []byte(` - { - "field1": 1, - "field2": 2 - } - `) - - t.Run("test unmarshal with StrictStruct without err", func(t *testing.T) { - decodeWithFnCalled := 0 - mockStruct := &mockStrictStruct{ - EncodeWithFn: func(encoder *ObjectEncoder, value interface{}) error { - return nil - }, - DecodeWithFn: func(decoder *ObjectDecoder, value interface{}) error { - decodeWithFnCalled++ - return nil - }, - } - err := UnmarshalStrictStruct(data, mockStruct) - require.NoError(t, err) - require.Equal(t, 1, decodeWithFnCalled) - }) - - t.Run("test unmarshal with StrictStruct with err", func(t *testing.T) { - decodeWithFnCalled := 0 - mockStruct := &mockStrictStruct{ - EncodeWithFn: func(encoder *ObjectEncoder, value interface{}) error { - return nil - }, - DecodeWithFn: func(decoder *ObjectDecoder, value interface{}) error { - decodeWithFnCalled++ - return errors.New("unable to decode the value") - }, - } - err := UnmarshalStrictStruct(data, mockStruct) - require.Error(t, err) - require.Equal(t, 1, decodeWithFnCalled) - }) -} - -func TestDecodeStructFieldsAndExtensions(t *testing.T) { - data := []byte(` - { - "field1": "field1", - "field2": "field2" - } -`) - decoder, err := NewObjectDecoder(data) - require.NoError(t, err) - require.NotNil(t, decoder) - - t.Run("value is not pointer", func(t *testing.T) { - var value interface{} - require.Panics(t, func() { - _ = decoder.DecodeStructFieldsAndExtensions(value) - }, "value is not a pointer") - }) - - t.Run("value is nil", func(t *testing.T) { - var value *string = nil - require.Panics(t, func() { - _ = decoder.DecodeStructFieldsAndExtensions(value) - }, "value is nil") - }) - - t.Run("value is not struct", func(t *testing.T) { - var value = "simple string" - require.Panics(t, func() { - _ = decoder.DecodeStructFieldsAndExtensions(&value) - }, "value is not struct") - }) - - t.Run("successfully decoded with all fields", func(t *testing.T) { - d, err := NewObjectDecoder(data) - require.NoError(t, err) - require.NotNil(t, d) - - var value = struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` - }{} - err = d.DecodeStructFieldsAndExtensions(&value) - require.NoError(t, err) - require.Equal(t, "field1", value.Field1) - require.Equal(t, "field2", value.Field2) - require.Equal(t, 0, len(d.DecodeExtensionMap())) - }) - - t.Run("successfully decoded with renaming field", func(t *testing.T) { - d, err := NewObjectDecoder(data) - require.NoError(t, err) - require.NotNil(t, d) - - var value = struct { - Field1 string `json:"field1"` - }{} - err = d.DecodeStructFieldsAndExtensions(&value) - require.NoError(t, err) - require.Equal(t, "field1", value.Field1) - require.Equal(t, 1, len(d.DecodeExtensionMap())) - }) - - t.Run("un-successfully decoded due to data mismatch", func(t *testing.T) { - d, err := NewObjectDecoder(data) - require.NoError(t, err) - require.NotNil(t, d) - - var value = struct { - Field1 int `json:"field1"` - }{} - err = d.DecodeStructFieldsAndExtensions(&value) - require.Error(t, err) - require.EqualError(t, err, `failed to unmarshal property "field1" (*int): json: cannot unmarshal string into Go value of type int`) - require.Equal(t, 0, value.Field1) - require.Equal(t, 2, len(d.DecodeExtensionMap())) - }) -} diff --git a/jsoninfo/unsupported_properties_error.go b/jsoninfo/unsupported_properties_error.go deleted file mode 100644 index f69aafdc3..000000000 --- a/jsoninfo/unsupported_properties_error.go +++ /dev/null @@ -1,42 +0,0 @@ -package jsoninfo - -import ( - "encoding/json" - "fmt" - "sort" -) - -// UnsupportedPropertiesError is a helper for extensions that want to refuse -// unsupported JSON object properties. -// -// It produces a helpful error message. -type UnsupportedPropertiesError struct { - Value interface{} - UnsupportedProperties map[string]json.RawMessage -} - -func NewUnsupportedPropertiesError(v interface{}, m map[string]json.RawMessage) error { - return &UnsupportedPropertiesError{ - Value: v, - UnsupportedProperties: m, - } -} - -func (err *UnsupportedPropertiesError) Error() string { - m := err.UnsupportedProperties - typeInfo := GetTypeInfoForValue(err.Value) - if m == nil || typeInfo == nil { - return fmt.Sprintf("invalid %T", *err) - } - keys := make([]string, 0, len(m)) - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - supported := typeInfo.FieldNames() - if len(supported) == 0 { - return fmt.Sprintf("type \"%T\" doesn't take any properties. Unsupported properties: %+v", - err.Value, keys) - } - return fmt.Sprintf("unsupported properties: %+v (supported properties are: %+v)", keys, supported) -} diff --git a/openapi2/header.go b/openapi2/header.go new file mode 100644 index 000000000..a51f99dee --- /dev/null +++ b/openapi2/header.go @@ -0,0 +1,15 @@ +package openapi2 + +type Header struct { + Parameter +} + +// MarshalJSON returns the JSON encoding of Header. +func (header Header) MarshalJSON() ([]byte, error) { + return header.Parameter.MarshalJSON() +} + +// UnmarshalJSON sets Header to a copy of data. +func (header *Header) UnmarshalJSON(data []byte) error { + return header.Parameter.UnmarshalJSON(data) +} diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 4927ade86..88835db95 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -1,19 +1,17 @@ package openapi2 import ( - "fmt" - "net/http" - "sort" + "encoding/json" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/getkin/kin-openapi/openapi3" ) // T is the root of an OpenAPI v2 document type T struct { - openapi3.ExtensionProps - Swagger string `json:"swagger" yaml:"swagger"` - Info openapi3.Info `json:"info" yaml:"info"` + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Swagger string `json:"swagger" yaml:"swagger"` // required + Info openapi3.Info `json:"info" yaml:"info"` // required ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` @@ -30,253 +28,90 @@ type T struct { } // MarshalJSON returns the JSON encoding of T. -func (doc *T) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(doc) -} - -// UnmarshalJSON sets T to a copy of data. -func (doc *T) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, doc) -} - -func (doc *T) AddOperation(path string, method string, operation *Operation) { - if doc.Paths == nil { - doc.Paths = make(map[string]*PathItem) +func (doc T) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 15+len(doc.Extensions)) + for k, v := range doc.Extensions { + m[k] = v } - pathItem := doc.Paths[path] - if pathItem == nil { - pathItem = &PathItem{} - doc.Paths[path] = pathItem + m["swagger"] = doc.Swagger + m["info"] = doc.Info + if x := doc.ExternalDocs; x != nil { + m["externalDocs"] = x } - pathItem.SetOperation(method, operation) -} - -type PathItem struct { - openapi3.ExtensionProps - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` - Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` - Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` - Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` - Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` - Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` - Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` - Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` -} - -// MarshalJSON returns the JSON encoding of PathItem. -func (pathItem *PathItem) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(pathItem) -} - -// UnmarshalJSON sets PathItem to a copy of data. -func (pathItem *PathItem) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, pathItem) -} - -func (pathItem *PathItem) Operations() map[string]*Operation { - operations := make(map[string]*Operation) - if v := pathItem.Delete; v != nil { - operations[http.MethodDelete] = v + if x := doc.Schemes; len(x) != 0 { + m["schemes"] = x } - if v := pathItem.Get; v != nil { - operations[http.MethodGet] = v + if x := doc.Consumes; len(x) != 0 { + m["consumes"] = x } - if v := pathItem.Head; v != nil { - operations[http.MethodHead] = v + if x := doc.Produces; len(x) != 0 { + m["produces"] = x } - if v := pathItem.Options; v != nil { - operations[http.MethodOptions] = v + if x := doc.Host; x != "" { + m["host"] = x } - if v := pathItem.Patch; v != nil { - operations[http.MethodPatch] = v + if x := doc.BasePath; x != "" { + m["basePath"] = x } - if v := pathItem.Post; v != nil { - operations[http.MethodPost] = v + if x := doc.Paths; len(x) != 0 { + m["paths"] = x } - if v := pathItem.Put; v != nil { - operations[http.MethodPut] = v + if x := doc.Definitions; len(x) != 0 { + m["definitions"] = x } - return operations -} - -func (pathItem *PathItem) GetOperation(method string) *Operation { - switch method { - case http.MethodDelete: - return pathItem.Delete - case http.MethodGet: - return pathItem.Get - case http.MethodHead: - return pathItem.Head - case http.MethodOptions: - return pathItem.Options - case http.MethodPatch: - return pathItem.Patch - case http.MethodPost: - return pathItem.Post - case http.MethodPut: - return pathItem.Put - default: - panic(fmt.Errorf("unsupported HTTP method %q", method)) + if x := doc.Parameters; len(x) != 0 { + m["parameters"] = x } -} - -func (pathItem *PathItem) SetOperation(method string, operation *Operation) { - switch method { - case http.MethodDelete: - pathItem.Delete = operation - case http.MethodGet: - pathItem.Get = operation - case http.MethodHead: - pathItem.Head = operation - case http.MethodOptions: - pathItem.Options = operation - case http.MethodPatch: - pathItem.Patch = operation - case http.MethodPost: - pathItem.Post = operation - case http.MethodPut: - pathItem.Put = operation - default: - panic(fmt.Errorf("unsupported HTTP method %q", method)) + if x := doc.Responses; len(x) != 0 { + m["responses"] = x } -} - -type Operation struct { - openapi3.ExtensionProps - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` - OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` - Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Responses map[string]*Response `json:"responses" yaml:"responses"` - Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` - Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` - Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` - Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` -} - -// MarshalJSON returns the JSON encoding of Operation. -func (operation *Operation) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(operation) -} - -// UnmarshalJSON sets Operation to a copy of data. -func (operation *Operation) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, operation) -} - -type Parameters []*Parameter - -var _ sort.Interface = Parameters{} - -func (ps Parameters) Len() int { return len(ps) } -func (ps Parameters) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] } -func (ps Parameters) Less(i, j int) bool { - if ps[i].Name != ps[j].Name { - return ps[i].Name < ps[j].Name + if x := doc.SecurityDefinitions; len(x) != 0 { + m["securityDefinitions"] = x } - if ps[i].In != ps[j].In { - return ps[i].In < ps[j].In + if x := doc.Security; len(x) != 0 { + m["security"] = x } - return ps[i].Ref < ps[j].Ref -} - -type Parameter struct { - openapi3.ExtensionProps - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` - Format string `json:"format,omitempty" yaml:"format,omitempty"` - Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` - ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` - ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` - Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` - Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` - Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` - MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` - MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` - MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` - MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` - Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` -} - -// MarshalJSON returns the JSON encoding of Parameter. -func (parameter *Parameter) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(parameter) -} - -// UnmarshalJSON sets Parameter to a copy of data. -func (parameter *Parameter) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, parameter) -} - -type Response struct { - openapi3.ExtensionProps - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` - Examples map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` -} - -// MarshalJSON returns the JSON encoding of Response. -func (response *Response) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(response) -} - -// UnmarshalJSON sets Response to a copy of data. -func (response *Response) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, response) -} - -type Header struct { - Parameter -} - -// MarshalJSON returns the JSON encoding of Header. -func (header *Header) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(header) -} - -// UnmarshalJSON sets Header to a copy of data. -func (header *Header) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, header) -} - -type SecurityRequirements []map[string][]string - -type SecurityScheme struct { - openapi3.ExtensionProps - Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Flow string `json:"flow,omitempty" yaml:"flow,omitempty"` - AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` - TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` - Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"` - Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + if x := doc.Tags; len(x) != 0 { + m["tags"] = x + } + return json.Marshal(m) } -// MarshalJSON returns the JSON encoding of SecurityScheme. -func (securityScheme *SecurityScheme) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(securityScheme) +// UnmarshalJSON sets T to a copy of data. +func (doc *T) UnmarshalJSON(data []byte) error { + type TBis T + var x TBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "swagger") + delete(x.Extensions, "info") + delete(x.Extensions, "externalDocs") + delete(x.Extensions, "schemes") + delete(x.Extensions, "consumes") + delete(x.Extensions, "produces") + delete(x.Extensions, "host") + delete(x.Extensions, "basePath") + delete(x.Extensions, "paths") + delete(x.Extensions, "definitions") + delete(x.Extensions, "parameters") + delete(x.Extensions, "responses") + delete(x.Extensions, "securityDefinitions") + delete(x.Extensions, "security") + delete(x.Extensions, "tags") + *doc = T(x) + return nil } -// UnmarshalJSON sets SecurityScheme to a copy of data. -func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, securityScheme) +func (doc *T) AddOperation(path string, method string, operation *Operation) { + if doc.Paths == nil { + doc.Paths = make(map[string]*PathItem) + } + pathItem := doc.Paths[path] + if pathItem == nil { + pathItem = &PathItem{} + doc.Paths[path] = pathItem + } + pathItem.SetOperation(method, operation) } diff --git a/openapi2/operation.go b/openapi2/operation.go new file mode 100644 index 000000000..b29f67de3 --- /dev/null +++ b/openapi2/operation.go @@ -0,0 +1,91 @@ +package openapi2 + +import ( + "encoding/json" + + "github.com/getkin/kin-openapi/openapi3" +) + +type Operation struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` + OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` + Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Responses map[string]*Response `json:"responses" yaml:"responses"` + Consumes []string `json:"consumes,omitempty" yaml:"consumes,omitempty"` + Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` + Schemes []string `json:"schemes,omitempty" yaml:"schemes,omitempty"` + Security *SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` +} + +// MarshalJSON returns the JSON encoding of Operation. +func (operation Operation) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 12+len(operation.Extensions)) + for k, v := range operation.Extensions { + m[k] = v + } + if x := operation.Summary; x != "" { + m["summary"] = x + } + if x := operation.Description; x != "" { + m["description"] = x + } + if x := operation.Deprecated; x { + m["deprecated"] = x + } + if x := operation.ExternalDocs; x != nil { + m["externalDocs"] = x + } + if x := operation.Tags; len(x) != 0 { + m["tags"] = x + } + if x := operation.OperationID; x != "" { + m["operationId"] = x + } + if x := operation.Parameters; len(x) != 0 { + m["parameters"] = x + } + m["responses"] = operation.Responses + if x := operation.Consumes; len(x) != 0 { + m["consumes"] = x + } + if x := operation.Produces; len(x) != 0 { + m["produces"] = x + } + if x := operation.Schemes; len(x) != 0 { + m["schemes"] = x + } + if x := operation.Security; x != nil { + m["security"] = x + } + return json.Marshal(m) +} + +// UnmarshalJSON sets Operation to a copy of data. +func (operation *Operation) UnmarshalJSON(data []byte) error { + type OperationBis Operation + var x OperationBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "summary") + delete(x.Extensions, "description") + delete(x.Extensions, "deprecated") + delete(x.Extensions, "externalDocs") + delete(x.Extensions, "tags") + delete(x.Extensions, "operationId") + delete(x.Extensions, "parameters") + delete(x.Extensions, "responses") + delete(x.Extensions, "consumes") + delete(x.Extensions, "produces") + delete(x.Extensions, "schemes") + delete(x.Extensions, "security") + *operation = Operation(x) + return nil +} diff --git a/openapi2/parameter.go b/openapi2/parameter.go new file mode 100644 index 000000000..d2c71c64f --- /dev/null +++ b/openapi2/parameter.go @@ -0,0 +1,176 @@ +package openapi2 + +import ( + "encoding/json" + "sort" + + "github.com/getkin/kin-openapi/openapi3" +) + +type Parameters []*Parameter + +var _ sort.Interface = Parameters{} + +func (ps Parameters) Len() int { return len(ps) } +func (ps Parameters) Swap(i, j int) { ps[i], ps[j] = ps[j], ps[i] } +func (ps Parameters) Less(i, j int) bool { + if ps[i].Name != ps[j].Name { + return ps[i].Name < ps[j].Name + } + if ps[i].In != ps[j].In { + return ps[i].In < ps[j].In + } + return ps[i].Ref < ps[j].Ref +} + +type Parameter struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + + In string `json:"in,omitempty" yaml:"in,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` + Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` +} + +// MarshalJSON returns the JSON encoding of Parameter. +func (parameter Parameter) MarshalJSON() ([]byte, error) { + if ref := parameter.Ref; ref != "" { + return json.Marshal(openapi3.Ref{Ref: ref}) + } + + m := make(map[string]interface{}, 24+len(parameter.Extensions)) + for k, v := range parameter.Extensions { + m[k] = v + } + + if x := parameter.In; x != "" { + m["in"] = x + } + if x := parameter.Name; x != "" { + m["name"] = x + } + if x := parameter.Description; x != "" { + m["description"] = x + } + if x := parameter.CollectionFormat; x != "" { + m["collectionFormat"] = x + } + if x := parameter.Type; x != "" { + m["type"] = x + } + if x := parameter.Format; x != "" { + m["format"] = x + } + if x := parameter.Pattern; x != "" { + m["pattern"] = x + } + if x := parameter.AllowEmptyValue; x { + m["allowEmptyValue"] = x + } + if x := parameter.Required; x { + m["required"] = x + } + if x := parameter.UniqueItems; x { + m["uniqueItems"] = x + } + if x := parameter.ExclusiveMin; x { + m["exclusiveMinimum"] = x + } + if x := parameter.ExclusiveMax; x { + m["exclusiveMaximum"] = x + } + if x := parameter.Schema; x != nil { + m["schema"] = x + } + if x := parameter.Items; x != nil { + m["items"] = x + } + if x := parameter.Enum; x != nil { + m["enum"] = x + } + if x := parameter.MultipleOf; x != nil { + m["multipleOf"] = x + } + if x := parameter.Minimum; x != nil { + m["minimum"] = x + } + if x := parameter.Maximum; x != nil { + m["maximum"] = x + } + if x := parameter.MaxLength; x != nil { + m["maxLength"] = x + } + if x := parameter.MaxItems; x != nil { + m["maxItems"] = x + } + if x := parameter.MinLength; x != 0 { + m["minLength"] = x + } + if x := parameter.MinItems; x != 0 { + m["minItems"] = x + } + if x := parameter.Default; x != nil { + m["default"] = x + } + + return json.Marshal(m) +} + +// UnmarshalJSON sets Parameter to a copy of data. +func (parameter *Parameter) UnmarshalJSON(data []byte) error { + type ParameterBis Parameter + var x ParameterBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "$ref") + + delete(x.Extensions, "in") + delete(x.Extensions, "name") + delete(x.Extensions, "description") + delete(x.Extensions, "collectionFormat") + delete(x.Extensions, "type") + delete(x.Extensions, "format") + delete(x.Extensions, "pattern") + delete(x.Extensions, "allowEmptyValue") + delete(x.Extensions, "required") + delete(x.Extensions, "uniqueItems") + delete(x.Extensions, "exclusiveMinimum") + delete(x.Extensions, "exclusiveMaximum") + delete(x.Extensions, "schema") + delete(x.Extensions, "items") + delete(x.Extensions, "enum") + delete(x.Extensions, "multipleOf") + delete(x.Extensions, "minimum") + delete(x.Extensions, "maximum") + delete(x.Extensions, "maxLength") + delete(x.Extensions, "maxItems") + delete(x.Extensions, "minLength") + delete(x.Extensions, "minItems") + delete(x.Extensions, "default") + + *parameter = Parameter(x) + return nil +} diff --git a/openapi2/path_item.go b/openapi2/path_item.go new file mode 100644 index 000000000..95c060e7b --- /dev/null +++ b/openapi2/path_item.go @@ -0,0 +1,150 @@ +package openapi2 + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/getkin/kin-openapi/openapi3" +) + +type PathItem struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + + Delete *Operation `json:"delete,omitempty" yaml:"delete,omitempty"` + Get *Operation `json:"get,omitempty" yaml:"get,omitempty"` + Head *Operation `json:"head,omitempty" yaml:"head,omitempty"` + Options *Operation `json:"options,omitempty" yaml:"options,omitempty"` + Patch *Operation `json:"patch,omitempty" yaml:"patch,omitempty"` + Post *Operation `json:"post,omitempty" yaml:"post,omitempty"` + Put *Operation `json:"put,omitempty" yaml:"put,omitempty"` + Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"` +} + +// MarshalJSON returns the JSON encoding of PathItem. +func (pathItem PathItem) MarshalJSON() ([]byte, error) { + if ref := pathItem.Ref; ref != "" { + return json.Marshal(openapi3.Ref{Ref: ref}) + } + + m := make(map[string]interface{}, 8+len(pathItem.Extensions)) + for k, v := range pathItem.Extensions { + m[k] = v + } + if x := pathItem.Delete; x != nil { + m["delete"] = x + } + if x := pathItem.Get; x != nil { + m["get"] = x + } + if x := pathItem.Head; x != nil { + m["head"] = x + } + if x := pathItem.Options; x != nil { + m["options"] = x + } + if x := pathItem.Patch; x != nil { + m["patch"] = x + } + if x := pathItem.Post; x != nil { + m["post"] = x + } + if x := pathItem.Put; x != nil { + m["put"] = x + } + if x := pathItem.Parameters; len(x) != 0 { + m["parameters"] = x + } + return json.Marshal(m) +} + +// UnmarshalJSON sets PathItem to a copy of data. +func (pathItem *PathItem) UnmarshalJSON(data []byte) error { + type PathItemBis PathItem + var x PathItemBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "$ref") + delete(x.Extensions, "delete") + delete(x.Extensions, "get") + delete(x.Extensions, "head") + delete(x.Extensions, "options") + delete(x.Extensions, "patch") + delete(x.Extensions, "post") + delete(x.Extensions, "put") + delete(x.Extensions, "parameters") + *pathItem = PathItem(x) + return nil +} + +func (pathItem *PathItem) Operations() map[string]*Operation { + operations := make(map[string]*Operation) + if v := pathItem.Delete; v != nil { + operations[http.MethodDelete] = v + } + if v := pathItem.Get; v != nil { + operations[http.MethodGet] = v + } + if v := pathItem.Head; v != nil { + operations[http.MethodHead] = v + } + if v := pathItem.Options; v != nil { + operations[http.MethodOptions] = v + } + if v := pathItem.Patch; v != nil { + operations[http.MethodPatch] = v + } + if v := pathItem.Post; v != nil { + operations[http.MethodPost] = v + } + if v := pathItem.Put; v != nil { + operations[http.MethodPut] = v + } + return operations +} + +func (pathItem *PathItem) GetOperation(method string) *Operation { + switch method { + case http.MethodDelete: + return pathItem.Delete + case http.MethodGet: + return pathItem.Get + case http.MethodHead: + return pathItem.Head + case http.MethodOptions: + return pathItem.Options + case http.MethodPatch: + return pathItem.Patch + case http.MethodPost: + return pathItem.Post + case http.MethodPut: + return pathItem.Put + default: + panic(fmt.Errorf("unsupported HTTP method %q", method)) + } +} + +func (pathItem *PathItem) SetOperation(method string, operation *Operation) { + switch method { + case http.MethodDelete: + pathItem.Delete = operation + case http.MethodGet: + pathItem.Get = operation + case http.MethodHead: + pathItem.Head = operation + case http.MethodOptions: + pathItem.Options = operation + case http.MethodPatch: + pathItem.Patch = operation + case http.MethodPost: + pathItem.Post = operation + case http.MethodPut: + pathItem.Put = operation + default: + panic(fmt.Errorf("unsupported HTTP method %q", method)) + } +} diff --git a/openapi2/response.go b/openapi2/response.go new file mode 100644 index 000000000..bd18f882d --- /dev/null +++ b/openapi2/response.go @@ -0,0 +1,60 @@ +package openapi2 + +import ( + "encoding/json" + + "github.com/getkin/kin-openapi/openapi3" +) + +type Response struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Examples map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` +} + +// MarshalJSON returns the JSON encoding of Response. +func (response Response) MarshalJSON() ([]byte, error) { + if ref := response.Ref; ref != "" { + return json.Marshal(openapi3.Ref{Ref: ref}) + } + + m := make(map[string]interface{}, 4+len(response.Extensions)) + for k, v := range response.Extensions { + m[k] = v + } + if x := response.Description; x != "" { + m["description"] = x + } + if x := response.Schema; x != nil { + m["schema"] = x + } + if x := response.Headers; len(x) != 0 { + m["headers"] = x + } + if x := response.Examples; len(x) != 0 { + m["examples"] = x + } + return json.Marshal(m) +} + +// UnmarshalJSON sets Response to a copy of data. +func (response *Response) UnmarshalJSON(data []byte) error { + type ResponseBis Response + var x ResponseBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "$ref") + delete(x.Extensions, "description") + delete(x.Extensions, "schema") + delete(x.Extensions, "headers") + delete(x.Extensions, "examples") + *response = Response(x) + return nil +} diff --git a/openapi2/security_scheme.go b/openapi2/security_scheme.go new file mode 100644 index 000000000..5a8c278bd --- /dev/null +++ b/openapi2/security_scheme.go @@ -0,0 +1,87 @@ +package openapi2 + +import ( + "encoding/json" + + "github.com/getkin/kin-openapi/openapi3" +) + +type SecurityRequirements []map[string][]string + +type SecurityScheme struct { + Extensions map[string]interface{} `json:"-" yaml:"-"` + + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Flow string `json:"flow,omitempty" yaml:"flow,omitempty"` + AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` + TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` + Scopes map[string]string `json:"scopes,omitempty" yaml:"scopes,omitempty"` + Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` +} + +// MarshalJSON returns the JSON encoding of SecurityScheme. +func (securityScheme SecurityScheme) MarshalJSON() ([]byte, error) { + if ref := securityScheme.Ref; ref != "" { + return json.Marshal(openapi3.Ref{Ref: ref}) + } + + m := make(map[string]interface{}, 10+len(securityScheme.Extensions)) + for k, v := range securityScheme.Extensions { + m[k] = v + } + if x := securityScheme.Description; x != "" { + m["description"] = x + } + if x := securityScheme.Type; x != "" { + m["type"] = x + } + if x := securityScheme.In; x != "" { + m["in"] = x + } + if x := securityScheme.Name; x != "" { + m["name"] = x + } + if x := securityScheme.Flow; x != "" { + m["flow"] = x + } + if x := securityScheme.AuthorizationURL; x != "" { + m["authorizationUrl"] = x + } + if x := securityScheme.TokenURL; x != "" { + m["tokenUrl"] = x + } + if x := securityScheme.Scopes; len(x) != 0 { + m["scopes"] = x + } + if x := securityScheme.Tags; len(x) != 0 { + m["tags"] = x + } + return json.Marshal(m) +} + +// UnmarshalJSON sets SecurityScheme to a copy of data. +func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error { + type SecuritySchemeBis SecurityScheme + var x SecuritySchemeBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "$ref") + delete(x.Extensions, "description") + delete(x.Extensions, "type") + delete(x.Extensions, "in") + delete(x.Extensions, "name") + delete(x.Extensions, "flow") + delete(x.Extensions, "authorizationUrl") + delete(x.Extensions, "tokenUrl") + delete(x.Extensions, "scopes") + delete(x.Extensions, "tags") + *securityScheme = SecurityScheme(x) + return nil +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 989b685d5..c80e67201 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -1,7 +1,6 @@ package openapi2conv import ( - "encoding/json" "errors" "fmt" "net/url" @@ -14,15 +13,13 @@ import ( // ToV3 converts an OpenAPIv2 spec to an OpenAPIv3 spec func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { - stripNonCustomExtensions(doc2.Extensions) - doc3 := &openapi3.T{ - OpenAPI: "3.0.3", - Info: &doc2.Info, - Components: openapi3.Components{}, - Tags: doc2.Tags, - ExtensionProps: doc2.ExtensionProps, - ExternalDocs: doc2.ExternalDocs, + OpenAPI: "3.0.3", + Info: &doc2.Info, + Components: &openapi3.Components{}, + Tags: doc2.Tags, + Extensions: stripNonExtensions(doc2.Extensions), + ExternalDocs: doc2.ExternalDocs, } if host := doc2.Host; host != "" { @@ -53,7 +50,7 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { doc3.Components.Parameters = make(map[string]*openapi3.ParameterRef) doc3.Components.RequestBodies = make(map[string]*openapi3.RequestBodyRef) for k, parameter := range parameters { - v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(&doc3.Components, parameter, doc2.Consumes) + v3Parameter, v3RequestBody, v3SchemaMap, err := ToV3Parameter(doc3.Components, parameter, doc2.Consumes) switch { case err != nil: return nil, err @@ -72,7 +69,7 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { if paths := doc2.Paths; len(paths) != 0 { doc3Paths := make(map[string]*openapi3.PathItem, len(paths)) for path, pathItem := range paths { - r, err := ToV3PathItem(doc2, &doc3.Components, pathItem, doc2.Consumes) + r, err := ToV3PathItem(doc2, doc3.Components, pathItem, doc2.Consumes) if err != nil { return nil, err } @@ -119,9 +116,8 @@ func ToV3(doc2 *openapi2.T) (*openapi3.T, error) { } func ToV3PathItem(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, consumes []string) (*openapi3.PathItem, error) { - stripNonCustomExtensions(pathItem.Extensions) doc3 := &openapi3.PathItem{ - ExtensionProps: pathItem.ExtensionProps, + Extensions: stripNonExtensions(pathItem.Extensions), } for method, operation := range pathItem.Operations() { doc3Operation, err := ToV3Operation(doc2, components, pathItem, operation, consumes) @@ -150,14 +146,13 @@ func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem * if operation == nil { return nil, nil } - stripNonCustomExtensions(operation.Extensions) doc3 := &openapi3.Operation{ - OperationID: operation.OperationID, - Summary: operation.Summary, - Description: operation.Description, - Deprecated: operation.Deprecated, - Tags: operation.Tags, - ExtensionProps: operation.ExtensionProps, + OperationID: operation.OperationID, + Summary: operation.Summary, + Description: operation.Description, + Deprecated: operation.Deprecated, + Tags: operation.Tags, + Extensions: stripNonExtensions(operation.Extensions), } if v := operation.Security; v != nil { doc3Security := ToV3SecurityRequirements(*v) @@ -230,24 +225,22 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete } return &openapi3.ParameterRef{Ref: ToV3Ref(ref)}, nil, nil, nil } - stripNonCustomExtensions(parameter.Extensions) switch parameter.In { case "body": result := &openapi3.RequestBody{ - Description: parameter.Description, - Required: parameter.Required, - ExtensionProps: parameter.ExtensionProps, + Description: parameter.Description, + Required: parameter.Required, + Extensions: stripNonExtensions(parameter.Extensions), } if parameter.Name != "" { if result.Extensions == nil { - result.Extensions = make(map[string]interface{}) + result.Extensions = make(map[string]interface{}, 1) } result.Extensions["x-originalParamName"] = parameter.Name } if schemaRef := parameter.Schema; schemaRef != nil { - // Assuming JSON result.WithSchemaRef(ToV3SchemaRef(schemaRef), consumes) } return nil, &openapi3.RequestBodyRef{Value: result}, nil, nil @@ -257,39 +250,37 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete if typ == "file" { format, typ = "binary", "string" } - if parameter.ExtensionProps.Extensions == nil { - parameter.ExtensionProps.Extensions = make(map[string]interface{}) + if parameter.Extensions == nil { + parameter.Extensions = make(map[string]interface{}, 1) } - parameter.ExtensionProps.Extensions["x-formData-name"] = parameter.Name + parameter.Extensions["x-formData-name"] = parameter.Name var required []string if parameter.Required { required = []string{parameter.Name} } - schemaRef := &openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Description: parameter.Description, - Type: typ, - ExtensionProps: parameter.ExtensionProps, - Format: format, - Enum: parameter.Enum, - Min: parameter.Minimum, - Max: parameter.Maximum, - ExclusiveMin: parameter.ExclusiveMin, - ExclusiveMax: parameter.ExclusiveMax, - MinLength: parameter.MinLength, - MaxLength: parameter.MaxLength, - Default: parameter.Default, - Items: parameter.Items, - MinItems: parameter.MinItems, - MaxItems: parameter.MaxItems, - Pattern: parameter.Pattern, - AllowEmptyValue: parameter.AllowEmptyValue, - UniqueItems: parameter.UniqueItems, - MultipleOf: parameter.MultipleOf, - Required: required, - }, - } - schemaRefMap := make(map[string]*openapi3.SchemaRef) + schemaRef := &openapi3.SchemaRef{Value: &openapi3.Schema{ + Description: parameter.Description, + Type: typ, + Extensions: stripNonExtensions(parameter.Extensions), + Format: format, + Enum: parameter.Enum, + Min: parameter.Minimum, + Max: parameter.Maximum, + ExclusiveMin: parameter.ExclusiveMin, + ExclusiveMax: parameter.ExclusiveMax, + MinLength: parameter.MinLength, + MaxLength: parameter.MaxLength, + Default: parameter.Default, + Items: parameter.Items, + MinItems: parameter.MinItems, + MaxItems: parameter.MaxItems, + Pattern: parameter.Pattern, + AllowEmptyValue: parameter.AllowEmptyValue, + UniqueItems: parameter.UniqueItems, + MultipleOf: parameter.MultipleOf, + Required: required, + }} + schemaRefMap := make(map[string]*openapi3.SchemaRef, 1) schemaRefMap[parameter.Name] = schemaRef return nil, nil, schemaRefMap, nil @@ -304,11 +295,11 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete schemaRefRef = schemaRef.Ref } result := &openapi3.Parameter{ - In: parameter.In, - Name: parameter.Name, - Description: parameter.Description, - Required: required, - ExtensionProps: parameter.ExtensionProps, + In: parameter.In, + Name: parameter.Name, + Description: parameter.Description, + Required: required, + Extensions: stripNonExtensions(parameter.Extensions), Schema: ToV3SchemaRef(&openapi3.SchemaRef{Value: &openapi3.Schema{ Type: parameter.Type, Format: parameter.Format, @@ -417,10 +408,9 @@ func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.Res if ref := response.Ref; ref != "" { return &openapi3.ResponseRef{Ref: ToV3Ref(ref)}, nil } - stripNonCustomExtensions(response.Extensions) result := &openapi3.Response{ - Description: &response.Description, - ExtensionProps: response.ExtensionProps, + Description: &response.Description, + Extensions: stripNonExtensions(response.Extensions), } // Default to "application/json" if "produces" is not specified. @@ -479,19 +469,14 @@ func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { for k, v := range schema.Value.Properties { schema.Value.Properties[k] = ToV3SchemaRef(v) } - if v := schema.Value.AdditionalProperties; v != nil { - schema.Value.AdditionalProperties = ToV3SchemaRef(v) + if v := schema.Value.AdditionalProperties.Schema; v != nil { + schema.Value.AdditionalProperties.Schema = ToV3SchemaRef(v) } for i, v := range schema.Value.AllOf { schema.Value.AllOf[i] = ToV3SchemaRef(v) } if val, ok := schema.Value.Extensions["x-nullable"]; ok { - var nullable bool - - if err := json.Unmarshal(val.(json.RawMessage), &nullable); err == nil { - schema.Value.Nullable = nullable - } - + schema.Value.Nullable, _ = val.(bool) delete(schema.Value.Extensions, "x-nullable") } @@ -539,10 +524,9 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu if securityScheme == nil { return nil, nil } - stripNonCustomExtensions(securityScheme.Extensions) result := &openapi3.SecurityScheme{ - Description: securityScheme.Description, - ExtensionProps: securityScheme.ExtensionProps, + Description: securityScheme.Description, + Extensions: stripNonExtensions(securityScheme.Extensions), } switch securityScheme.Type { case "basic": @@ -586,21 +570,20 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu // FromV3 converts an OpenAPIv3 spec to an OpenAPIv2 spec func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { - doc2Responses, err := FromV3Responses(doc3.Components.Responses, &doc3.Components) + doc2Responses, err := FromV3Responses(doc3.Components.Responses, doc3.Components) if err != nil { return nil, err } - stripNonCustomExtensions(doc3.Extensions) - schemas, parameters := FromV3Schemas(doc3.Components.Schemas, &doc3.Components) + schemas, parameters := FromV3Schemas(doc3.Components.Schemas, doc3.Components) doc2 := &openapi2.T{ - Swagger: "2.0", - Info: *doc3.Info, - Definitions: schemas, - Parameters: parameters, - Responses: doc2Responses, - Tags: doc3.Tags, - ExtensionProps: doc3.ExtensionProps, - ExternalDocs: doc3.ExternalDocs, + Swagger: "2.0", + Info: *doc3.Info, + Definitions: schemas, + Parameters: parameters, + Responses: doc2Responses, + Tags: doc3.Tags, + Extensions: stripNonExtensions(doc3.Extensions), + ExternalDocs: doc3.ExternalDocs, } isHTTPS := false @@ -633,8 +616,7 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { continue } doc2.AddOperation(path, "GET", nil) - stripNonCustomExtensions(pathItem.Extensions) - addPathExtensions(doc2, path, pathItem.ExtensionProps) + addPathExtensions(doc2, path, stripNonExtensions(pathItem.Extensions)) for method, operation := range pathItem.Operations() { if operation == nil { continue @@ -647,7 +629,7 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { } params := openapi2.Parameters{} for _, param := range pathItem.Parameters { - p, err := FromV3Parameter(param, &doc3.Components) + p, err := FromV3Parameter(param, doc3.Components) if err != nil { return nil, err } @@ -658,13 +640,13 @@ func FromV3(doc3 *openapi3.T) (*openapi2.T, error) { } for name, param := range doc3.Components.Parameters { - if doc2.Parameters[name], err = FromV3Parameter(param, &doc3.Components); err != nil { + if doc2.Parameters[name], err = FromV3Parameter(param, doc3.Components); err != nil { return nil, err } } for name, requestBodyRef := range doc3.Components.RequestBodies { - bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, requestBodyRef, &doc3.Components) + bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, requestBodyRef, doc3.Components) if err != nil { return nil, err } @@ -733,7 +715,7 @@ func fromV3RequestBodies(name string, requestBodyRef *openapi3.RequestBodyRef, c paramName := name if originalName, ok := requestBodyRef.Value.Extensions["x-originalParamName"]; ok { - json.Unmarshal(originalName.(json.RawMessage), ¶mName) + paramName = originalName.(string) } var r *openapi2.Parameter @@ -786,11 +768,11 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components required := false value, _ := schema.Value.Extensions["x-formData-name"] - var originalName string - json.Unmarshal(value.(json.RawMessage), &originalName) + originalName, _ := value.(string) for _, prop := range schema.Value.Required { if originalName == prop { required = true + break } } return nil, &openapi2.Parameter{ @@ -812,7 +794,7 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components AllowEmptyValue: schema.Value.AllowEmptyValue, UniqueItems: schema.Value.UniqueItems, MultipleOf: schema.Value.MultipleOf, - ExtensionProps: schema.Value.ExtensionProps, + Extensions: stripNonExtensions(schema.Value.Extensions), Required: required, } } @@ -828,8 +810,8 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components for _, key := range keys { schema.Value.Properties[key], _ = FromV3SchemaRef(schema.Value.Properties[key], components) } - if v := schema.Value.AdditionalProperties; v != nil { - schema.Value.AdditionalProperties, _ = FromV3SchemaRef(v, components) + if v := schema.Value.AdditionalProperties.Schema; v != nil { + schema.Value.AdditionalProperties.Schema, _ = FromV3SchemaRef(v, components) } for i, v := range schema.Value.AllOf { schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components) @@ -854,9 +836,8 @@ func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) open } func FromV3PathItem(doc3 *openapi3.T, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) { - stripNonCustomExtensions(pathItem.Extensions) result := &openapi2.PathItem{ - ExtensionProps: pathItem.ExtensionProps, + Extensions: stripNonExtensions(pathItem.Extensions), } for method, operation := range pathItem.Operations() { r, err := FromV3Operation(doc3, operation) @@ -866,7 +847,7 @@ func FromV3PathItem(doc3 *openapi3.T, pathItem *openapi3.PathItem) (*openapi2.Pa result.SetOperation(method, r) } for _, parameter := range pathItem.Parameters { - p, err := FromV3Parameter(parameter, &doc3.Components) + p, err := FromV3Parameter(parameter, doc3.Components) if err != nil { return nil, err } @@ -910,23 +891,23 @@ func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameter } } parameter := &openapi2.Parameter{ - Name: propName, - Description: val.Description, - Type: typ, - In: "formData", - ExtensionProps: val.ExtensionProps, - Enum: val.Enum, - ExclusiveMin: val.ExclusiveMin, - ExclusiveMax: val.ExclusiveMax, - MinLength: val.MinLength, - MaxLength: val.MaxLength, - Default: val.Default, - Items: val.Items, - MinItems: val.MinItems, - MaxItems: val.MaxItems, - Maximum: val.Max, - Minimum: val.Min, - Pattern: val.Pattern, + Name: propName, + Description: val.Description, + Type: typ, + In: "formData", + Extensions: stripNonExtensions(val.Extensions), + Enum: val.Enum, + ExclusiveMin: val.ExclusiveMin, + ExclusiveMax: val.ExclusiveMax, + MinLength: val.MinLength, + MaxLength: val.MaxLength, + Default: val.Default, + Items: val.Items, + MinItems: val.MinItems, + MaxItems: val.MaxItems, + Maximum: val.Max, + Minimum: val.Min, + Pattern: val.Pattern, // CollectionFormat: val.CollectionFormat, // Format: val.Format, AllowEmptyValue: val.AllowEmptyValue, @@ -943,21 +924,20 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2 if operation == nil { return nil, nil } - stripNonCustomExtensions(operation.Extensions) result := &openapi2.Operation{ - OperationID: operation.OperationID, - Summary: operation.Summary, - Description: operation.Description, - Deprecated: operation.Deprecated, - Tags: operation.Tags, - ExtensionProps: operation.ExtensionProps, + OperationID: operation.OperationID, + Summary: operation.Summary, + Description: operation.Description, + Deprecated: operation.Deprecated, + Tags: operation.Tags, + Extensions: stripNonExtensions(operation.Extensions), } if v := operation.Security; v != nil { resultSecurity := FromV3SecurityRequirements(*v) result.Security = &resultSecurity } for _, parameter := range operation.Parameters { - r, err := FromV3Parameter(parameter, &doc3.Components) + r, err := FromV3Parameter(parameter, doc3.Components) if err != nil { return nil, err } @@ -970,7 +950,7 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2 return nil, errors.New("could not find a name for request body") } - bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, v, &doc3.Components) + bodyOrRefParameters, formDataParameters, consumes, err := fromV3RequestBodies(name, v, doc3.Components) if err != nil { return nil, err } @@ -991,7 +971,7 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2 sort.Sort(result.Parameters) if responses := operation.Responses; responses != nil { - resultResponses, err := FromV3Responses(responses, &doc3.Components) + resultResponses, err := FromV3Responses(responses, doc3.Components) if err != nil { return nil, err } @@ -1003,13 +983,12 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2 func FromV3RequestBody(name string, requestBodyRef *openapi3.RequestBodyRef, mediaType *openapi3.MediaType, components *openapi3.Components) (*openapi2.Parameter, error) { requestBody := requestBodyRef.Value - stripNonCustomExtensions(requestBody.Extensions) result := &openapi2.Parameter{ - In: "body", - Name: name, - Description: requestBody.Description, - Required: requestBody.Required, - ExtensionProps: requestBody.ExtensionProps, + In: "body", + Name: name, + Description: requestBody.Description, + Required: requestBody.Required, + Extensions: stripNonExtensions(requestBody.Extensions), } if mediaType != nil { @@ -1026,13 +1005,12 @@ func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components if parameter == nil { return nil, nil } - stripNonCustomExtensions(parameter.Extensions) result := &openapi2.Parameter{ - Description: parameter.Description, - In: parameter.In, - Name: parameter.Name, - Required: parameter.Required, - ExtensionProps: parameter.ExtensionProps, + Description: parameter.Description, + In: parameter.In, + Name: parameter.Name, + Required: parameter.Required, + Extensions: stripNonExtensions(parameter.Extensions), } if schemaRef := parameter.Schema; schemaRef != nil { schemaRef, _ = FromV3SchemaRef(schemaRef, components) @@ -1088,10 +1066,9 @@ func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) if desc := response.Description; desc != nil { description = *desc } - stripNonCustomExtensions(response.Extensions) result := &openapi2.Response{ - Description: description, - ExtensionProps: response.ExtensionProps, + Description: description, + Extensions: stripNonExtensions(response.Extensions), } if content := response.Content; content != nil { if ct := content["application/json"]; ct != nil { @@ -1127,11 +1104,10 @@ func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*o if securityScheme == nil { return nil, nil } - stripNonCustomExtensions(securityScheme.Extensions) result := &openapi2.SecurityScheme{ - Ref: FromV3Ref(ref.Ref), - Description: securityScheme.Description, - ExtensionProps: securityScheme.ExtensionProps, + Ref: FromV3Ref(ref.Ref), + Description: securityScheme.Description, + Extensions: stripNonExtensions(securityScheme.Extensions), } switch securityScheme.Type { case "http": @@ -1195,15 +1171,17 @@ var attemptedBodyParameterNames = []string{ "requestBody", } -func stripNonCustomExtensions(extensions map[string]interface{}) { +// stripNonExtensions removes invalid extensions: those not prefixed by "x-" and returns them +func stripNonExtensions(extensions map[string]interface{}) map[string]interface{} { for extName := range extensions { if !strings.HasPrefix(extName, "x-") { delete(extensions, extName) } } + return extensions } -func addPathExtensions(doc2 *openapi2.T, path string, extensionProps openapi3.ExtensionProps) { +func addPathExtensions(doc2 *openapi2.T, path string, extensions map[string]interface{}) { if doc2.Paths == nil { doc2.Paths = make(map[string]*openapi2.PathItem) } @@ -1212,5 +1190,5 @@ func addPathExtensions(doc2 *openapi2.T, path string, extensionProps openapi3.Ex pathItem = &openapi2.PathItem{} doc2.Paths[path] = pathItem } - pathItem.ExtensionProps = extensionProps + pathItem.Extensions = extensions } diff --git a/openapi3/components.go b/openapi3/components.go index 8abc3fe9c..0981e8bfe 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -2,17 +2,16 @@ package openapi3 import ( "context" + "encoding/json" "fmt" "regexp" "sort" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Components is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#components-object type Components struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` @@ -30,13 +29,60 @@ func NewComponents() Components { } // MarshalJSON returns the JSON encoding of Components. -func (components *Components) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(components) +func (components Components) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 9+len(components.Extensions)) + for k, v := range components.Extensions { + m[k] = v + } + if x := components.Schemas; len(x) != 0 { + m["schemas"] = x + } + if x := components.Parameters; len(x) != 0 { + m["parameters"] = x + } + if x := components.Headers; len(x) != 0 { + m["headers"] = x + } + if x := components.RequestBodies; len(x) != 0 { + m["requestBodies"] = x + } + if x := components.Responses; len(x) != 0 { + m["responses"] = x + } + if x := components.SecuritySchemes; len(x) != 0 { + m["securitySchemes"] = x + } + if x := components.Examples; len(x) != 0 { + m["examples"] = x + } + if x := components.Links; len(x) != 0 { + m["links"] = x + } + if x := components.Callbacks; len(x) != 0 { + m["callbacks"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Components to a copy of data. func (components *Components) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, components) + type ComponentsBis Components + var x ComponentsBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "schemas") + delete(x.Extensions, "parameters") + delete(x.Extensions, "headers") + delete(x.Extensions, "requestBodies") + delete(x.Extensions, "responses") + delete(x.Extensions, "securitySchemes") + delete(x.Extensions, "examples") + delete(x.Extensions, "links") + delete(x.Extensions, "callbacks") + *components = Components(x) + return nil } // Validate returns an error if Components does not comply with the OpenAPI spec. @@ -178,7 +224,7 @@ func (components *Components) Validate(ctx context.Context, opts ...ValidationOp } } - return + return validateExtensions(ctx, components.Extensions) } const identifierPattern = `^[a-zA-Z0-9._-]+$` diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index 8ab344a84..8b6b813f2 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -2,32 +2,48 @@ package openapi3 import ( "context" - - "github.com/getkin/kin-openapi/jsoninfo" + "encoding/json" ) // Discriminator is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object type Discriminator struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` - PropertyName string `json:"propertyName" yaml:"propertyName"` + PropertyName string `json:"propertyName" yaml:"propertyName"` // required Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` } // MarshalJSON returns the JSON encoding of Discriminator. -func (discriminator *Discriminator) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(discriminator) +func (discriminator Discriminator) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 2+len(discriminator.Extensions)) + for k, v := range discriminator.Extensions { + m[k] = v + } + m["propertyName"] = discriminator.PropertyName + if x := discriminator.Mapping; len(x) != 0 { + m["mapping"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Discriminator to a copy of data. func (discriminator *Discriminator) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, discriminator) + type DiscriminatorBis Discriminator + var x DiscriminatorBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "propertyName") + delete(x.Extensions, "mapping") + *discriminator = Discriminator(x) + return nil } // Validate returns an error if Discriminator does not comply with the OpenAPI spec. func (discriminator *Discriminator) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) - return nil + return validateExtensions(ctx, discriminator.Extensions) } diff --git a/openapi3/encoding.go b/openapi3/encoding.go index 003833e16..dc2e54438 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -2,16 +2,15 @@ package openapi3 import ( "context" + "encoding/json" "fmt" "sort" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Encoding is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encoding-object type Encoding struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -41,13 +40,44 @@ func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding { } // MarshalJSON returns the JSON encoding of Encoding. -func (encoding *Encoding) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(encoding) +func (encoding Encoding) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 5+len(encoding.Extensions)) + for k, v := range encoding.Extensions { + m[k] = v + } + if x := encoding.ContentType; x != "" { + m["contentType"] = x + } + if x := encoding.Headers; len(x) != 0 { + m["headers"] = x + } + if x := encoding.Style; x != "" { + m["style"] = x + } + if x := encoding.Explode; x != nil { + m["explode"] = x + } + if x := encoding.AllowReserved; x { + m["allowReserved"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Encoding to a copy of data. func (encoding *Encoding) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, encoding) + type EncodingBis Encoding + var x EncodingBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "contentType") + delete(x.Extensions, "headers") + delete(x.Extensions, "style") + delete(x.Extensions, "explode") + delete(x.Extensions, "allowReserved") + *encoding = Encoding(x) + return nil } // SerializationMethod returns a serialization method of request body. @@ -102,5 +132,5 @@ func (encoding *Encoding) Validate(ctx context.Context, opts ...ValidationOption return fmt.Errorf("serialization method with style=%q and explode=%v is not supported by media type", sm.Style, sm.Explode) } - return nil + return validateExtensions(ctx, encoding.Extensions) } diff --git a/openapi3/example.go b/openapi3/example.go index f4cbfc074..04338beee 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -2,12 +2,11 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type Examples map[string]*ExampleRef @@ -30,7 +29,7 @@ func (e Examples) JSONLookup(token string) (interface{}, error) { // Example is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object type Example struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -39,24 +38,49 @@ type Example struct { } func NewExample(value interface{}) *Example { - return &Example{ - Value: value, - } + return &Example{Value: value} } // MarshalJSON returns the JSON encoding of Example. -func (example *Example) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(example) +func (example Example) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(example.Extensions)) + for k, v := range example.Extensions { + m[k] = v + } + if x := example.Summary; x != "" { + m["summary"] = x + } + if x := example.Description; x != "" { + m["description"] = x + } + if x := example.Value; x != nil { + m["value"] = x + } + if x := example.ExternalValue; x != "" { + m["externalValue"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Example to a copy of data. func (example *Example) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, example) + type ExampleBis Example + var x ExampleBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "summary") + delete(x.Extensions, "description") + delete(x.Extensions, "value") + delete(x.Extensions, "externalValue") + *example = Example(x) + return nil } // Validate returns an error if Example does not comply with the OpenAPI spec. func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if example.Value != nil && example.ExternalValue != "" { return errors.New("value and externalValue are mutually exclusive") @@ -65,5 +89,5 @@ func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) return errors.New("no value or externalValue field") } - return nil + return validateExtensions(ctx, example.Extensions) } diff --git a/openapi3/example_validation_test.go b/openapi3/example_validation_test.go index 6ce7c0a48..de8954828 100644 --- a/openapi3/example_validation_test.go +++ b/openapi3/example_validation_test.go @@ -241,6 +241,7 @@ paths: spec.WriteString(tc.parametersExample) spec.WriteString(` requestBody: + required: true content: application/json: schema: @@ -249,7 +250,6 @@ paths: spec.WriteString(tc.mediaTypeRequestExample) spec.WriteString(` description: Created user object - required: true responses: '204': description: "success" @@ -262,11 +262,12 @@ paths: /readWriteOnly: post: requestBody: + required: true content: application/json: schema: $ref: "#/components/schemas/ReadWriteOnlyData" - required: true`) +`) spec.WriteString(tc.readWriteOnlyMediaTypeRequestExample) spec.WriteString(` responses: diff --git a/openapi3/extension.go b/openapi3/extension.go index f6b7ef9bb..c29959091 100644 --- a/openapi3/extension.go +++ b/openapi3/extension.go @@ -1,38 +1,24 @@ package openapi3 import ( - "github.com/getkin/kin-openapi/jsoninfo" + "context" + "fmt" + "sort" + "strings" ) -// ExtensionProps provides support for OpenAPI extensions. -// It reads/writes all properties that begin with "x-". -type ExtensionProps struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` -} - -// Assert that the type implements the interface -var _ jsoninfo.StrictStruct = &ExtensionProps{} - -// EncodeWith will be invoked by package "jsoninfo" -func (props *ExtensionProps) EncodeWith(encoder *jsoninfo.ObjectEncoder, value interface{}) error { - for k, v := range props.Extensions { - if err := encoder.EncodeExtension(k, v); err != nil { - return err +func validateExtensions(ctx context.Context, extensions map[string]interface{}) error { // FIXME: newtype + Validate(...) + var unknowns []string + for k := range extensions { + if !strings.HasPrefix(k, "x-") { + unknowns = append(unknowns, k) } } - return encoder.EncodeStructFieldsAndExtensions(value) -} -// DecodeWith will be invoked by package "jsoninfo" -func (props *ExtensionProps) DecodeWith(decoder *jsoninfo.ObjectDecoder, value interface{}) error { - if err := decoder.DecodeStructFieldsAndExtensions(value); err != nil { - return err - } - source := decoder.DecodeExtensionMap() - result := make(map[string]interface{}, len(source)) - for k, v := range source { - result[k] = v + if len(unknowns) != 0 { + sort.Strings(unknowns) + return fmt.Errorf("extra sibling fields: %+v", unknowns) } - props.Extensions = result + return nil } diff --git a/openapi3/extension_test.go b/openapi3/extension_test.go deleted file mode 100644 index a99537892..000000000 --- a/openapi3/extension_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package openapi3 - -import ( - "encoding/json" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/getkin/kin-openapi/jsoninfo" -) - -func ExampleExtensionProps_DecodeWith() { - loader := NewLoader() - loader.IsExternalRefsAllowed = true - spec, err := loader.LoadFromFile("testdata/testref.openapi.json") - if err != nil { - panic(err) - } - - dec, err := jsoninfo.NewObjectDecoder(spec.Info.Extensions["x-my-extension"].(json.RawMessage)) - if err != nil { - panic(err) - } - var value struct { - Key int `json:"k"` - } - if err = spec.Info.DecodeWith(dec, &value); err != nil { - panic(err) - } - fmt.Println(value.Key) - // Output: 42 -} - -func TestExtensionProps_EncodeWith(t *testing.T) { - t.Run("successfully encoded", func(t *testing.T) { - encoder := jsoninfo.NewObjectEncoder() - var extensionProps = ExtensionProps{ - Extensions: map[string]interface{}{ - "field1": "value1", - }, - } - - var value = struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` - }{} - - err := extensionProps.EncodeWith(encoder, &value) - require.NoError(t, err) - }) -} - -func TestExtensionProps_DecodeWith(t *testing.T) { - data := []byte(` - { - "field1": "value1", - "field2": "value2" - } -`) - t.Run("successfully decode all the fields", func(t *testing.T) { - decoder, err := jsoninfo.NewObjectDecoder(data) - require.NoError(t, err) - var extensionProps = &ExtensionProps{ - Extensions: map[string]interface{}{ - "field1": "value1", - "field2": "value1", - }, - } - - var value = struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` - }{} - - err = extensionProps.DecodeWith(decoder, &value) - require.NoError(t, err) - require.Equal(t, 0, len(extensionProps.Extensions)) - require.Equal(t, "value1", value.Field1) - require.Equal(t, "value2", value.Field2) - }) - - t.Run("successfully decode some of the fields", func(t *testing.T) { - decoder, err := jsoninfo.NewObjectDecoder(data) - require.NoError(t, err) - var extensionProps = &ExtensionProps{ - Extensions: map[string]interface{}{ - "field1": "value1", - "field2": "value2", - }, - } - - var value = &struct { - Field1 string `json:"field1"` - }{} - - err = extensionProps.DecodeWith(decoder, value) - require.NoError(t, err) - require.Equal(t, 1, len(extensionProps.Extensions)) - require.Equal(t, "value1", value.Field1) - }) - - t.Run("successfully decode none of the fields", func(t *testing.T) { - decoder, err := jsoninfo.NewObjectDecoder(data) - require.NoError(t, err) - - var extensionProps = &ExtensionProps{ - Extensions: map[string]interface{}{ - "field1": "value1", - "field2": "value2", - }, - } - - var value = struct { - Field3 string `json:"field3"` - Field4 string `json:"field4"` - }{} - - err = extensionProps.DecodeWith(decoder, &value) - require.NoError(t, err) - require.Equal(t, 2, len(extensionProps.Extensions)) - require.Empty(t, value.Field3) - require.Empty(t, value.Field4) - }) -} diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index 65ec2e88f..276a36cce 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -2,35 +2,53 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "net/url" - - "github.com/getkin/kin-openapi/jsoninfo" ) // ExternalDocs is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object type ExternalDocs struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Description string `json:"description,omitempty" yaml:"description,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` } // MarshalJSON returns the JSON encoding of ExternalDocs. -func (e *ExternalDocs) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(e) +func (e ExternalDocs) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 2+len(e.Extensions)) + for k, v := range e.Extensions { + m[k] = v + } + if x := e.Description; x != "" { + m["description"] = x + } + if x := e.URL; x != "" { + m["url"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets ExternalDocs to a copy of data. func (e *ExternalDocs) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, e) + type ExternalDocsBis ExternalDocs + var x ExternalDocsBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "description") + delete(x.Extensions, "url") + *e = ExternalDocs(x) + return nil } // Validate returns an error if ExternalDocs does not comply with the OpenAPI spec. func (e *ExternalDocs) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if e.URL == "" { return errors.New("url is required") @@ -38,5 +56,6 @@ func (e *ExternalDocs) Validate(ctx context.Context, opts ...ValidationOption) e if _, err := url.Parse(e.URL); err != nil { return fmt.Errorf("url is incorrect: %w", err) } - return nil + + return validateExtensions(ctx, e.Extensions) } diff --git a/openapi3/header.go b/openapi3/header.go index 454fad51e..8bce69f2e 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -6,8 +6,6 @@ import ( "fmt" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type Headers map[string]*HeaderRef @@ -35,9 +33,19 @@ type Header struct { var _ jsonpointer.JSONPointable = (*Header)(nil) +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable +func (header Header) JSONLookup(token string) (interface{}, error) { + return header.Parameter.JSONLookup(token) +} + +// MarshalJSON returns the JSON encoding of Header. +func (header Header) MarshalJSON() ([]byte, error) { + return header.Parameter.MarshalJSON() +} + // UnmarshalJSON sets Header to a copy of data. func (header *Header) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, header) + return header.Parameter.UnmarshalJSON(data) } // SerializationMethod returns a header's serialization method. @@ -93,43 +101,3 @@ func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) er } return nil } - -// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (header Header) JSONLookup(token string) (interface{}, error) { - switch token { - case "schema": - if header.Schema != nil { - if header.Schema.Ref != "" { - return &Ref{Ref: header.Schema.Ref}, nil - } - return header.Schema.Value, nil - } - case "name": - return header.Name, nil - case "in": - return header.In, nil - case "description": - return header.Description, nil - case "style": - return header.Style, nil - case "explode": - return header.Explode, nil - case "allowEmptyValue": - return header.AllowEmptyValue, nil - case "allowReserved": - return header.AllowReserved, nil - case "deprecated": - return header.Deprecated, nil - case "required": - return header.Required, nil - case "example": - return header.Example, nil - case "examples": - return header.Examples, nil - case "content": - return header.Content, nil - } - - v, _, err := jsonpointer.GetForToken(header.ExtensionProps, token) - return v, err -} diff --git a/openapi3/info.go b/openapi3/info.go index 72076095e..381047fca 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -2,15 +2,14 @@ package openapi3 import ( "context" + "encoding/json" "errors" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Info is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#info-object type Info struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -21,13 +20,44 @@ type Info struct { } // MarshalJSON returns the JSON encoding of Info. -func (info *Info) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(info) +func (info Info) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 6+len(info.Extensions)) + for k, v := range info.Extensions { + m[k] = v + } + m["title"] = info.Title + if x := info.Description; x != "" { + m["description"] = x + } + if x := info.TermsOfService; x != "" { + m["termsOfService"] = x + } + if x := info.Contact; x != nil { + m["contact"] = x + } + if x := info.License; x != nil { + m["license"] = x + } + m["version"] = info.Version + return json.Marshal(m) } // UnmarshalJSON sets Info to a copy of data. func (info *Info) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, info) + type InfoBis Info + var x InfoBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "title") + delete(x.Extensions, "description") + delete(x.Extensions, "termsOfService") + delete(x.Extensions, "contact") + delete(x.Extensions, "license") + delete(x.Extensions, "version") + *info = Info(x) + return nil } // Validate returns an error if Info does not comply with the OpenAPI spec. @@ -54,13 +84,13 @@ func (info *Info) Validate(ctx context.Context, opts ...ValidationOption) error return errors.New("value of title must be a non-empty string") } - return nil + return validateExtensions(ctx, info.Extensions) } // Contact is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contact-object type Contact struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -68,47 +98,88 @@ type Contact struct { } // MarshalJSON returns the JSON encoding of Contact. -func (contact *Contact) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(contact) +func (contact Contact) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 3+len(contact.Extensions)) + for k, v := range contact.Extensions { + m[k] = v + } + if x := contact.Name; x != "" { + m["name"] = x + } + if x := contact.URL; x != "" { + m["url"] = x + } + if x := contact.Email; x != "" { + m["email"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Contact to a copy of data. func (contact *Contact) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, contact) + type ContactBis Contact + var x ContactBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "name") + delete(x.Extensions, "url") + delete(x.Extensions, "email") + *contact = Contact(x) + return nil } // Validate returns an error if Contact does not comply with the OpenAPI spec. func (contact *Contact) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) - return nil + return validateExtensions(ctx, contact.Extensions) } // License is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#license-object type License struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` } // MarshalJSON returns the JSON encoding of License. -func (license *License) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(license) +func (license License) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 2+len(license.Extensions)) + for k, v := range license.Extensions { + m[k] = v + } + m["name"] = license.Name + if x := license.URL; x != "" { + m["url"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets License to a copy of data. func (license *License) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, license) + type LicenseBis License + var x LicenseBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "name") + delete(x.Extensions, "url") + *license = License(x) + return nil } // Validate returns an error if License does not comply with the OpenAPI spec. func (license *License) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if license.Name == "" { return errors.New("value of license name must be a non-empty string") } - return nil + + return validateExtensions(ctx, license.Extensions) } diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index dcec43b40..acb83cd0c 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -55,11 +55,16 @@ func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver, par } name := refNameResolver(s.Ref) - if _, ok := doc.Components.Schemas[name]; ok { - s.Ref = "#/components/schemas/" + name - return true + if doc.Components != nil { + if _, ok := doc.Components.Schemas[name]; ok { + s.Ref = "#/components/schemas/" + name + return true + } } + if doc.Components == nil { + doc.Components = &Components{} + } if doc.Components.Schemas == nil { doc.Components.Schemas = make(Schemas) } @@ -220,7 +225,7 @@ func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver, parentIsEx doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal) } } - for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties, s.Items} { + for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties.Schema, s.Items} { isExternal := doc.addSchemaToSpec(ref, refNameResolver, parentIsExternal) if ref != nil { doc.derefSchema(ref.Value, refNameResolver, isExternal || parentIsExternal) @@ -335,44 +340,45 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri refNameResolver = DefaultRefNameResolver } - // Handle components section - names := schemaNames(doc.Components.Schemas) - for _, name := range names { - schema := doc.Components.Schemas[name] - isExternal := doc.addSchemaToSpec(schema, refNameResolver, false) - if schema != nil { - schema.Ref = "" // always dereference the top level - doc.derefSchema(schema.Value, refNameResolver, isExternal) + if components := doc.Components; components != nil { + names := schemaNames(components.Schemas) + for _, name := range names { + schema := components.Schemas[name] + isExternal := doc.addSchemaToSpec(schema, refNameResolver, false) + if schema != nil { + schema.Ref = "" // always dereference the top level + doc.derefSchema(schema.Value, refNameResolver, isExternal) + } } - } - names = parametersMapNames(doc.Components.Parameters) - for _, name := range names { - p := doc.Components.Parameters[name] - isExternal := doc.addParameterToSpec(p, refNameResolver, false) - if p != nil && p.Value != nil { - p.Ref = "" // always dereference the top level - doc.derefParameter(*p.Value, refNameResolver, isExternal) + names = parametersMapNames(components.Parameters) + for _, name := range names { + p := components.Parameters[name] + isExternal := doc.addParameterToSpec(p, refNameResolver, false) + if p != nil && p.Value != nil { + p.Ref = "" // always dereference the top level + doc.derefParameter(*p.Value, refNameResolver, isExternal) + } } - } - doc.derefHeaders(doc.Components.Headers, refNameResolver, false) - for _, req := range doc.Components.RequestBodies { - isExternal := doc.addRequestBodyToSpec(req, refNameResolver, false) - if req != nil && req.Value != nil { - req.Ref = "" // always dereference the top level - doc.derefRequestBody(*req.Value, refNameResolver, isExternal) + doc.derefHeaders(components.Headers, refNameResolver, false) + for _, req := range components.RequestBodies { + isExternal := doc.addRequestBodyToSpec(req, refNameResolver, false) + if req != nil && req.Value != nil { + req.Ref = "" // always dereference the top level + doc.derefRequestBody(*req.Value, refNameResolver, isExternal) + } } - } - doc.derefResponses(doc.Components.Responses, refNameResolver, false) - for _, ss := range doc.Components.SecuritySchemes { - doc.addSecuritySchemeToSpec(ss, refNameResolver, false) - } - doc.derefExamples(doc.Components.Examples, refNameResolver, false) - doc.derefLinks(doc.Components.Links, refNameResolver, false) - for _, cb := range doc.Components.Callbacks { - isExternal := doc.addCallbackToSpec(cb, refNameResolver, false) - if cb != nil && cb.Value != nil { - cb.Ref = "" // always dereference the top level - doc.derefPaths(*cb.Value, refNameResolver, isExternal) + doc.derefResponses(components.Responses, refNameResolver, false) + for _, ss := range components.SecuritySchemes { + doc.addSecuritySchemeToSpec(ss, refNameResolver, false) + } + doc.derefExamples(components.Examples, refNameResolver, false) + doc.derefLinks(components.Links, refNameResolver, false) + for _, cb := range components.Callbacks { + isExternal := doc.addCallbackToSpec(cb, refNameResolver, false) + if cb != nil && cb.Value != nil { + cb.Ref = "" // always dereference the top level + doc.derefPaths(*cb.Value, refNameResolver, isExternal) + } } } diff --git a/openapi3/issue341_test.go b/openapi3/issue341_test.go index 93364d0e8..15ea9d48c 100644 --- a/openapi3/issue341_test.go +++ b/openapi3/issue341_test.go @@ -20,7 +20,7 @@ func TestIssue341(t *testing.T) { bs, err := doc.MarshalJSON() require.NoError(t, err) - require.Equal(t, []byte(`{"components":{},"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"get":{"responses":{"200":{"$ref":"#/components/responses/testpath_200_response"}}}}}}`), bs) + require.JSONEq(t, `{"info":{"title":"test file","version":"n/a"},"openapi":"3.0.0","paths":{"/testpath":{"get":{"responses":{"200":{"$ref":"#/components/responses/testpath_200_response"}}}}}}`, string(bs)) require.Equal(t, "string", doc.Paths["/testpath"].Get.Responses["200"].Value.Content["application/json"].Schema.Value.Type) } diff --git a/openapi3/issue376_test.go b/openapi3/issue376_test.go index 22aa7fb40..825f1d1ac 100644 --- a/openapi3/issue376_test.go +++ b/openapi3/issue376_test.go @@ -1,6 +1,7 @@ package openapi3 import ( + "context" "fmt" "testing" @@ -42,15 +43,42 @@ info: require.Equal(t, "string", doc.Components.Schemas["schema2"].Value.Properties["prop"].Value.Type) } +func TestExclusiveValuesOfValuesAdditionalProperties(t *testing.T) { + schema := &Schema{ + AdditionalProperties: AdditionalProperties{ + Has: BoolPtr(false), + Schema: NewSchemaRef("", &Schema{}), + }, + } + err := schema.Validate(context.Background()) + require.ErrorContains(t, err, ` to both `) + + schema = &Schema{ + AdditionalProperties: AdditionalProperties{ + Has: BoolPtr(false), + }, + } + err = schema.Validate(context.Background()) + require.NoError(t, err) + + schema = &Schema{ + AdditionalProperties: AdditionalProperties{ + Schema: NewSchemaRef("", &Schema{}), + }, + } + err = schema.Validate(context.Background()) + require.NoError(t, err) +} + func TestMultijsonTagSerialization(t *testing.T) { - spec := []byte(` + specYAML := []byte(` openapi: 3.0.0 components: schemas: unset: type: number - #empty-object: - # TODO additionalProperties: {} + empty-object: + additionalProperties: {} object: additionalProperties: {type: string} boolean: @@ -61,42 +89,77 @@ info: version: 1.2.3.4 `) - loader := NewLoader() - - doc, err := loader.LoadFromData(spec) - require.NoError(t, err) - - err = doc.Validate(loader.Context) - require.NoError(t, err) - - for propName, propSchema := range doc.Components.Schemas { - ap := propSchema.Value.AdditionalProperties - apa := propSchema.Value.AdditionalPropertiesAllowed - - encoded, err := propSchema.MarshalJSON() - require.NoError(t, err) - require.Equal(t, string(encoded), map[string]string{ - "unset": `{"type":"number"}`, - // TODO: "empty-object":`{"additionalProperties":{}}`, - "object": `{"additionalProperties":{"type":"string"}}`, - "boolean": `{"additionalProperties":false}`, - }[propName]) - - if propName == "unset" { - require.True(t, ap == nil && apa == nil) - continue - } - - apStr := "" - if ap != nil { - apStr = fmt.Sprintf("{Ref:%s Value.Type:%v}", (*ap).Ref, (*ap).Value.Type) - } - apaStr := "" - if apa != nil { - apaStr = fmt.Sprintf("%v", *apa) - } - - require.Truef(t, (ap != nil && apa == nil) || (ap == nil && apa != nil), - "%s: isnil(%s) xor isnil(%s)", propName, apaStr, apStr) + specJSON := []byte(`{ + "openapi": "3.0.0", + "components": { + "schemas": { + "unset": { + "type": "number" + }, + "empty-object": { + "additionalProperties": { + } + }, + "object": { + "additionalProperties": { + "type": "string" + } + }, + "boolean": { + "additionalProperties": false + } + } + }, + "paths": { + }, + "info": { + "title": "An API", + "version": "1.2.3.4" + } +}`) + + for i, spec := range [][]byte{specJSON, specYAML} { + t.Run(fmt.Sprintf("spec%02d", i), func(t *testing.T) { + loader := NewLoader() + + doc, err := loader.LoadFromData(spec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + for propName, propSchema := range doc.Components.Schemas { + t.Run(propName, func(t *testing.T) { + ap := propSchema.Value.AdditionalProperties.Schema + apa := propSchema.Value.AdditionalProperties.Has + + apStr := "" + if ap != nil { + apStr = fmt.Sprintf("{Ref:%s Value.Type:%v}", (*ap).Ref, (*ap).Value.Type) + } + apaStr := "" + if apa != nil { + apaStr = fmt.Sprintf("%v", *apa) + } + + encoded, err := propSchema.MarshalJSON() + require.NoError(t, err) + require.Equal(t, map[string]string{ + "unset": `{"type":"number"}`, + "empty-object": `{"additionalProperties":{}}`, + "object": `{"additionalProperties":{"type":"string"}}`, + "boolean": `{"additionalProperties":false}`, + }[propName], string(encoded)) + + if propName == "unset" { + require.True(t, ap == nil && apa == nil) + return + } + + require.Truef(t, (ap != nil && apa == nil) || (ap == nil && apa != nil), + "%s: isnil(%s) xor isnil(%s)", propName, apaStr, apStr) + }) + } + }) } } diff --git a/openapi3/issue513_test.go b/openapi3/issue513_test.go new file mode 100644 index 000000000..332b9226e --- /dev/null +++ b/openapi3/issue513_test.go @@ -0,0 +1,173 @@ +package openapi3 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue513OKWithExtension(t *testing.T) { + spec := ` +openapi: "3.0.3" +info: + title: 'My app' + version: 1.0.0 + description: 'An API' + +paths: + /v1/operation: + delete: + summary: Delete something + responses: + 200: + description: Success + default: + description: '* **400** - Bad Request' + x-my-extension: {val: ue} + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Error: + type: object + description: An error response body. + properties: + message: + description: A detailed message describing the error. + type: string +`[1:] + sl := NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + err = doc.Validate(sl.Context) + require.NoError(t, err) + data, err := json.Marshal(doc) + require.NoError(t, err) + require.Contains(t, string(data), `x-my-extension`) +} + +func TestIssue513KOHasExtraFieldSchema(t *testing.T) { + spec := ` +openapi: "3.0.3" +info: + title: 'My app' + version: 1.0.0 + description: 'An API' + +paths: + /v1/operation: + delete: + summary: Delete something + responses: + 200: + description: Success + default: + description: '* **400** - Bad Request' + x-my-extension: {val: ue} + # Notice here schema is invalid. It should instead be: + # content: + # application/json: + # schema: + # $ref: '#/components/schemas/Error' + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Error: + type: object + description: An error response body. + properties: + message: + description: A detailed message describing the error. + type: string +`[1:] + sl := NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + require.Contains(t, doc.Paths["/v1/operation"].Delete.Responses["default"].Value.Extensions, `x-my-extension`) + err = doc.Validate(sl.Context) + require.ErrorContains(t, err, `extra sibling fields: [schema]`) +} + +func TestIssue513KOMixesRefAlongWithOtherFields(t *testing.T) { + spec := ` +openapi: "3.0.3" +info: + title: 'My app' + version: 1.0.0 + description: 'An API' + +paths: + /v1/operation: + delete: + summary: Delete something + responses: + 200: + description: A sibling field that the spec says is ignored + $ref: '#/components/responses/SomeResponseBody' +components: + responses: + SomeResponseBody: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + Error: + type: object + description: An error response body. + properties: + message: + description: A detailed message describing the error. + type: string +`[1:] + sl := NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + err = doc.Validate(sl.Context) + require.ErrorContains(t, err, `extra sibling fields: [description]`) +} + +func TestIssue513KOMixesRefAlongWithOtherFieldsAllowed(t *testing.T) { + spec := ` +openapi: "3.0.3" +info: + title: 'My app' + version: 1.0.0 + description: 'An API' + +paths: + /v1/operation: + delete: + summary: Delete something + responses: + 200: + description: A sibling field that the spec says is ignored + $ref: '#/components/responses/SomeResponseBody' +components: + responses: + SomeResponseBody: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + schemas: + Error: + type: object + description: An error response body. + properties: + message: + description: A detailed message describing the error. + type: string +`[1:] + sl := NewLoader() + doc, err := sl.LoadFromData([]byte(spec)) + require.NoError(t, err) + err = doc.Validate(sl.Context, AllowExtraSiblingFields("description")) + require.NoError(t, err) +} diff --git a/openapi3/link.go b/openapi3/link.go index 137aef309..08dfa8d67 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -2,12 +2,11 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type Links map[string]*LinkRef @@ -30,7 +29,7 @@ var _ jsonpointer.JSONPointable = (*Links)(nil) // Link is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#link-object type Link struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` @@ -41,18 +40,55 @@ type Link struct { } // MarshalJSON returns the JSON encoding of Link. -func (link *Link) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(link) +func (link Link) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 6+len(link.Extensions)) + for k, v := range link.Extensions { + m[k] = v + } + + if x := link.OperationRef; x != "" { + m["operationRef"] = x + } + if x := link.OperationID; x != "" { + m["operationId"] = x + } + if x := link.Description; x != "" { + m["description"] = x + } + if x := link.Parameters; len(x) != 0 { + m["parameters"] = x + } + if x := link.Server; x != nil { + m["server"] = x + } + if x := link.RequestBody; x != nil { + m["requestBody"] = x + } + + return json.Marshal(m) } // UnmarshalJSON sets Link to a copy of data. func (link *Link) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, link) + type LinkBis Link + var x LinkBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "operationRef") + delete(x.Extensions, "operationId") + delete(x.Extensions, "description") + delete(x.Extensions, "parameters") + delete(x.Extensions, "server") + delete(x.Extensions, "requestBody") + *link = Link(x) + return nil } // Validate returns an error if Link does not comply with the OpenAPI spec. func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if link.OperationID == "" && link.OperationRef == "" { return errors.New("missing operationId or operationRef on link") @@ -60,5 +96,6 @@ func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error if link.OperationID != "" && link.OperationRef != "" { return fmt.Errorf("operationId %q and operationRef %q are mutually exclusive", link.OperationID, link.OperationRef) } - return nil + + return validateExtensions(ctx, link.Extensions) } diff --git a/openapi3/load_cicular_ref_with_external_file_test.go b/openapi3/load_cicular_ref_with_external_file_test.go index 978ef8f38..9bcaaf77f 100644 --- a/openapi3/load_cicular_ref_with_external_file_test.go +++ b/openapi3/load_cicular_ref_with_external_file_test.go @@ -53,8 +53,8 @@ func TestLoadCircularRefFromFile(t *testing.T) { Title: "Recursive cyclic refs example", Version: "1.0", }, - Components: openapi3.Components{ - Schemas: map[string]*openapi3.SchemaRef{ + Components: &openapi3.Components{ + Schemas: openapi3.Schemas{ "Foo": foo, "Bar": bar, }, diff --git a/openapi3/loader.go b/openapi3/loader.go index ecc2ef256..72ab8c46a 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -175,6 +175,7 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR func unmarshal(data []byte, v interface{}) 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) } return nil @@ -190,54 +191,54 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { loader.resetVisitedPathItemRefs() } - // Visit all components - components := doc.Components - for _, component := range components.Headers { - if err = loader.resolveHeaderRef(doc, component, location); err != nil { - return + if components := doc.Components; components != nil { + for _, component := range components.Headers { + if err = loader.resolveHeaderRef(doc, component, location); err != nil { + return + } } - } - for _, component := range components.Parameters { - if err = loader.resolveParameterRef(doc, component, location); err != nil { - return + for _, component := range components.Parameters { + if err = loader.resolveParameterRef(doc, component, location); err != nil { + return + } } - } - for _, component := range components.RequestBodies { - if err = loader.resolveRequestBodyRef(doc, component, location); err != nil { - return + for _, component := range components.RequestBodies { + if err = loader.resolveRequestBodyRef(doc, component, location); err != nil { + return + } } - } - for _, component := range components.Responses { - if err = loader.resolveResponseRef(doc, component, location); err != nil { - return + for _, component := range components.Responses { + if err = loader.resolveResponseRef(doc, component, location); err != nil { + return + } } - } - for _, component := range components.Schemas { - if err = loader.resolveSchemaRef(doc, component, location, []string{}); err != nil { - return + for _, component := range components.Schemas { + if err = loader.resolveSchemaRef(doc, component, location, []string{}); err != nil { + return + } } - } - for _, component := range components.SecuritySchemes { - if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil { - return + for _, component := range components.SecuritySchemes { + if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil { + return + } } - } - examples := make([]string, 0, len(components.Examples)) - for name := range components.Examples { - examples = append(examples, name) - } - sort.Strings(examples) - for _, name := range examples { - component := components.Examples[name] - if err = loader.resolveExampleRef(doc, component, location); err != nil { - return + examples := make([]string, 0, len(components.Examples)) + for name := range components.Examples { + examples = append(examples, name) + } + sort.Strings(examples) + for _, name := range examples { + component := components.Examples[name] + if err = loader.resolveExampleRef(doc, component, location); err != nil { + return + } } - } - for _, component := range components.Callbacks { - if err = loader.resolveCallbackRef(doc, component, location); err != nil { - return + for _, component := range components.Callbacks { + if err = loader.resolveCallbackRef(doc, component, location); err != nil { + return + } } } @@ -361,10 +362,10 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { // Special case due to multijson if s, ok := cursor.(*SchemaRef); ok && fieldName == "additionalProperties" { - if ap := s.Value.AdditionalPropertiesAllowed; ap != nil { + if ap := s.Value.AdditionalProperties.Has; ap != nil { return *ap, nil } - return s.Value.AdditionalProperties, nil + return s.Value.AdditionalProperties.Schema, nil } switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() { @@ -390,14 +391,7 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { hasFields := false for i := 0; i < val.NumField(); i++ { hasFields = true - field := val.Type().Field(i) - tagValue := field.Tag.Get("yaml") - yamlKey := strings.Split(tagValue, ",")[0] - if yamlKey == "-" { - tagValue := field.Tag.Get("multijson") - yamlKey = strings.Split(tagValue, ",")[0] - } - if yamlKey == fieldName { + if fieldName == strings.Split(val.Type().Field(i).Tag.Get("yaml"), ",")[0] { return val.Field(i).Interface(), nil } } @@ -407,14 +401,10 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { return drillIntoField(val.FieldByName("Value").Interface(), fieldName) } if hasFields { - if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "ExtensionProps" { - extensions := val.Field(0).Interface().(ExtensionProps).Extensions + if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "Extensions" { + extensions := val.Field(0).Interface().(map[string]interface{}) if enc, ok := extensions[fieldName]; ok { - var dec interface{} - if err := json.Unmarshal(enc.(json.RawMessage), &dec); err != nil { - return nil, err - } - return dec, nil + return enc, nil } } } @@ -757,7 +747,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } } - if v := value.AdditionalProperties; v != nil { + if v := value.AdditionalProperties.Schema; v != nil { if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } @@ -923,7 +913,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen } id := unescapeRefString(rest) - if doc.Components.Callbacks == nil { + if doc.Components == nil || doc.Components.Callbacks == nil { return failedToResolveRefFragmentPart(ref, "callbacks") } resolved := doc.Components.Callbacks[id] diff --git a/openapi3/loader_paths_test.go b/openapi3/loader_paths_test.go index 584f00e85..f7edc7374 100644 --- a/openapi3/loader_paths_test.go +++ b/openapi3/loader_paths_test.go @@ -13,7 +13,6 @@ openapi: "3.0" info: version: "1.0" title: sample -basePath: /adc/v1 paths: PATH: get: diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index e792767fd..3515586a0 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -78,7 +78,7 @@ func ExampleLoader() { } func TestResolveSchemaRef(t *testing.T) { - source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1",description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`) + source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"B":{"type":"string"},"A":{"allOf":[{"$ref":"#/components/schemas/B"}]}}}}`) loader := NewLoader() doc, err := loader.LoadFromData(source) require.NoError(t, err) @@ -90,15 +90,6 @@ func TestResolveSchemaRef(t *testing.T) { require.NotNil(t, refAVisited.Value) } -func TestResolveSchemaRefWithNullSchemaRef(t *testing.T) { - source := []byte(`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{"/foo":{"post":{"requestBody":{"content":{"application/json":{"schema":null}}}}}}}`) - loader := NewLoader() - doc, err := loader.LoadFromData(source) - require.NoError(t, err) - err = doc.Validate(loader.Context) - require.EqualError(t, err, `invalid paths: invalid path /foo: invalid operation POST: found unresolved ref: ""`) -} - func TestResolveResponseExampleRef(t *testing.T) { source := []byte(` openapi: 3.0.1 diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 090be7657..2a9b4721c 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -2,19 +2,18 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "sort" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) // MediaType is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#media-type-object type MediaType struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` @@ -65,13 +64,40 @@ func (mediaType *MediaType) WithEncoding(name string, enc *Encoding) *MediaType } // MarshalJSON returns the JSON encoding of MediaType. -func (mediaType *MediaType) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(mediaType) +func (mediaType MediaType) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(mediaType.Extensions)) + for k, v := range mediaType.Extensions { + m[k] = v + } + if x := mediaType.Schema; x != nil { + m["schema"] = x + } + if x := mediaType.Example; x != nil { + m["example"] = x + } + if x := mediaType.Examples; len(x) != 0 { + m["examples"] = x + } + if x := mediaType.Encoding; len(x) != 0 { + m["encoding"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets MediaType to a copy of data. func (mediaType *MediaType) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, mediaType) + type MediaTypeBis MediaType + var x MediaTypeBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "schema") + delete(x.Extensions, "example") + delete(x.Extensions, "examples") + delete(x.Extensions, "encoding") + *mediaType = MediaType(x) + return nil } // Validate returns an error if MediaType does not comply with the OpenAPI spec. @@ -90,35 +116,33 @@ func (mediaType *MediaType) Validate(ctx context.Context, opts ...ValidationOpti return errors.New("example and examples are mutually exclusive") } - if vo := getValidationOptions(ctx); vo.examplesValidationDisabled { - return nil - } - - if example := mediaType.Example; example != nil { - if err := validateExampleValue(ctx, example, schema.Value); err != nil { - return fmt.Errorf("invalid example: %w", err) + if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled { + if example := mediaType.Example; example != nil { + if err := validateExampleValue(ctx, example, schema.Value); err != nil { + return fmt.Errorf("invalid example: %w", err) + } } - } - if examples := mediaType.Examples; examples != nil { - names := make([]string, 0, len(examples)) - for name := range examples { - names = append(names, name) - } - sort.Strings(names) - for _, k := range names { - v := examples[k] - if err := v.Validate(ctx); err != nil { - return fmt.Errorf("example %s: %w", k, err) + if examples := mediaType.Examples; examples != nil { + names := make([]string, 0, len(examples)) + for name := range examples { + names = append(names, name) } - if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil { - return fmt.Errorf("example %s: %w", k, err) + sort.Strings(names) + for _, k := range names { + v := examples[k] + if err := v.Validate(ctx); err != nil { + return fmt.Errorf("example %s: %w", k, err) + } + if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil { + return fmt.Errorf("example %s: %w", k, err) + } } } } } - return nil + return validateExtensions(ctx, mediaType.Extensions) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable @@ -138,6 +162,6 @@ func (mediaType MediaType) JSONLookup(token string) (interface{}, error) { case "encoding": return mediaType.Encoding, nil } - v, _, err := jsonpointer.GetForToken(mediaType.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(mediaType.Extensions, token) return v, err } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 6622ef030..8b8f71bb7 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -2,19 +2,18 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" - - "github.com/getkin/kin-openapi/jsoninfo" ) // T is the root of an OpenAPI v3 document // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#openapi-object type T struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` OpenAPI string `json:"openapi" yaml:"openapi"` // Required - Components Components `json:"components,omitempty" yaml:"components,omitempty"` + Components *Components `json:"components,omitempty" yaml:"components,omitempty"` Info *Info `json:"info" yaml:"info"` // Required Paths Paths `json:"paths" yaml:"paths"` // Required Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` @@ -26,13 +25,50 @@ type T struct { } // MarshalJSON returns the JSON encoding of T. -func (doc *T) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(doc) +func (doc T) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(doc.Extensions)) + for k, v := range doc.Extensions { + m[k] = v + } + m["openapi"] = doc.OpenAPI + if x := doc.Components; x != nil { + m["components"] = x + } + m["info"] = doc.Info + m["paths"] = doc.Paths + if x := doc.Security; len(x) != 0 { + m["security"] = x + } + if x := doc.Servers; len(x) != 0 { + m["servers"] = x + } + if x := doc.Tags; len(x) != 0 { + m["tags"] = x + } + if x := doc.ExternalDocs; x != nil { + m["externalDocs"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets T to a copy of data. func (doc *T) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, doc) + type TBis T + var x TBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "openapi") + delete(x.Extensions, "components") + delete(x.Extensions, "info") + delete(x.Extensions, "paths") + delete(x.Extensions, "security") + delete(x.Extensions, "servers") + delete(x.Extensions, "tags") + delete(x.Extensions, "externalDocs") + *doc = T(x) + return nil } func (doc *T) AddOperation(path string, method string, operation *Operation) { @@ -64,8 +100,10 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { // NOTE: only mention info/components/paths/... key in this func's errors. wrap = func(e error) error { return fmt.Errorf("invalid components: %w", e) } - if err := doc.Components.Validate(ctx); err != nil { - return wrap(err) + if v := doc.Components; v != nil { + if err := v.Validate(ctx); err != nil { + return wrap(err) + } } wrap = func(e error) error { return fmt.Errorf("invalid info: %w", e) } @@ -114,5 +152,5 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error { } } - return nil + return validateExtensions(ctx, doc.Extensions) } diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index 7736310cc..e01af82ba 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -299,28 +299,28 @@ func spec() *T { }, }, }, - Components: Components{ - Parameters: map[string]*ParameterRef{ + Components: &Components{ + Parameters: ParametersMap{ "someParameter": { Value: parameter, }, }, - RequestBodies: map[string]*RequestBodyRef{ + RequestBodies: RequestBodies{ "someRequestBody": { Value: requestBody, }, }, - Responses: map[string]*ResponseRef{ + Responses: Responses{ "someResponse": { Value: response, }, }, - Schemas: map[string]*SchemaRef{ + Schemas: Schemas{ "someSchema": { Value: schema, }, }, - Headers: map[string]*HeaderRef{ + Headers: Headers{ "someHeader": { Ref: "#/components/headers/otherHeader", }, @@ -328,7 +328,7 @@ func spec() *T { Value: &Header{Parameter{Schema: &SchemaRef{Value: NewStringSchema()}}}, }, }, - Examples: map[string]*ExampleRef{ + Examples: Examples{ "someExample": { Ref: "#/components/examples/otherExample", }, @@ -336,7 +336,7 @@ func spec() *T { Value: NewExample(example), }, }, - SecuritySchemes: map[string]*SecuritySchemeRef{ + SecuritySchemes: SecuritySchemes{ "someSecurityScheme": { Ref: "#/components/securitySchemes/otherSecurityScheme", }, diff --git a/openapi3/operation.go b/openapi3/operation.go index d87704905..645c0805f 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -2,19 +2,18 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "strconv" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object type Operation struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` // Optional tags for documentation. Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` @@ -58,13 +57,70 @@ func NewOperation() *Operation { } // MarshalJSON returns the JSON encoding of Operation. -func (operation *Operation) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(operation) +func (operation Operation) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 12+len(operation.Extensions)) + for k, v := range operation.Extensions { + m[k] = v + } + if x := operation.Tags; len(x) != 0 { + m["tags"] = x + } + if x := operation.Summary; x != "" { + m["summary"] = x + } + if x := operation.Description; x != "" { + m["description"] = x + } + if x := operation.OperationID; x != "" { + m["operationId"] = x + } + if x := operation.Parameters; len(x) != 0 { + m["parameters"] = x + } + if x := operation.RequestBody; x != nil { + m["requestBody"] = x + } + m["responses"] = operation.Responses + if x := operation.Callbacks; len(x) != 0 { + m["callbacks"] = x + } + if x := operation.Deprecated; x { + m["deprecated"] = x + } + if x := operation.Security; x != nil { + m["security"] = x + } + if x := operation.Servers; x != nil { + m["servers"] = x + } + if x := operation.ExternalDocs; x != nil { + m["externalDocs"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Operation to a copy of data. func (operation *Operation) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, operation) + type OperationBis Operation + var x OperationBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "tags") + delete(x.Extensions, "summary") + delete(x.Extensions, "description") + delete(x.Extensions, "operationId") + delete(x.Extensions, "parameters") + delete(x.Extensions, "requestBody") + delete(x.Extensions, "responses") + delete(x.Extensions, "callbacks") + delete(x.Extensions, "deprecated") + delete(x.Extensions, "security") + delete(x.Extensions, "servers") + delete(x.Extensions, "externalDocs") + *operation = Operation(x) + return nil } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable @@ -101,7 +157,7 @@ func (operation Operation) JSONLookup(token string) (interface{}, error) { return operation.ExternalDocs, nil } - v, _, err := jsonpointer.GetForToken(operation.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(operation.Extensions, token) return v, err } @@ -156,5 +212,5 @@ func (operation *Operation) Validate(ctx context.Context, opts ...ValidationOpti } } - return nil + return validateExtensions(ctx, operation.Extensions) } diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 04e13b203..ec1893e9a 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -2,14 +2,13 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "sort" "strconv" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type ParametersMap map[string]*ParameterRef @@ -92,7 +91,7 @@ func (parameters Parameters) Validate(ctx context.Context, opts ...ValidationOpt // Parameter is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object type Parameter struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` @@ -169,13 +168,80 @@ func (parameter *Parameter) WithSchema(value *Schema) *Parameter { } // MarshalJSON returns the JSON encoding of Parameter. -func (parameter *Parameter) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(parameter) +func (parameter Parameter) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 13+len(parameter.Extensions)) + for k, v := range parameter.Extensions { + m[k] = v + } + + if x := parameter.Name; x != "" { + m["name"] = x + } + if x := parameter.In; x != "" { + m["in"] = x + } + if x := parameter.Description; x != "" { + m["description"] = x + } + if x := parameter.Style; x != "" { + m["style"] = x + } + if x := parameter.Explode; x != nil { + m["explode"] = x + } + if x := parameter.AllowEmptyValue; x { + m["allowEmptyValue"] = x + } + if x := parameter.AllowReserved; x { + m["allowReserved"] = x + } + if x := parameter.Deprecated; x { + m["deprecated"] = x + } + if x := parameter.Required; x { + m["required"] = x + } + if x := parameter.Schema; x != nil { + m["schema"] = x + } + if x := parameter.Example; x != nil { + m["example"] = x + } + if x := parameter.Examples; len(x) != 0 { + m["examples"] = x + } + if x := parameter.Content; len(x) != 0 { + m["content"] = x + } + + return json.Marshal(m) } // UnmarshalJSON sets Parameter to a copy of data. func (parameter *Parameter) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, parameter) + type ParameterBis Parameter + var x ParameterBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, "name") + delete(x.Extensions, "in") + delete(x.Extensions, "description") + delete(x.Extensions, "style") + delete(x.Extensions, "explode") + delete(x.Extensions, "allowEmptyValue") + delete(x.Extensions, "allowReserved") + delete(x.Extensions, "deprecated") + delete(x.Extensions, "required") + delete(x.Extensions, "schema") + delete(x.Extensions, "example") + delete(x.Extensions, "examples") + delete(x.Extensions, "content") + + *parameter = Parameter(x) + return nil } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable @@ -214,7 +280,7 @@ func (parameter Parameter) JSONLookup(token string) (interface{}, error) { return parameter.Content, nil } - v, _, err := jsonpointer.GetForToken(parameter.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(parameter.Extensions, token) return v, err } @@ -348,5 +414,5 @@ func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOpti } } - return nil + return validateExtensions(ctx, parameter.Extensions) } diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 5323dc163..fab75d93c 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -2,17 +2,16 @@ package openapi3 import ( "context" + "encoding/json" "fmt" "net/http" "sort" - - "github.com/getkin/kin-openapi/jsoninfo" ) // PathItem is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#path-item-object type PathItem struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` @@ -31,13 +30,81 @@ type PathItem struct { } // MarshalJSON returns the JSON encoding of PathItem. -func (pathItem *PathItem) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(pathItem) +func (pathItem PathItem) MarshalJSON() ([]byte, error) { + if ref := pathItem.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + + m := make(map[string]interface{}, 13+len(pathItem.Extensions)) + for k, v := range pathItem.Extensions { + m[k] = v + } + if x := pathItem.Summary; x != "" { + m["summary"] = x + } + if x := pathItem.Description; x != "" { + m["description"] = x + } + if x := pathItem.Connect; x != nil { + m["connect"] = x + } + if x := pathItem.Delete; x != nil { + m["delete"] = x + } + if x := pathItem.Get; x != nil { + m["get"] = x + } + if x := pathItem.Head; x != nil { + m["head"] = x + } + if x := pathItem.Options; x != nil { + m["options"] = x + } + if x := pathItem.Patch; x != nil { + m["patch"] = x + } + if x := pathItem.Post; x != nil { + m["post"] = x + } + if x := pathItem.Put; x != nil { + m["put"] = x + } + if x := pathItem.Trace; x != nil { + m["trace"] = x + } + if x := pathItem.Servers; len(x) != 0 { + m["servers"] = x + } + if x := pathItem.Parameters; len(x) != 0 { + m["parameters"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets PathItem to a copy of data. func (pathItem *PathItem) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, pathItem) + type PathItemBis PathItem + var x PathItemBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "$ref") + delete(x.Extensions, "summary") + delete(x.Extensions, "description") + delete(x.Extensions, "connect") + delete(x.Extensions, "delete") + delete(x.Extensions, "get") + delete(x.Extensions, "head") + delete(x.Extensions, "options") + delete(x.Extensions, "patch") + delete(x.Extensions, "post") + delete(x.Extensions, "put") + delete(x.Extensions, "trace") + delete(x.Extensions, "servers") + delete(x.Extensions, "parameters") + *pathItem = PathItem(x) + return nil } func (pathItem *PathItem) Operations() map[string]*Operation { @@ -139,5 +206,6 @@ func (pathItem *PathItem) Validate(ctx context.Context, opts ...ValidationOption return fmt.Errorf("invalid operation %s: %v", method, err) } } - return nil + + return validateExtensions(ctx, pathItem.Extensions) } diff --git a/openapi3/ref.go b/openapi3/ref.go new file mode 100644 index 000000000..a937de4a5 --- /dev/null +++ b/openapi3/ref.go @@ -0,0 +1,7 @@ +package openapi3 + +// 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 { + Ref string `json:"$ref" yaml:"$ref"` +} diff --git a/openapi3/refs.go b/openapi3/refs.go index d36d562fe..cc9b41a45 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -2,58 +2,88 @@ package openapi3 import ( "context" + "encoding/json" + "fmt" + "sort" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" + "github.com/perimeterx/marshmallow" ) -// 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 { - Ref string `json:"$ref" yaml:"$ref"` -} - // CallbackRef represents either a Callback or a $ref to a Callback. // When serializing and both fields are set, Ref is preferred over Value. type CallbackRef struct { Ref string Value *Callback + extra []string } var _ jsonpointer.JSONPointable = (*CallbackRef)(nil) // MarshalYAML returns the YAML encoding of CallbackRef. -func (value *CallbackRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x CallbackRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of CallbackRef. -func (value *CallbackRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x CallbackRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return json.Marshal(x.Value) } // UnmarshalJSON sets CallbackRef to a copy of data. -func (value *CallbackRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *CallbackRef) 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) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if CallbackRef does not comply with the OpenAPI spec. -func (value *CallbackRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *CallbackRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + 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(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value CallbackRef) JSONLookup(token string) (interface{}, error) { +func (x *CallbackRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -62,41 +92,75 @@ func (value CallbackRef) JSONLookup(token string) (interface{}, error) { type ExampleRef struct { Ref string Value *Example + extra []string } var _ jsonpointer.JSONPointable = (*ExampleRef)(nil) // MarshalYAML returns the YAML encoding of ExampleRef. -func (value *ExampleRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x ExampleRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of ExampleRef. -func (value *ExampleRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x ExampleRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets ExampleRef to a copy of data. -func (value *ExampleRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *ExampleRef) 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) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if ExampleRef does not comply with the OpenAPI spec. -func (value *ExampleRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *ExampleRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + 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(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value ExampleRef) JSONLookup(token string) (interface{}, error) { +func (x *ExampleRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -105,41 +169,75 @@ func (value ExampleRef) JSONLookup(token string) (interface{}, error) { type HeaderRef struct { Ref string Value *Header + extra []string } var _ jsonpointer.JSONPointable = (*HeaderRef)(nil) // MarshalYAML returns the YAML encoding of HeaderRef. -func (value *HeaderRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x HeaderRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of HeaderRef. -func (value *HeaderRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x HeaderRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets HeaderRef to a copy of data. -func (value *HeaderRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *HeaderRef) 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) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if HeaderRef does not comply with the OpenAPI spec. -func (value *HeaderRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *HeaderRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + 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(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value HeaderRef) JSONLookup(token string) (interface{}, error) { +func (x *HeaderRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -148,30 +246,76 @@ func (value HeaderRef) JSONLookup(token string) (interface{}, error) { type LinkRef struct { Ref string Value *Link + extra []string } +var _ jsonpointer.JSONPointable = (*LinkRef)(nil) + // MarshalYAML returns the YAML encoding of LinkRef. -func (value *LinkRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x LinkRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of LinkRef. -func (value *LinkRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x LinkRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets LinkRef to a copy of data. -func (value *LinkRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *LinkRef) 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) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if LinkRef does not comply with the OpenAPI spec. -func (value *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + 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(value.Ref) + return foundUnresolvedRef(x.Ref) +} + +// JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable +func (x *LinkRef) JSONLookup(token string) (interface{}, error) { + if token == "$ref" { + return x.Ref, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) + return ptr, err } // ParameterRef represents either a Parameter or a $ref to a Parameter. @@ -179,127 +323,229 @@ func (value *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) er type ParameterRef struct { Ref string Value *Parameter + extra []string } var _ jsonpointer.JSONPointable = (*ParameterRef)(nil) // MarshalYAML returns the YAML encoding of ParameterRef. -func (value *ParameterRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x ParameterRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of ParameterRef. -func (value *ParameterRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x ParameterRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets ParameterRef to a copy of data. -func (value *ParameterRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *ParameterRef) 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) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if ParameterRef does not comply with the OpenAPI spec. -func (value *ParameterRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *ParameterRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + 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(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value ParameterRef) JSONLookup(token string) (interface{}, error) { +func (x *ParameterRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } -// ResponseRef represents either a Response or a $ref to a Response. +// RequestBodyRef represents either a RequestBody or a $ref to a RequestBody. // When serializing and both fields are set, Ref is preferred over Value. -type ResponseRef struct { +type RequestBodyRef struct { Ref string - Value *Response + Value *RequestBody + extra []string } -var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) +var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) -// MarshalYAML returns the YAML encoding of ResponseRef. -func (value *ResponseRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +// MarshalYAML returns the YAML encoding of RequestBodyRef. +func (x RequestBodyRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } -// MarshalJSON returns the JSON encoding of ResponseRef. -func (value *ResponseRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +// MarshalJSON returns the JSON encoding of RequestBodyRef. +func (x RequestBodyRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } -// UnmarshalJSON sets ResponseRef to a copy of data. -func (value *ResponseRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +// UnmarshalJSON sets RequestBodyRef to a copy of data. +func (x *RequestBodyRef) 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) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } -// Validate returns an error if ResponseRef does not comply with the OpenAPI spec. -func (value *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) error { +// Validate returns an error if RequestBodyRef does not comply with the OpenAPI spec. +func (x *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + 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(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value ResponseRef) JSONLookup(token string) (interface{}, error) { +func (x *RequestBodyRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } -// RequestBodyRef represents either a RequestBody or a $ref to a RequestBody. +// ResponseRef represents either a Response or a $ref to a Response. // When serializing and both fields are set, Ref is preferred over Value. -type RequestBodyRef struct { +type ResponseRef struct { Ref string - Value *RequestBody + Value *Response + extra []string } -var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) +var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) -// MarshalYAML returns the YAML encoding of RequestBodyRef. -func (value *RequestBodyRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +// MarshalYAML returns the YAML encoding of ResponseRef. +func (x ResponseRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } -// MarshalJSON returns the JSON encoding of RequestBodyRef. -func (value *RequestBodyRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +// MarshalJSON returns the JSON encoding of ResponseRef. +func (x ResponseRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } -// UnmarshalJSON sets RequestBodyRef to a copy of data. -func (value *RequestBodyRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +// UnmarshalJSON sets ResponseRef to a copy of data. +func (x *ResponseRef) 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) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } -// Validate returns an error if RequestBodyRef does not comply with the OpenAPI spec. -func (value *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) error { +// Validate returns an error if ResponseRef does not comply with the OpenAPI spec. +func (x *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + 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(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) { +func (x *ResponseRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -308,48 +554,75 @@ func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) { type SchemaRef struct { Ref string Value *Schema + extra []string } var _ jsonpointer.JSONPointable = (*SchemaRef)(nil) -func NewSchemaRef(ref string, value *Schema) *SchemaRef { - return &SchemaRef{ - Ref: ref, - Value: value, - } -} - // MarshalYAML returns the YAML encoding of SchemaRef. -func (value *SchemaRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x SchemaRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of SchemaRef. -func (value *SchemaRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x SchemaRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets SchemaRef to a copy of data. -func (value *SchemaRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *SchemaRef) 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) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if SchemaRef does not comply with the OpenAPI spec. -func (value *SchemaRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *SchemaRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + 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(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value SchemaRef) JSONLookup(token string) (interface{}, error) { +func (x *SchemaRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -358,48 +631,74 @@ func (value SchemaRef) JSONLookup(token string) (interface{}, error) { type SecuritySchemeRef struct { Ref string Value *SecurityScheme + extra []string } var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil) // MarshalYAML returns the YAML encoding of SecuritySchemeRef. -func (value *SecuritySchemeRef) MarshalYAML() (interface{}, error) { - return marshalRefYAML(value.Ref, value.Value) +func (x SecuritySchemeRef) MarshalYAML() (interface{}, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value, nil } // MarshalJSON returns the JSON encoding of SecuritySchemeRef. -func (value *SecuritySchemeRef) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalRef(value.Ref, value.Value) +func (x SecuritySchemeRef) MarshalJSON() ([]byte, error) { + if ref := x.Ref; ref != "" { + return json.Marshal(Ref{Ref: ref}) + } + return x.Value.MarshalJSON() } // UnmarshalJSON sets SecuritySchemeRef to a copy of data. -func (value *SecuritySchemeRef) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalRef(data, &value.Ref, &value.Value) +func (x *SecuritySchemeRef) 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) + } + } + return nil + } + return json.Unmarshal(data, &x.Value) } // Validate returns an error if SecuritySchemeRef does not comply with the OpenAPI spec. -func (value *SecuritySchemeRef) Validate(ctx context.Context, opts ...ValidationOption) error { +func (x *SecuritySchemeRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) - if v := value.Value; v != nil { + if extra := x.extra; len(extra) != 0 { + sort.Strings(extra) + + extras := make([]string, 0, len(extra)) + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + if allowed == nil { + allowed = make(map[string]struct{}, 0) + } + for _, ex := range extra { + if _, ok := allowed[ex]; !ok { + 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(value.Ref) + return foundUnresolvedRef(x.Ref) } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable -func (value SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { +func (x *SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { if token == "$ref" { - return value.Ref, nil + return x.Ref, nil } - - ptr, _, err := jsonpointer.GetForToken(value.Value, token) + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } - -// marshalRefYAML returns the YAML encoding of ref values. -func marshalRefYAML(value string, otherwise interface{}) (interface{}, error) { - if value != "" { - return &Ref{Ref: value}, nil - } - return otherwise, nil -} diff --git a/openapi3/request_body.go b/openapi3/request_body.go index f0d9e1ec2..de8919f41 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -2,12 +2,11 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type RequestBodies map[string]*RequestBodyRef @@ -30,7 +29,7 @@ func (r RequestBodies) JSONLookup(token string) (interface{}, error) { // RequestBody is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#request-body-object type RequestBody struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` @@ -95,13 +94,36 @@ func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType { } // MarshalJSON returns the JSON encoding of RequestBody. -func (requestBody *RequestBody) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(requestBody) +func (requestBody RequestBody) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 3+len(requestBody.Extensions)) + for k, v := range requestBody.Extensions { + m[k] = v + } + if x := requestBody.Description; x != "" { + m["description"] = requestBody.Description + } + if x := requestBody.Required; x { + m["required"] = x + } + if x := requestBody.Content; true { + m["content"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets RequestBody to a copy of data. func (requestBody *RequestBody) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, requestBody) + type RequestBodyBis RequestBody + var x RequestBodyBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "description") + delete(x.Extensions, "required") + delete(x.Extensions, "content") + *requestBody = RequestBody(x) + return nil } // Validate returns an error if RequestBody does not comply with the OpenAPI spec. @@ -116,5 +138,9 @@ func (requestBody *RequestBody) Validate(ctx context.Context, opts ...Validation vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false } - return requestBody.Content.Validate(ctx) + if err := requestBody.Content.Validate(ctx); err != nil { + return err + } + + return validateExtensions(ctx, requestBody.Extensions) } diff --git a/openapi3/response.go b/openapi3/response.go index 324f77ddc..b85c9145c 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -2,14 +2,13 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "sort" "strconv" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Responses is specified by OpenAPI/Swagger 3.0 standard. @@ -70,7 +69,7 @@ func (responses Responses) JSONLookup(token string) (interface{}, error) { // 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 { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -103,13 +102,40 @@ func (response *Response) WithJSONSchemaRef(schema *SchemaRef) *Response { } // MarshalJSON returns the JSON encoding of Response. -func (response *Response) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(response) +func (response Response) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(response.Extensions)) + for k, v := range response.Extensions { + m[k] = v + } + if x := response.Description; x != nil { + m["description"] = x + } + if x := response.Headers; len(x) != 0 { + m["headers"] = x + } + if x := response.Content; len(x) != 0 { + m["content"] = x + } + if x := response.Links; len(x) != 0 { + m["links"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Response to a copy of data. func (response *Response) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, response) + type ResponseBis Response + var x ResponseBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "description") + delete(x.Extensions, "headers") + delete(x.Extensions, "content") + delete(x.Extensions, "links") + *response = Response(x) + return nil } // Validate returns an error if Response does not comply with the OpenAPI spec. @@ -152,5 +178,6 @@ func (response *Response) Validate(ctx context.Context, opts ...ValidationOption return err } } - return nil + + return validateExtensions(ctx, response.Extensions) } diff --git a/openapi3/schema.go b/openapi3/schema.go index d795fb530..9d21c00e6 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -17,8 +17,6 @@ import ( "github.com/go-openapi/jsonpointer" "github.com/mohae/deepcopy" - - "github.com/getkin/kin-openapi/jsoninfo" ) const ( @@ -71,6 +69,14 @@ func Uint64Ptr(value uint64) *uint64 { return &value } +// NewSchemaRef simply builds a SchemaRef +func NewSchemaRef(ref string, value *Schema) *SchemaRef { + return &SchemaRef{ + Ref: ref, + Value: value, + } +} + type Schemas map[string]*SchemaRef var _ jsonpointer.JSONPointable = (*Schemas)(nil) @@ -114,7 +120,7 @@ func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { // Schema is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object type Schema struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` @@ -159,13 +165,57 @@ type Schema struct { Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` // Object - Required []string `json:"required,omitempty" yaml:"required,omitempty"` - Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` - MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` - MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` - AdditionalPropertiesAllowed *bool `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // In this order... - AdditionalProperties *SchemaRef `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // ...for multijson - Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"` + MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"` + AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` +} + +type AdditionalProperties struct { + Has *bool + Schema *SchemaRef +} + +// 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) + } + return nil, nil +} + +// UnmarshalJSON sets AdditionalProperties to a copy of data. +func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error { + var x interface{} + if err := json.Unmarshal(data, &x); err != nil { + return err + } + switch y := x.(type) { + case nil: + case bool: + addProps.Has = &y + case map[string]interface{}: + if len(y) == 0 { + addProps.Schema = &SchemaRef{Value: &Schema{}} + } else { + buf := new(bytes.Buffer) + json.NewEncoder(buf).Encode(y) + if err := json.NewDecoder(buf).Decode(&addProps.Schema); err != nil { + return err + } + } + default: + return errors.New("cannot unmarshal additionalProperties: value must be either a schema object or a boolean") + } + return nil } var _ jsonpointer.JSONPointable = (*Schema)(nil) @@ -175,31 +225,217 @@ func NewSchema() *Schema { } // MarshalJSON returns the JSON encoding of Schema. -func (schema *Schema) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(schema) +func (schema Schema) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 36+len(schema.Extensions)) + for k, v := range schema.Extensions { + m[k] = v + } + + if x := schema.OneOf; len(x) != 0 { + m["oneOf"] = x + } + if x := schema.AnyOf; len(x) != 0 { + m["anyOf"] = x + } + if x := schema.AllOf; len(x) != 0 { + m["allOf"] = x + } + if x := schema.Not; x != nil { + m["not"] = x + } + if x := schema.Type; len(x) != 0 { + m["type"] = x + } + if x := schema.Title; len(x) != 0 { + m["title"] = x + } + if x := schema.Format; len(x) != 0 { + m["format"] = x + } + if x := schema.Description; len(x) != 0 { + m["description"] = x + } + if x := schema.Enum; len(x) != 0 { + m["enum"] = x + } + if x := schema.Default; x != nil { + m["default"] = x + } + if x := schema.Example; x != nil { + m["example"] = x + } + if x := schema.ExternalDocs; x != nil { + m["externalDocs"] = x + } + + // Array-related + if x := schema.UniqueItems; x { + m["uniqueItems"] = x + } + // Number-related + if x := schema.ExclusiveMin; x { + m["exclusiveMinimum"] = x + } + if x := schema.ExclusiveMax; x { + m["exclusiveMaximum"] = x + } + // Properties + if x := schema.Nullable; x { + m["nullable"] = x + } + if x := schema.ReadOnly; x { + m["readOnly"] = x + } + if x := schema.WriteOnly; x { + m["writeOnly"] = x + } + if x := schema.AllowEmptyValue; x { + m["allowEmptyValue"] = x + } + if x := schema.Deprecated; x { + m["deprecated"] = x + } + if x := schema.XML; x != nil { + m["xml"] = x + } + + // Number + if x := schema.Min; x != nil { + m["minimum"] = x + } + if x := schema.Max; x != nil { + m["maximum"] = x + } + if x := schema.MultipleOf; x != nil { + m["multipleOf"] = x + } + + // String + if x := schema.MinLength; x != 0 { + m["minLength"] = x + } + if x := schema.MaxLength; x != nil { + m["maxLength"] = x + } + if x := schema.Pattern; x != "" { + m["pattern"] = x + } + + // Array + if x := schema.MinItems; x != 0 { + m["minItems"] = x + } + if x := schema.MaxItems; x != nil { + m["maxItems"] = x + } + if x := schema.Items; x != nil { + m["items"] = x + } + + // Object + if x := schema.Required; len(x) != 0 { + m["required"] = x + } + if x := schema.Properties; len(x) != 0 { + m["properties"] = x + } + if x := schema.MinProps; x != 0 { + m["minProperties"] = x + } + if x := schema.MaxProps; x != nil { + m["maxProperties"] = x + } + if x := schema.AdditionalProperties; x.Has != nil || x.Schema != nil { + m["additionalProperties"] = &x + } + if x := schema.Discriminator; x != nil { + m["discriminator"] = x + } + + return json.Marshal(m) } // UnmarshalJSON sets Schema to a copy of data. func (schema *Schema) UnmarshalJSON(data []byte) error { - err := jsoninfo.UnmarshalStrictStruct(data, schema) + type SchemaBis Schema + var x SchemaBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, "oneOf") + delete(x.Extensions, "anyOf") + delete(x.Extensions, "allOf") + delete(x.Extensions, "not") + delete(x.Extensions, "type") + delete(x.Extensions, "title") + delete(x.Extensions, "format") + delete(x.Extensions, "description") + delete(x.Extensions, "enum") + delete(x.Extensions, "default") + delete(x.Extensions, "example") + delete(x.Extensions, "externalDocs") + + // Array-related + delete(x.Extensions, "uniqueItems") + // Number-related + delete(x.Extensions, "exclusiveMinimum") + delete(x.Extensions, "exclusiveMaximum") + // Properties + delete(x.Extensions, "nullable") + delete(x.Extensions, "readOnly") + delete(x.Extensions, "writeOnly") + delete(x.Extensions, "allowEmptyValue") + delete(x.Extensions, "deprecated") + delete(x.Extensions, "xml") + + // Number + delete(x.Extensions, "minimum") + delete(x.Extensions, "maximum") + delete(x.Extensions, "multipleOf") + + // String + delete(x.Extensions, "minLength") + delete(x.Extensions, "maxLength") + delete(x.Extensions, "pattern") + + // Array + delete(x.Extensions, "minItems") + delete(x.Extensions, "maxItems") + delete(x.Extensions, "items") + + // Object + delete(x.Extensions, "required") + delete(x.Extensions, "properties") + delete(x.Extensions, "minProperties") + delete(x.Extensions, "maxProperties") + delete(x.Extensions, "additionalProperties") + delete(x.Extensions, "discriminator") + + *schema = Schema(x) + if schema.Format == "date" { // This is a fix for: https://github.com/getkin/kin-openapi/issues/697 if eg, ok := schema.Example.(string); ok { schema.Example = strings.TrimSuffix(eg, "T00:00:00Z") } } - return err + return nil } // JSONLookup implements github.com/go-openapi/jsonpointer#JSONPointable func (schema Schema) JSONLookup(token string) (interface{}, error) { switch token { case "additionalProperties": - if schema.AdditionalProperties != nil { - if schema.AdditionalProperties.Ref != "" { - return &Ref{Ref: schema.AdditionalProperties.Ref}, nil + if addProps := schema.AdditionalProperties.Has; addProps != nil { + return *addProps, nil + } + if addProps := schema.AdditionalProperties.Schema; addProps != nil { + if addProps.Ref != "" { + return &Ref{Ref: addProps.Ref}, nil } - return schema.AdditionalProperties.Value, nil + return addProps.Value, nil } case "not": if schema.Not != nil { @@ -237,8 +473,6 @@ func (schema Schema) JSONLookup(token string) (interface{}, error) { return schema.Example, nil case "externalDocs": return schema.ExternalDocs, nil - case "additionalPropertiesAllowed": - return schema.AdditionalPropertiesAllowed, nil case "uniqueItems": return schema.UniqueItems, nil case "exclusiveMin": @@ -285,7 +519,7 @@ func (schema Schema) JSONLookup(token string) (interface{}, error) { return schema.Discriminator, nil } - v, _, err := jsonpointer.GetForToken(schema.ExtensionProps, token) + v, _, err := jsonpointer.GetForToken(schema.Extensions, token) return v, err } @@ -546,23 +780,19 @@ func (schema *Schema) WithMaxProperties(i int64) *Schema { } func (schema *Schema) WithAnyAdditionalProperties() *Schema { - schema.AdditionalProperties = nil - t := true - schema.AdditionalPropertiesAllowed = &t + schema.AdditionalProperties = AdditionalProperties{Has: BoolPtr(true)} return schema } func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema { - if v == nil { - schema.AdditionalProperties = nil - } else { - schema.AdditionalProperties = &SchemaRef{ - Value: v, - } + schema.AdditionalProperties = AdditionalProperties{} + if v != nil { + schema.AdditionalProperties.Schema = &SchemaRef{Value: v} } return schema } +// IsEmpty tells whether schema is equivalent to the empty schema `{}`. func (schema *Schema) IsEmpty() bool { if schema.Type != "" || schema.Format != "" || len(schema.Enum) != 0 || schema.UniqueItems || schema.ExclusiveMin || schema.ExclusiveMax || @@ -577,10 +807,10 @@ func (schema *Schema) IsEmpty() bool { if n := schema.Not; n != nil && !n.Value.IsEmpty() { return false } - if ap := schema.AdditionalProperties; ap != nil && !ap.Value.IsEmpty() { + if ap := schema.AdditionalProperties.Schema; ap != nil && !ap.Value.IsEmpty() { return false } - if apa := schema.AdditionalPropertiesAllowed; apa != nil && !*apa { + if apa := schema.AdditionalProperties.Has; apa != nil && *apa { return false } if items := schema.Items; items != nil && !items.Value.IsEmpty() { @@ -615,12 +845,12 @@ func (schema *Schema) Validate(ctx context.Context, opts ...ValidationOption) er return schema.validate(ctx, []*Schema{}) } -func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) { +func (schema *Schema) validate(ctx context.Context, stack []*Schema) error { validationOpts := getValidationOptions(ctx) for _, existing := range stack { if existing == schema { - return + return nil } } stack = append(stack, schema) @@ -634,8 +864,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(item.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } @@ -644,8 +874,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(item.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } @@ -654,8 +884,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(item.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } @@ -664,8 +894,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } @@ -716,7 +946,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } if schema.Pattern != "" && !validationOpts.schemaPatternValidationDisabled { - if err = schema.compilePattern(); err != nil { + if err := schema.compilePattern(); err != nil { return err } } @@ -734,8 +964,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } @@ -750,23 +980,26 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } - if ref := schema.AdditionalProperties; ref != nil { + if schema.AdditionalProperties.Has != nil && schema.AdditionalProperties.Schema != nil { + return errors.New("additionalProperties are set to both boolean and schema") + } + if ref := schema.AdditionalProperties.Schema; ref != nil { v := ref.Value if v == nil { return foundUnresolvedRef(ref.Ref) } - if err = v.validate(ctx, stack); err != nil { - return + if err := v.validate(ctx, stack); err != nil { + return err } } if v := schema.ExternalDocs; v != nil { - if err = v.Validate(ctx); err != nil { + if err := v.Validate(ctx); err != nil { return fmt.Errorf("invalid external docs: %w", err) } } @@ -783,7 +1016,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error) } } - return + return validateExtensions(ctx, schema.Extensions) } func (schema *Schema) IsMatching(value interface{}) bool { @@ -1572,7 +1805,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value // "additionalProperties" var additionalProperties *Schema - if ref := schema.AdditionalProperties; ref != nil { + if ref := schema.AdditionalProperties.Schema; ref != nil { additionalProperties = ref.Value } keys := make([]string, 0, len(value)) @@ -1606,8 +1839,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value continue } } - allowed := schema.AdditionalPropertiesAllowed - if additionalProperties != nil || allowed == nil || *allowed { + if allowed := schema.AdditionalProperties.Has; allowed == nil || *allowed { if additionalProperties != nil { if err := additionalProperties.visitJSON(settings, v); err != nil { if settings.failfast { diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index 89971cff0..2bd9848dd 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -801,11 +801,11 @@ var schemaExamples = []schemaExample{ { Schema: &Schema{ Type: "object", - AdditionalProperties: &SchemaRef{ + AdditionalProperties: AdditionalProperties{Schema: &SchemaRef{ Value: &Schema{ Type: "number", }, - }, + }}, }, Serialization: map[string]interface{}{ "type": "object", @@ -828,8 +828,8 @@ var schemaExamples = []schemaExample{ }, { Schema: &Schema{ - Type: "object", - AdditionalPropertiesAllowed: BoolPtr(true), + Type: "object", + AdditionalProperties: AdditionalProperties{Has: BoolPtr(true)}, }, Serialization: map[string]interface{}{ "type": "object", diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index 3f5bd9510..87891c954 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -45,7 +45,7 @@ func (security SecurityRequirement) Authenticate(provider string, scopes ...stri // Validate returns an error if SecurityRequirement does not comply with the OpenAPI spec. func (security *SecurityRequirement) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) return nil } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 83330f24a..f9a08385b 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -2,13 +2,12 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "net/url" "github.com/go-openapi/jsonpointer" - - "github.com/getkin/kin-openapi/jsoninfo" ) type SecuritySchemes map[string]*SecuritySchemeRef @@ -31,7 +30,7 @@ var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil) // SecurityScheme is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object type SecurityScheme struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -71,13 +70,56 @@ func NewJWTSecurityScheme() *SecurityScheme { } // MarshalJSON returns the JSON encoding of SecurityScheme. -func (ss *SecurityScheme) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(ss) +func (ss SecurityScheme) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 8+len(ss.Extensions)) + for k, v := range ss.Extensions { + m[k] = v + } + if x := ss.Type; x != "" { + m["type"] = x + } + if x := ss.Description; x != "" { + m["description"] = x + } + if x := ss.Name; x != "" { + m["name"] = x + } + if x := ss.In; x != "" { + m["in"] = x + } + if x := ss.Scheme; x != "" { + m["scheme"] = x + } + if x := ss.BearerFormat; x != "" { + m["bearerFormat"] = x + } + if x := ss.Flows; x != nil { + m["flows"] = x + } + if x := ss.OpenIdConnectUrl; x != "" { + m["openIdConnectUrl"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets SecurityScheme to a copy of data. func (ss *SecurityScheme) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, ss) + type SecuritySchemeBis SecurityScheme + var x SecuritySchemeBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "type") + delete(x.Extensions, "description") + delete(x.Extensions, "name") + delete(x.Extensions, "in") + delete(x.Extensions, "scheme") + delete(x.Extensions, "bearerFormat") + delete(x.Extensions, "flows") + delete(x.Extensions, "openIdConnectUrl") + *ss = SecurityScheme(x) + return nil } func (ss *SecurityScheme) WithType(value string) *SecurityScheme { @@ -173,13 +215,14 @@ func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption } else if ss.Flows != nil { return fmt.Errorf("security scheme of type %q can't have 'flows'", ss.Type) } - return nil + + return validateExtensions(ctx, ss.Extensions) } // OAuthFlows is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object type OAuthFlows struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` @@ -197,13 +240,40 @@ const ( ) // MarshalJSON returns the JSON encoding of OAuthFlows. -func (flows *OAuthFlows) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(flows) +func (flows OAuthFlows) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(flows.Extensions)) + for k, v := range flows.Extensions { + m[k] = v + } + if x := flows.Implicit; x != nil { + m["implicit"] = x + } + if x := flows.Password; x != nil { + m["password"] = x + } + if x := flows.ClientCredentials; x != nil { + m["clientCredentials"] = x + } + if x := flows.AuthorizationCode; x != nil { + m["authorizationCode"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets OAuthFlows to a copy of data. func (flows *OAuthFlows) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, flows) + type OAuthFlowsBis OAuthFlows + var x OAuthFlowsBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "implicit") + delete(x.Extensions, "password") + delete(x.Extensions, "clientCredentials") + delete(x.Extensions, "authorizationCode") + *flows = OAuthFlows(x) + return nil } // Validate returns an error if OAuthFlows does not comply with the OpenAPI spec. @@ -215,48 +285,77 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) return fmt.Errorf("the OAuth flow 'implicit' is invalid: %w", err) } } + if v := flows.Password; v != nil { if err := v.validate(ctx, oAuthFlowTypePassword, opts...); err != nil { return fmt.Errorf("the OAuth flow 'password' is invalid: %w", err) } } + if v := flows.ClientCredentials; v != nil { if err := v.validate(ctx, oAuthFlowTypeClientCredentials, opts...); err != nil { return fmt.Errorf("the OAuth flow 'clientCredentials' is invalid: %w", err) } } + if v := flows.AuthorizationCode; v != nil { if err := v.validate(ctx, oAuthFlowAuthorizationCode, opts...); err != nil { return fmt.Errorf("the OAuth flow 'authorizationCode' is invalid: %w", err) } } - return nil + + return validateExtensions(ctx, flows.Extensions) } // OAuthFlow is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object type OAuthFlow struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes map[string]string `json:"scopes" yaml:"scopes"` + Scopes map[string]string `json:"scopes" yaml:"scopes"` // required } // MarshalJSON returns the JSON encoding of OAuthFlow. -func (flow *OAuthFlow) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(flow) +func (flow OAuthFlow) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(flow.Extensions)) + for k, v := range flow.Extensions { + m[k] = v + } + if x := flow.AuthorizationURL; x != "" { + m["authorizationUrl"] = x + } + if x := flow.TokenURL; x != "" { + m["tokenUrl"] = x + } + if x := flow.RefreshURL; x != "" { + m["refreshUrl"] = x + } + m["scopes"] = flow.Scopes + return json.Marshal(m) } // UnmarshalJSON sets OAuthFlow to a copy of data. func (flow *OAuthFlow) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, flow) + type OAuthFlowBis OAuthFlow + var x OAuthFlowBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "authorizationUrl") + delete(x.Extensions, "tokenUrl") + delete(x.Extensions, "refreshUrl") + delete(x.Extensions, "scopes") + *flow = OAuthFlow(x) + return nil } // Validate returns an error if OAuthFlows does not comply with the OpenAPI spec. func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if v := flow.RefreshURL; v != "" { if _, err := url.Parse(v); err != nil { @@ -268,7 +367,7 @@ func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) e return errors.New("field 'scopes' is empty or missing") } - return nil + return validateExtensions(ctx, flow.Extensions) } func (flow *OAuthFlow) validate(ctx context.Context, typ oAuthFlowType, opts ...ValidationOption) error { diff --git a/openapi3/server.go b/openapi3/server.go index 587e8e0e1..9fc99f90c 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -2,14 +2,13 @@ package openapi3 import ( "context" + "encoding/json" "errors" "fmt" "math" "net/url" "sort" "strings" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Servers is specified by OpenAPI/Swagger standard version 3. @@ -52,9 +51,9 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) // Server is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-object type Server struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` - URL string `json:"url" yaml:"url"` + URL string `json:"url" yaml:"url"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"` } @@ -84,13 +83,34 @@ func (server *Server) BasePath() (string, error) { } // MarshalJSON returns the JSON encoding of Server. -func (server *Server) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(server) +func (server Server) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 3+len(server.Extensions)) + for k, v := range server.Extensions { + m[k] = v + } + m["url"] = server.URL + if x := server.Description; x != "" { + m["description"] = x + } + if x := server.Variables; len(x) != 0 { + m["variables"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Server to a copy of data. func (server *Server) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, server) + type ServerBis Server + var x ServerBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "url") + delete(x.Extensions, "description") + delete(x.Extensions, "variables") + *server = Server(x) + return nil } func (server Server) ParameterNames() ([]string, error) { @@ -195,13 +215,14 @@ func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (e return } } - return + + return validateExtensions(ctx, server.Extensions) } // ServerVariable is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object type ServerVariable struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` @@ -209,18 +230,41 @@ type ServerVariable struct { } // MarshalJSON returns the JSON encoding of ServerVariable. -func (serverVariable *ServerVariable) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(serverVariable) +func (serverVariable ServerVariable) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 4+len(serverVariable.Extensions)) + for k, v := range serverVariable.Extensions { + m[k] = v + } + if x := serverVariable.Enum; len(x) != 0 { + m["enum"] = x + } + if x := serverVariable.Default; x != "" { + m["default"] = x + } + if x := serverVariable.Description; x != "" { + m["description"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets ServerVariable to a copy of data. func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, serverVariable) + type ServerVariableBis ServerVariable + var x ServerVariableBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "enum") + delete(x.Extensions, "default") + delete(x.Extensions, "description") + *serverVariable = ServerVariable(x) + return nil } // Validate returns an error if ServerVariable does not comply with the OpenAPI spec. func (serverVariable *ServerVariable) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) if serverVariable.Default == "" { data, err := serverVariable.MarshalJSON() @@ -229,5 +273,6 @@ func (serverVariable *ServerVariable) Validate(ctx context.Context, opts ...Vali } return fmt.Errorf("field default is required in %s", data) } - return nil + + return validateExtensions(ctx, serverVariable.Extensions) } diff --git a/openapi3/tag.go b/openapi3/tag.go index b5cb7f899..93009a13c 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -2,9 +2,8 @@ package openapi3 import ( "context" + "encoding/json" "fmt" - - "github.com/getkin/kin-openapi/jsoninfo" ) // Tags is specified by OpenAPI/Swagger 3.0 standard. @@ -34,7 +33,7 @@ func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error { // Tag is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tag-object type Tag struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -42,13 +41,36 @@ type Tag struct { } // MarshalJSON returns the JSON encoding of Tag. -func (t *Tag) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(t) +func (t Tag) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 3+len(t.Extensions)) + for k, v := range t.Extensions { + m[k] = v + } + if x := t.Name; x != "" { + m["name"] = x + } + if x := t.Description; x != "" { + m["description"] = x + } + if x := t.ExternalDocs; x != nil { + m["externalDocs"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets Tag to a copy of data. func (t *Tag) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, t) + type TagBis Tag + var x TagBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "name") + delete(x.Extensions, "description") + delete(x.Extensions, "externalDocs") + *t = Tag(x) + return nil } // Validate returns an error if Tag does not comply with the OpenAPI spec. @@ -60,5 +82,6 @@ func (t *Tag) Validate(ctx context.Context, opts ...ValidationOption) error { return fmt.Errorf("invalid external docs: %w", err) } } - return nil + + return validateExtensions(ctx, t.Extensions) } diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 343b6836e..0ca12e5ab 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -12,10 +12,23 @@ type ValidationOptions struct { schemaDefaultsValidationDisabled bool schemaFormatValidationEnabled bool schemaPatternValidationDisabled bool + extraSiblingFieldsAllowed map[string]struct{} } type validationOptionsKey struct{} +// AllowExtraSiblingFields called as AllowExtraSiblingFields("description") makes Validate not return an error when said field appears next to a $ref. +func AllowExtraSiblingFields(fields ...string) ValidationOption { + return func(options *ValidationOptions) { + for _, field := range fields { + if options.extraSiblingFieldsAllowed == nil { + options.extraSiblingFieldsAllowed = make(map[string]struct{}, len(fields)) + } + options.extraSiblingFieldsAllowed[field] = struct{}{} + } + } +} + // EnableSchemaFormatValidation makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification. // By default, schema format validation is disabled. func EnableSchemaFormatValidation() ValidationOption { diff --git a/openapi3/xml.go b/openapi3/xml.go index a55ff410d..34ed3be32 100644 --- a/openapi3/xml.go +++ b/openapi3/xml.go @@ -2,14 +2,13 @@ package openapi3 import ( "context" - - "github.com/getkin/kin-openapi/jsoninfo" + "encoding/json" ) // XML is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xml-object type XML struct { - ExtensionProps `json:"-" yaml:"-"` + Extensions map[string]interface{} `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` @@ -19,18 +18,49 @@ type XML struct { } // MarshalJSON returns the JSON encoding of XML. -func (xml *XML) MarshalJSON() ([]byte, error) { - return jsoninfo.MarshalStrictStruct(xml) +func (xml XML) MarshalJSON() ([]byte, error) { + m := make(map[string]interface{}, 5+len(xml.Extensions)) + for k, v := range xml.Extensions { + m[k] = v + } + if x := xml.Name; x != "" { + m["name"] = x + } + if x := xml.Namespace; x != "" { + m["namespace"] = x + } + if x := xml.Prefix; x != "" { + m["prefix"] = x + } + if x := xml.Attribute; x { + m["attribute"] = x + } + if x := xml.Wrapped; x { + m["wrapped"] = x + } + return json.Marshal(m) } // UnmarshalJSON sets XML to a copy of data. func (xml *XML) UnmarshalJSON(data []byte) error { - return jsoninfo.UnmarshalStrictStruct(data, xml) + type XMLBis XML + var x XMLBis + if err := json.Unmarshal(data, &x); err != nil { + return err + } + _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, "name") + delete(x.Extensions, "namespace") + delete(x.Extensions, "prefix") + delete(x.Extensions, "attribute") + delete(x.Extensions, "wrapped") + *xml = XML(x) + return nil } // Validate returns an error if XML does not comply with the OpenAPI spec. func (xml *XML) Validate(ctx context.Context, opts ...ValidationOption) error { - // ctx = WithValidationOptions(ctx, opts...) + ctx = WithValidationOptions(ctx, opts...) - return nil // TODO + return validateExtensions(ctx, xml.Extensions) } diff --git a/openapi3filter/issue707_test.go b/openapi3filter/issue707_test.go index c0dbe6462..a7cbc39ed 100644 --- a/openapi3filter/issue707_test.go +++ b/openapi3filter/issue707_test.go @@ -15,27 +15,26 @@ func TestIssue707(t *testing.T) { loader := openapi3.NewLoader() ctx := loader.Context spec := ` - openapi: 3.0.0 - info: - version: 1.0.0 - title: Sample API - paths: - /items: - get: - description: Returns a list of stuff - parameters: - - description: parameter with a default value - explode: true - in: query - name: param-with-default - schema: - default: 124 - type: integer - style: form - required: false - responses: +openapi: 3.0.0 +info: + version: 1.0.0 + title: Sample API +paths: + /items: + get: + description: Returns a list of stuff + parameters: + - description: parameter with a default value + explode: true + in: query + name: param-with-default + schema: + default: 124 + type: integer + required: false + responses: '200': - description: Successful response + description: Successful response `[1:] doc, err := loader.LoadFromData([]byte(spec)) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 2cd700cd1..9381a27fd 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -1125,8 +1125,7 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S if len(schema.Value.AllOf) > 0 { var exists bool for _, sr := range schema.Value.AllOf { - valueSchema, exists = sr.Value.Properties[name] - if exists { + if valueSchema, exists = sr.Value.Properties[name]; exists { break } } @@ -1137,10 +1136,8 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S // If the property's schema has type "array" it is means that the form contains a few parts with the same name. // Every such part has a type that is defined by an items schema in the property's schema. var exists bool - valueSchema, exists = schema.Value.Properties[name] - if !exists { - anyProperties := schema.Value.AdditionalPropertiesAllowed - if anyProperties != nil { + if valueSchema, exists = schema.Value.Properties[name]; !exists { + if anyProperties := schema.Value.AdditionalProperties.Has; anyProperties != nil { switch *anyProperties { case true: //additionalProperties: true @@ -1150,11 +1147,10 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } } - if schema.Value.AdditionalProperties == nil { + if schema.Value.AdditionalProperties.Schema == nil { return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } - valueSchema, exists = schema.Value.AdditionalProperties.Value.Properties[name] - if !exists { + if valueSchema, exists = schema.Value.AdditionalProperties.Schema.Value.Properties[name]; !exists { return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } } @@ -1179,8 +1175,8 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S for k, v := range sr.Value.Properties { allTheProperties[k] = v } - if sr.Value.AdditionalProperties != nil { - for k, v := range sr.Value.AdditionalProperties.Value.Properties { + if addProps := sr.Value.AdditionalProperties.Schema; addProps != nil { + for k, v := range addProps.Value.Properties { allTheProperties[k] = v } } @@ -1189,8 +1185,8 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S for k, v := range schema.Value.Properties { allTheProperties[k] = v } - if schema.Value.AdditionalProperties != nil { - for k, v := range schema.Value.AdditionalProperties.Value.Properties { + if addProps := schema.Value.AdditionalProperties.Schema; addProps != nil { + for k, v := range addProps.Value.Properties { allTheProperties[k] = v } } diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index a61c57a09..7245cbe03 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -346,10 +346,6 @@ func ValidateSecurityRequirements(ctx context.Context, input *RequestValidationI // validateSecurityRequirement validates a single OpenAPI 3 security requirement func validateSecurityRequirement(ctx context.Context, input *RequestValidationInput, securityRequirement openapi3.SecurityRequirement) error { - doc := input.Route.Spec - securitySchemes := doc.Components.SecuritySchemes - - // Ensure deterministic order names := make([]string, 0, len(securityRequirement)) for name := range securityRequirement { names = append(names, name) @@ -366,6 +362,11 @@ func validateSecurityRequirement(ctx context.Context, input *RequestValidationIn return ErrAuthenticationServiceMissing } + var securitySchemes openapi3.SecuritySchemes + if components := input.Route.Spec.Components; components != nil { + securitySchemes = components.SecuritySchemes + } + // For each scheme for the requirement for _, name := range names { var securityScheme *openapi3.SecurityScheme diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index cdbeb1262..d3a1b45bb 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -541,7 +541,7 @@ func TestRootSecurityRequirementsAreUsedIfNotProvidedAtTheOperationLevel(t *test securitySchemes[1].Name: {}, }, }, - Components: openapi3.Components{ + Components: &openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, } @@ -670,7 +670,7 @@ func TestAnySecurityRequirementMet(t *testing.T) { Version: "0.1", }, Paths: map[string]*openapi3.PathItem{}, - Components: openapi3.Components{ + Components: &openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, } @@ -767,7 +767,7 @@ func TestAllSchemesMet(t *testing.T) { Version: "0.1", }, Paths: map[string]*openapi3.PathItem{}, - Components: openapi3.Components{ + Components: &openapi3.Components{ SecuritySchemes: map[string]*openapi3.SecuritySchemeRef{}, }, } diff --git a/jsoninfo/field_info.go b/openapi3gen/field_info.go similarity index 78% rename from jsoninfo/field_info.go rename to openapi3gen/field_info.go index 6b45f8c69..13f5ba048 100644 --- a/jsoninfo/field_info.go +++ b/openapi3gen/field_info.go @@ -1,4 +1,4 @@ -package jsoninfo +package openapi3gen import ( "reflect" @@ -7,9 +7,8 @@ import ( "unicode/utf8" ) -// FieldInfo contains information about JSON serialization of a field. -type FieldInfo struct { - MultipleFields bool // Whether multiple Go fields share this JSON name +// theFieldInfo contains information about JSON serialization of a field. +type theFieldInfo struct { HasJSONTag bool TypeIsMarshaller bool TypeIsUnmarshaller bool @@ -20,7 +19,7 @@ type FieldInfo struct { JSONName string } -func AppendFields(fields []FieldInfo, parentIndex []int, t reflect.Type) []FieldInfo { +func appendFields(fields []theFieldInfo, parentIndex []int, t reflect.Type) []theFieldInfo { if t.Kind() == reflect.Ptr { t = t.Elem() } @@ -40,7 +39,7 @@ iteration: continue } if jsonTag == "" { - fields = AppendFields(fields, index, f.Type) + fields = appendFields(fields, index, f.Type) continue iteration } } @@ -58,7 +57,7 @@ iteration: } // Declare a field - field := FieldInfo{ + field := theFieldInfo{ Index: index, Type: f.Type, JSONName: f.Name, @@ -67,13 +66,6 @@ iteration: // Read "json" tag jsonTag := f.Tag.Get("json") - // Read our custom "multijson" tag that - // allows multiple fields with the same name. - if v := f.Tag.Get("multijson"); v != "" { - field.MultipleFields = true - jsonTag = v - } - // Handle "-" if jsonTag == "-" { continue @@ -108,7 +100,7 @@ iteration: return fields } -type sortableFieldInfos []FieldInfo +type sortableFieldInfos []theFieldInfo func (list sortableFieldInfos) Len() int { return len(list) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 4387727f7..eccabd85d 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -9,7 +9,6 @@ import ( "strings" "time" - "github.com/getkin/kin-openapi/jsoninfo" "github.com/getkin/kin-openapi/openapi3" ) @@ -119,7 +118,7 @@ func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Sch return ref, nil } -func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { +func (g *Generator) generateSchemaRefFor(parents []*theTypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { if ref := g.Types[t]; ref != nil && g.opts.schemaCustomizer == nil { g.SchemaRefs[ref]++ return ref, nil @@ -139,7 +138,7 @@ func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect return ref, nil } -func getStructField(t reflect.Type, fieldInfo jsoninfo.FieldInfo) reflect.StructField { +func getStructField(t reflect.Type, fieldInfo theFieldInfo) reflect.StructField { var ff reflect.StructField // fieldInfo.Index is an array of indexes starting from the root of the type for i := 0; i < len(fieldInfo.Index); i++ { @@ -152,8 +151,8 @@ func getStructField(t reflect.Type, fieldInfo jsoninfo.FieldInfo) reflect.Struct return ff } -func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { - typeInfo := jsoninfo.GetTypeInfo(t) +func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) { + typeInfo := getTypeInfo(t) for _, parent := range parents { if parent == typeInfo { return nil, &CycleError{} @@ -161,7 +160,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } if cap(parents) == 0 { - parents = make([]*jsoninfo.TypeInfo, 0, 4) + parents = make([]*theTypeInfo, 0, 4) } parents = append(parents, typeInfo) @@ -284,7 +283,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec } if additionalProperties != nil { g.SchemaRefs[additionalProperties]++ - schema.AdditionalProperties = additionalProperties + schema.AdditionalProperties = openapi3.AdditionalProperties{Schema: additionalProperties} } case reflect.Struct: @@ -374,7 +373,7 @@ func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Sche ref := g.generateCycleSchemaRef(t.Elem(), schema) mapSchema := openapi3.NewSchema() mapSchema.Type = "object" - mapSchema.AdditionalProperties = ref + mapSchema.AdditionalProperties = openapi3.AdditionalProperties{Schema: ref} return openapi3.NewSchemaRef("", mapSchema) default: typeName = t.Name() diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index bfa3120ec..9a143e415 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -379,7 +379,7 @@ func TestCyclicReferences(t *testing.T) { require.NotNil(t, schemaRef.Value.Properties["MapCycle"]) require.Equal(t, "object", schemaRef.Value.Properties["MapCycle"].Value.Type) - require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Ref) + require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Schema.Ref) } func ExampleSchemaCustomizer() { diff --git a/openapi3gen/type_info.go b/openapi3gen/type_info.go new file mode 100644 index 000000000..062882b4c --- /dev/null +++ b/openapi3gen/type_info.go @@ -0,0 +1,54 @@ +package openapi3gen + +import ( + "reflect" + "sort" + "sync" +) + +var ( + typeInfos = map[reflect.Type]*theTypeInfo{} + typeInfosMutex sync.RWMutex +) + +// theTypeInfo contains information about JSON serialization of a type +type theTypeInfo struct { + Type reflect.Type + Fields []theFieldInfo +} + +// getTypeInfo returns theTypeInfo for the given type. +func getTypeInfo(t reflect.Type) *theTypeInfo { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + typeInfosMutex.RLock() + typeInfo, exists := typeInfos[t] + typeInfosMutex.RUnlock() + if exists { + return typeInfo + } + if t.Kind() != reflect.Struct { + typeInfo = &theTypeInfo{ + Type: t, + } + } else { + // Allocate + typeInfo = &theTypeInfo{ + Type: t, + Fields: make([]theFieldInfo, 0, 16), + } + + // Add fields + typeInfo.Fields = appendFields(nil, nil, t) + + // Sort fields + sort.Sort(sortableFieldInfos(typeInfo.Fields)) + } + + // Publish + typeInfosMutex.Lock() + typeInfos[t] = typeInfo + typeInfosMutex.Unlock() + return typeInfo +} diff --git a/refs.sh b/refs.sh new file mode 100755 index 000000000..bbcbe54bc --- /dev/null +++ b/refs.sh @@ -0,0 +1,125 @@ +#!/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 <