From af90e9a5a20fd826d5289fcfb6063fc752400fbf Mon Sep 17 00:00:00 2001 From: Tommy PAGEARD Date: Fri, 2 Aug 2024 18:02:53 +0200 Subject: [PATCH 1/5] openapi3filter: Fix default value for array in for query param (#1000) --- openapi3filter/issue991_test.go | 119 ++++++++++++++++++++++++ openapi3filter/validate_request.go | 6 +- openapi3filter/validate_request_test.go | 91 ------------------ 3 files changed, 120 insertions(+), 96 deletions(-) create mode 100644 openapi3filter/issue991_test.go diff --git a/openapi3filter/issue991_test.go b/openapi3filter/issue991_test.go new file mode 100644 index 00000000..a81b5b5f --- /dev/null +++ b/openapi3filter/issue991_test.go @@ -0,0 +1,119 @@ +package openapi3filter + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateRequestDefault(t *testing.T) { + const spec = ` +openapi: 3.0.0 +info: + title: 'Validator' + version: 0.0.1 +paths: + /category: + get: + parameters: + - $ref: "#/components/parameters/Type" + responses: + '200': + description: Ok +components: + parameters: + Type: + in: query + name: type + required: false + description: Type parameter + schema: + type: array + default: + - A + - B + - C + items: + type: string + enum: + - A + - B + - C +` + + router := setupTestRouter(t, spec) + + type args struct { + url string + expected []string + } + tests := []struct { + name string + args args + expectedModification bool + expectedErr error + }{ + { + name: "Valid request without type parameters set", + args: args{ + url: "/category", + expected: []string{"A", "B", "C"}, + }, + expectedModification: false, + expectedErr: nil, + }, + { + name: "Valid request with 1 type parameters set", + args: args{ + url: "/category?type=A", + expected: []string{"A"}, + }, + expectedModification: false, + expectedErr: nil, + }, + { + name: "Valid request with 2 type parameters set", + args: args{ + url: "/category?type=A&type=C", + expected: []string{"A", "C"}, + }, + expectedModification: false, + expectedErr: nil, + }, + { + name: "Valid request with 1 type parameters set out of enum", + args: args{ + url: "/category?type=X", + expected: nil, + }, + expectedModification: false, + expectedErr: &RequestError{}, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + + req, err := http.NewRequest(http.MethodGet, tc.args.url, nil) + require.NoError(t, err) + + route, pathParams, err := router.FindRoute(req) + require.NoError(t, err) + + validationInput := &RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + } + err = ValidateRequest(context.Background(), validationInput) + assert.IsType(t, tc.expectedErr, err, "ValidateRequest(): error = %v, expectedError %v", err, tc.expectedErr) + if tc.expectedErr != nil { + return + } + + assert.Equal(t, tc.args.expected, req.URL.Query()["type"], "ValidateRequest(): query parameter type values do not match expected") + }) + } +} diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 296403c9..3892a39a 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -114,11 +114,7 @@ func appendToQueryValues[T any](q url.Values, parameterName string, v []T) { // populateDefaultQueryParameters populates default values inside query parameters, while ensuring types are respected func populateDefaultQueryParameters(q url.Values, parameterName string, value any) { switch t := value.(type) { - case []string: - appendToQueryValues(q, parameterName, t) - case []float64: - appendToQueryValues(q, parameterName, t) - case []int: + case []interface{}: appendToQueryValues(q, parameterName, t) default: q.Add(parameterName, fmt.Sprintf("%v", value)) diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go index 505e5cb9..474fab36 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -567,94 +567,3 @@ paths: }) require.Error(t, err) } - -var ( - StringArraySchemaWithDefault = &openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Type: &openapi3.Types{"array"}, - Items: stringSchema, - Default: []string{"A", "B", "C"}, - }, - } - FloatArraySchemaWithDefault = &openapi3.SchemaRef{ - Value: &openapi3.Schema{ - Type: &openapi3.Types{"array"}, - Items: numberSchema, - Default: []float64{1.5, 2.5, 3.5}, - }, - } -) - -func TestValidateRequestDefault(t *testing.T) { - type testCase struct { - name string - param *openapi3.Parameter - query string - wantQuery map[string][]string - wantHeader map[string]any - } - - testCases := []testCase{ - { - name: "String Array In Query", - param: &openapi3.Parameter{ - Name: "param", In: "query", Style: "form", Explode: explode, - Schema: StringArraySchemaWithDefault, - }, - wantQuery: map[string][]string{ - "param": { - "A", - "B", - "C", - }, - }, - }, - { - name: "Float Array In Query", - param: &openapi3.Parameter{ - Name: "param", In: "query", Style: "form", Explode: explode, - Schema: FloatArraySchemaWithDefault, - }, - wantQuery: map[string][]string{ - "param": { - "1.5", - "2.5", - "3.5", - }, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - info := &openapi3.Info{ - Title: "MyAPI", - Version: "0.1", - } - doc := &openapi3.T{OpenAPI: "3.0.0", Info: info, Paths: openapi3.NewPaths()} - op := &openapi3.Operation{ - OperationID: "test", - Parameters: []*openapi3.ParameterRef{{Value: tc.param}}, - Responses: openapi3.NewResponses(), - } - doc.AddOperation("/test", http.MethodGet, op) - err := doc.Validate(context.Background()) - require.NoError(t, err) - router, err := legacyrouter.NewRouter(doc) - require.NoError(t, err) - - req, err := http.NewRequest(http.MethodGet, "http://test.org/test?"+tc.query, nil) - route, pathParams, err := router.FindRoute(req) - require.NoError(t, err) - - input := &RequestValidationInput{Request: req, PathParams: pathParams, Route: route} - - err = ValidateParameter(context.Background(), input, tc.param) - require.NoError(t, err) - - for k, v := range tc.wantQuery { - require.Equal(t, v, input.Request.URL.Query()[k]) - } - }) - } -} From b77138045f2110db5fbcf70e96336e610bb24721 Mon Sep 17 00:00:00 2001 From: Jille Timmermans Date: Thu, 22 Aug 2024 14:26:31 +0200 Subject: [PATCH 2/5] Add github.com/pb33f/libopenapi (#1004) * Add github.com/pb33f/libopenapi it looks like a reasonable alternative * Update README.md --------- Co-authored-by: Pierre Fenoll --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0709ae7c..690ef903 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Here's some projects that depend on _kin-openapi_: * (Feel free to add your project by [creating an issue](https://github.com/getkin/kin-openapi/issues/new) or a pull request) ## Alternatives +* [libopenapi](https://github.com/pb33f/libopenapi) a fully featured, high performance OpenAPI 3.1, 3.0 and Swagger parser, library, validator and toolkit * [go-swagger](https://github.com/go-swagger/go-swagger) stated [*OpenAPIv3 won't be supported*](https://github.com/go-swagger/go-swagger/issues/1122#issuecomment-575968499) * [swaggo](https://github.com/swaggo/swag) has an [open issue on OpenAPIv3](https://github.com/swaggo/swag/issues/386) * [go-openapi](https://github.com/go-openapi)'s [spec3](https://github.com/go-openapi/spec3) From 2a0cad9ebfd6a2e57c4dc7557a6c344e28809b23 Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Mon, 26 Aug 2024 13:40:55 +0200 Subject: [PATCH 3/5] Introduce an option to override the regex implementation (#1006) --- .github/docs/openapi3.txt | 14 ++++++++++++++ .github/docs/openapi3filter.txt | 3 +++ openapi3/schema.go | 7 +++---- openapi3/schema_pattern.go | 10 ++++++++-- openapi3/schema_validation_settings.go | 13 +++++++++++++ openapi3/validation_options.go | 9 +++++++++ openapi3filter/options.go | 3 +++ openapi3filter/validate_request.go | 3 +++ 8 files changed, 56 insertions(+), 6 deletions(-) diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 09f1192a..cfccb97a 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -1240,6 +1240,12 @@ type RefNameResolver func(*T, ComponentRef) string The function should avoid name collisions (i.e. be a injective mapping). It must only contain characters valid for fixed field names: IdentifierRegExp. +type RegexCompilerFunc func(expr string) (RegexMatcher, error) + +type RegexMatcher interface { + MatchString(s string) bool +} + type RequestBodies map[string]*RequestBodyRef func (m RequestBodies) JSONLookup(token string) (any, error) @@ -1764,6 +1770,10 @@ func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaVali If the passed function returns an empty string, it returns to the previous Error() implementation. +func SetSchemaRegexCompiler(c RegexCompilerFunc) SchemaValidationOption + SetSchemaRegexCompiler allows to override the regex implementation used to + validate field "pattern". + func VisitAsRequest() SchemaValidationOption func VisitAsResponse() SchemaValidationOption @@ -2128,6 +2138,10 @@ func ProhibitExtensionsWithRef() ValidationOption fields. Non-extension fields are prohibited unless allowed explicitly with the AllowExtraSiblingFields option. +func SetRegexCompiler(c RegexCompilerFunc) ValidationOption + SetRegexCompiler allows to override the regex implementation used to + validate field "pattern". + type ValidationOptions struct { // Has unexported fields. } diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt index 626cd24a..d44ba5ab 100644 --- a/.github/docs/openapi3filter.txt +++ b/.github/docs/openapi3filter.txt @@ -223,6 +223,9 @@ type Options struct { MultiError bool + // Set RegexCompiler to override the regex implementation + RegexCompiler openapi3.RegexCompilerFunc + // A document with security schemes defined will not pass validation // unless an AuthenticationFunc is defined. // See NoopAuthenticationFunc diff --git a/openapi3/schema.go b/openapi3/schema.go index 7be6bd38..f8119606 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -9,7 +9,6 @@ import ( "math" "math/big" "reflect" - "regexp" "sort" "strconv" "strings" @@ -1019,7 +1018,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, } } if !validationOpts.schemaPatternValidationDisabled && schema.Pattern != "" { - if _, err := schema.compilePattern(); err != nil { + if _, err := schema.compilePattern(validationOpts.regexCompilerFunc); err != nil { return stack, err } } @@ -1729,10 +1728,10 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value // "pattern" if !settings.patternValidationDisabled && schema.Pattern != "" { cpiface, _ := compiledPatterns.Load(schema.Pattern) - cp, _ := cpiface.(*regexp.Regexp) + cp, _ := cpiface.(RegexMatcher) if cp == nil { var err error - if cp, err = schema.compilePattern(); err != nil { + if cp, err = schema.compilePattern(settings.regexCompiler); err != nil { if !settings.multiError { return err } diff --git a/openapi3/schema_pattern.go b/openapi3/schema_pattern.go index 4794b6a0..58197137 100644 --- a/openapi3/schema_pattern.go +++ b/openapi3/schema_pattern.go @@ -13,9 +13,14 @@ func intoGoRegexp(re string) string { } // NOTE: racey WRT [writes to schema.Pattern] vs [reads schema.Pattern then writes to compiledPatterns] -func (schema *Schema) compilePattern() (cp *regexp.Regexp, err error) { +func (schema *Schema) compilePattern(c RegexCompilerFunc) (cp RegexMatcher, err error) { pattern := schema.Pattern - if cp, err = regexp.Compile(intoGoRegexp(pattern)); err != nil { + if c != nil { + cp, err = c(pattern) + } else { + cp, err = regexp.Compile(intoGoRegexp(pattern)) + } + if err != nil { err = &SchemaError{ Schema: schema, SchemaField: "pattern", @@ -24,6 +29,7 @@ func (schema *Schema) compilePattern() (cp *regexp.Regexp, err error) { } return } + var _ bool = compiledPatterns.CompareAndSwap(pattern, nil, cp) return } diff --git a/openapi3/schema_validation_settings.go b/openapi3/schema_validation_settings.go index 17aad2fa..e9c1422b 100644 --- a/openapi3/schema_validation_settings.go +++ b/openapi3/schema_validation_settings.go @@ -7,6 +7,12 @@ import ( // SchemaValidationOption describes options a user has when validating request / response bodies. type SchemaValidationOption func(*schemaValidationSettings) +type RegexCompilerFunc func(expr string) (RegexMatcher, error) + +type RegexMatcher interface { + MatchString(s string) bool +} + type schemaValidationSettings struct { failfast bool multiError bool @@ -16,6 +22,8 @@ type schemaValidationSettings struct { readOnlyValidationDisabled bool writeOnlyValidationDisabled bool + regexCompiler RegexCompilerFunc + onceSettingDefaults sync.Once defaultsSet func() @@ -70,6 +78,11 @@ func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaVali return func(s *schemaValidationSettings) { s.customizeMessageError = f } } +// SetSchemaRegexCompiler allows to override the regex implementation used to validate field "pattern". +func SetSchemaRegexCompiler(c RegexCompilerFunc) SchemaValidationOption { + return func(s *schemaValidationSettings) { s.regexCompiler = c } +} + func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidationSettings { settings := &schemaValidationSettings{} for _, opt := range opts { diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 45563256..1d141d40 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -13,6 +13,7 @@ type ValidationOptions struct { schemaFormatValidationEnabled bool schemaPatternValidationDisabled bool schemaExtensionsInRefProhibited bool + regexCompilerFunc RegexCompilerFunc extraSiblingFieldsAllowed map[string]struct{} } @@ -113,6 +114,14 @@ func ProhibitExtensionsWithRef() ValidationOption { } } +// SetRegexCompiler allows to override the regex implementation used to validate +// field "pattern". +func SetRegexCompiler(c RegexCompilerFunc) ValidationOption { + return func(options *ValidationOptions) { + options.regexCompilerFunc = c + } +} + // WithValidationOptions allows adding validation options to a context object that can be used when validating any OpenAPI type. func WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context { if len(opts) == 0 { diff --git a/openapi3filter/options.go b/openapi3filter/options.go index 9b915c50..e7fad832 100644 --- a/openapi3filter/options.go +++ b/openapi3filter/options.go @@ -25,6 +25,9 @@ type Options struct { MultiError bool + // Set RegexCompiler to override the regex implementation + RegexCompiler openapi3.RegexCompilerFunc + // A document with security schemes defined will not pass validation // unless an AuthenticationFunc is defined. // See NoopAuthenticationFunc diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 3892a39a..bf4771a9 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -316,6 +316,9 @@ func ValidateRequestBody(ctx context.Context, input *RequestValidationInput, req if options.ExcludeReadOnlyValidations { opts = append(opts, openapi3.DisableReadOnlyValidation()) } + if options.RegexCompiler != nil { + opts = append(opts, openapi3.SetSchemaRegexCompiler(options.RegexCompiler)) + } // Validate JSON with the schema if err := contentType.Schema.Value.VisitJSON(value, opts...); err != nil { From c606b5546b122c29ebfc909cf0fa86611db80804 Mon Sep 17 00:00:00 2001 From: Justin Sherrill Date: Fri, 30 Aug 2024 07:37:39 -0400 Subject: [PATCH 4/5] make form required field order deterministic (#1008) --- openapi2conv/issue1008_test.go | 49 ++++++++++++++++++++++++++++++++++ openapi2conv/openapi2_conv.go | 1 + 2 files changed, 50 insertions(+) create mode 100644 openapi2conv/issue1008_test.go diff --git a/openapi2conv/issue1008_test.go b/openapi2conv/issue1008_test.go new file mode 100644 index 00000000..4d8dfe5e --- /dev/null +++ b/openapi2conv/issue1008_test.go @@ -0,0 +1,49 @@ +package openapi2conv + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIssue1008(t *testing.T) { + v2 := []byte(` +swagger: '2.0' +info: + version: '1.10' + title: title +paths: + "/ping": + post: + consumes: + - multipart/form-data + parameters: + - name: zebra + in: formData + description: stripes + required: true + type: string + - name: alpaca + in: formData + description: chewy + required: true + type: string + - name: bee + in: formData + description: buzz + required: true + type: string + responses: + '200': + description: OK +`) + + v3, err := v2v3YAML(v2) + require.NoError(t, err) + + err = v3.Validate(context.Background()) + require.NoError(t, err) + assert.Equal(t, []string{"alpaca", "bee", "zebra"}, v3.Paths.Value("/ping").Post.RequestBody.Value.Content.Get("multipart/form-data").Schema.Value.Required) +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 0a622f4f..fa161bca 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -346,6 +346,7 @@ func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool, c bodies[s] = ref } } + sort.Strings(requireds) schema := &openapi3.Schema{ Type: &openapi3.Types{"object"}, Properties: ToV3Schemas(bodies), From 1eeb41ca5a03ec81862d9135bcb0a09c6d7d7d2a Mon Sep 17 00:00:00 2001 From: Jay Shah Date: Tue, 1 Oct 2024 16:44:42 -0400 Subject: [PATCH 5/5] openapi2: fix un/marshalling discriminator field (#1011) * fix: issue unmarshalling when discriminator field is set in openapi2.0 * revert original approach * update with different approach * Revert "update with different approach" This reverts commit 2db2b3929adb6a9fc22c9b8300a5689ca4633d98. * v2 schema with discriminator field set as string * update ref link and comment * run docs.sh --- .github/docs/openapi2.txt | 191 ++++++++++++++++++------ .github/docs/openapi2conv.txt | 8 +- openapi2/helpers.go | 15 ++ openapi2/issues1010_test.go | 98 +++++++++++++ openapi2/openapi2.go | 30 ++-- openapi2/parameter.go | 46 +++--- openapi2/ref.go | 9 ++ openapi2/refs.go | 104 +++++++++++++ openapi2/response.go | 8 +- openapi2/schema.go | 269 ++++++++++++++++++++++++++++++++++ openapi2conv/openapi2_conv.go | 196 +++++++++++++++++++------ 11 files changed, 838 insertions(+), 136 deletions(-) create mode 100644 openapi2/helpers.go create mode 100644 openapi2/issues1010_test.go create mode 100644 openapi2/ref.go create mode 100644 openapi2/refs.go create mode 100644 openapi2/schema.go diff --git a/.github/docs/openapi2.txt b/.github/docs/openapi2.txt index aec6439d..9025f5a8 100644 --- a/.github/docs/openapi2.txt +++ b/.github/docs/openapi2.txt @@ -47,29 +47,29 @@ type Parameter struct { 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 *openapi3.Types `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 []any `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 any `json:"default,omitempty" yaml:"default,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 *openapi3.Types `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 *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` + Enum []any `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 any `json:"default,omitempty" yaml:"default,omitempty"` } func (parameter Parameter) MarshalJSON() ([]byte, error) @@ -113,15 +113,21 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) func (pathItem *PathItem) UnmarshalJSON(data []byte) error UnmarshalJSON sets PathItem to a copy of data. +type Ref struct { + Ref string `json:"$ref" yaml:"$ref"` +} + Ref is specified by OpenAPI/Swagger 2.0 standard. See + https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#reference-object + type Response struct { Extensions map[string]any `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]any `json:"examples,omitempty" yaml:"examples,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Examples map[string]any `json:"examples,omitempty" yaml:"examples,omitempty"` } func (response Response) MarshalJSON() ([]byte, error) @@ -130,6 +136,107 @@ func (response Response) MarshalJSON() ([]byte, error) func (response *Response) UnmarshalJSON(data []byte) error UnmarshalJSON sets Response to a copy of data. +type Schema struct { + Extensions map[string]any `json:"-" yaml:"-"` + + AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"` + Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"` + Type *openapi3.Types `json:"type,omitempty" yaml:"type,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` + ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + + // Array-related, here for struct compactness + UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + // Number-related, here for struct compactness + ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + // Properties + ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` + WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + XML *openapi3.XML `json:"xml,omitempty" yaml:"xml,omitempty"` + + // Number + Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + Max *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + + // String + MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + + // Array + MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + 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"` + AdditionalProperties openapi3.AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + Discriminator string `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` +} + Schema is specified by OpenAPI/Swagger 2.0 standard. See + https://swagger.io/specification/v2/#schema-object + +func (schema Schema) MarshalJSON() ([]byte, error) + MarshalJSON returns the JSON encoding of Schema. + +func (schema Schema) MarshalYAML() (any, error) + MarshalYAML returns the YAML encoding of Schema. + +func (schema *Schema) UnmarshalJSON(data []byte) error + UnmarshalJSON sets Schema to a copy of data. + +type SchemaRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + + Ref string + Value *Schema + + // Has unexported fields. +} + SchemaRef represents either a Schema or a $ref to a Schema. When serializing + and both fields are set, Ref is preferred over Value. + +func (x *SchemaRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *SchemaRef) JSONLookup(token string) (any, error) + JSONLookup implements + https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable + +func (x SchemaRef) MarshalJSON() ([]byte, error) + MarshalJSON returns the JSON encoding of SchemaRef. + +func (x SchemaRef) MarshalYAML() (any, error) + MarshalYAML returns the YAML encoding of SchemaRef. + +func (x *SchemaRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *SchemaRef) RefString() string + RefString returns the $ref value. + +func (x *SchemaRef) UnmarshalJSON(data []byte) error + UnmarshalJSON sets SchemaRef to a copy of data. + +type SchemaRefs []*SchemaRef + +type Schemas map[string]*SchemaRef + type SecurityRequirements []map[string][]string type SecurityScheme struct { @@ -157,21 +264,21 @@ func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error type T struct { Extensions map[string]any `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"` - Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` - Host string `json:"host,omitempty" yaml:"host,omitempty"` - BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` - Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` - Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"` - Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` - SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` - Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` - Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + 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"` + Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` + Host string `json:"host,omitempty" yaml:"host,omitempty"` + BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` + Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` + Definitions map[string]*SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"` + Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` + SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` + Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` + Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` } T is the root of an OpenAPI v2 document diff --git a/.github/docs/openapi2conv.txt b/.github/docs/openapi2conv.txt index 2f9f6dec..e24925aa 100644 --- a/.github/docs/openapi2conv.txt +++ b/.github/docs/openapi2conv.txt @@ -16,8 +16,8 @@ func FromV3RequestBody(name string, requestBodyRef *openapi3.RequestBodyRef, med func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameters func FromV3Response(ref *openapi3.ResponseRef, components *openapi3.Components) (*openapi2.Response, error) func FromV3Responses(responses map[string]*openapi3.ResponseRef, components *openapi3.Components) (map[string]*openapi2.Response, error) -func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter) -func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) +func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi2.SchemaRef, *openapi2.Parameter) +func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi2.SchemaRef, map[string]*openapi2.Parameter) func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements func FromV3SecurityScheme(doc3 *openapi3.T, ref *openapi3.SecuritySchemeRef) (*openapi2.SecurityScheme, error) func ToV3(doc2 *openapi2.T) (*openapi3.T, error) @@ -29,8 +29,8 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete func ToV3PathItem(doc2 *openapi2.T, components *openapi3.Components, pathItem *openapi2.PathItem, consumes []string) (*openapi3.PathItem, error) func ToV3Ref(ref string) string func ToV3Response(response *openapi2.Response, produces []string) (*openapi3.ResponseRef, error) -func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef -func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef +func ToV3SchemaRef(schema *openapi2.SchemaRef) *openapi3.SchemaRef +func ToV3Schemas(defs map[string]*openapi2.SchemaRef) map[string]*openapi3.SchemaRef func ToV3SecurityRequirements(requirements openapi2.SecurityRequirements) openapi3.SecurityRequirements func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.SecuritySchemeRef, error) func ToV3WithLoader(doc2 *openapi2.T, loader *openapi3.Loader, location *url.URL) (*openapi3.T, error) diff --git a/openapi2/helpers.go b/openapi2/helpers.go new file mode 100644 index 00000000..777a2f0b --- /dev/null +++ b/openapi2/helpers.go @@ -0,0 +1,15 @@ +package openapi2 + +import ( + "net/url" +) + +// copyURI makes a copy of the pointer. +func copyURI(u *url.URL) *url.URL { + if u == nil { + return nil + } + + c := *u // shallow-copy + return &c +} diff --git a/openapi2/issues1010_test.go b/openapi2/issues1010_test.go new file mode 100644 index 00000000..2f84dc94 --- /dev/null +++ b/openapi2/issues1010_test.go @@ -0,0 +1,98 @@ +package openapi2 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue1010(t *testing.T) { + v2 := []byte(` +{ + "basePath": "/v2", + "host": "test.example.com", + "info": { + "title": "MyAPI", + "version": "0.1", + "x-info": "info extension" + }, + "paths": { + "/foo": { + "get": { + "operationId": "getFoo", + "responses": { + "200": { + "description": "returns all information", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "default": { + "description": "OK" + } + }, + "summary": "get foo" + } + } + }, + "schemes": [ + "http" + ], + "swagger": "2.0", + "definitions": { + "Pet": { + "type": "object", + "required": ["petType"], + "properties": { + "petType": { + "type": "string" + }, + "name": { + "type": "string" + }, + "age": { + "type": "integer" + } + }, + "discriminator": "petType" + }, + "Dog": { + "allOf": [ + { + "$ref": "#/definitions/Pet" + }, + { + "type": "object", + "properties": { + "breed": { + "type": "string" + } + } + } + ] + }, + "Cat": { + "allOf": [ + { + "$ref": "#/definitions/Pet" + }, + { + "type": "object", + "properties": { + "color": { + "type": "string" + } + } + } + ] + } + } +} +`) + + var doc2 T + err := json.Unmarshal(v2, &doc2) + require.NoError(t, err) + require.Equal(t, "petType", doc2.Definitions["Pet"].Value.Discriminator) +} diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 2d922c63..bd337533 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -10,21 +10,21 @@ import ( type T struct { Extensions map[string]any `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"` - Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` - Host string `json:"host,omitempty" yaml:"host,omitempty"` - BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` - Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` - Definitions map[string]*openapi3.SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"` - Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` - SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` - Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` - Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` + 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"` + Produces []string `json:"produces,omitempty" yaml:"produces,omitempty"` + Host string `json:"host,omitempty" yaml:"host,omitempty"` + BasePath string `json:"basePath,omitempty" yaml:"basePath,omitempty"` + Paths map[string]*PathItem `json:"paths,omitempty" yaml:"paths,omitempty"` + Definitions map[string]*SchemaRef `json:"definitions,omitempty" yaml:"definitions,omitempty"` + Parameters map[string]*Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Responses map[string]*Response `json:"responses,omitempty" yaml:"responses,omitempty"` + SecurityDefinitions map[string]*SecurityScheme `json:"securityDefinitions,omitempty" yaml:"securityDefinitions,omitempty"` + Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"` + Tags openapi3.Tags `json:"tags,omitempty" yaml:"tags,omitempty"` } // MarshalJSON returns the JSON encoding of T. diff --git a/openapi2/parameter.go b/openapi2/parameter.go index c701705b..7f2bddb2 100644 --- a/openapi2/parameter.go +++ b/openapi2/parameter.go @@ -28,29 +28,29 @@ type Parameter struct { 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 *openapi3.Types `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 []any `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 any `json:"default,omitempty" yaml:"default,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 *openapi3.Types `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 *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` + Enum []any `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 any `json:"default,omitempty" yaml:"default,omitempty"` } // MarshalJSON returns the JSON encoding of Parameter. diff --git a/openapi2/ref.go b/openapi2/ref.go new file mode 100644 index 00000000..e591d143 --- /dev/null +++ b/openapi2/ref.go @@ -0,0 +1,9 @@ +package openapi2 + +//go:generate go run refsgenerator.go + +// Ref is specified by OpenAPI/Swagger 2.0 standard. +// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#reference-object +type Ref struct { + Ref string `json:"$ref" yaml:"$ref"` +} diff --git a/openapi2/refs.go b/openapi2/refs.go new file mode 100644 index 00000000..5109e988 --- /dev/null +++ b/openapi2/refs.go @@ -0,0 +1,104 @@ +package openapi2 + +import ( + "encoding/json" + "net/url" + "sort" + "strings" + + "github.com/go-openapi/jsonpointer" + "github.com/perimeterx/marshmallow" +) + +// SchemaRef represents either a Schema or a $ref to a Schema. +// When serializing and both fields are set, Ref is preferred over Value. +type SchemaRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + + Ref string + Value *Schema + extra []string + + refPath *url.URL +} + +var _ jsonpointer.JSONPointable = (*SchemaRef)(nil) + +func (x *SchemaRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } + +// RefString returns the $ref value. +func (x *SchemaRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *SchemaRef) CollectionName() string { return "schemas" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *SchemaRef) RefPath() *url.URL { return copyURI(x.refPath) } + +func (x *SchemaRef) setRefPath(u *url.URL) { + // Once the refPath is set don't override. References can be loaded + // multiple times not all with access to the correct path info. + if x.refPath != nil { + return + } + + x.refPath = copyURI(u) +} + +// MarshalYAML returns the YAML encoding of SchemaRef. +func (x SchemaRef) MarshalYAML() (any, error) { + if ref := x.Ref; ref != "" { + return &Ref{Ref: ref}, nil + } + return x.Value.MarshalYAML() +} + +// MarshalJSON returns the JSON encoding of SchemaRef. +func (x SchemaRef) MarshalJSON() ([]byte, error) { + y, err := x.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(y) +} + +// UnmarshalJSON sets SchemaRef to a copy of data. +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) + } + sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } + } + return nil + } + return json.Unmarshal(data, &x.Value) +} + +// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (x *SchemaRef) JSONLookup(token string) (any, error) { + if token == "$ref" { + return x.Ref, nil + } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + + ptr, _, err := jsonpointer.GetForToken(x.Value, token) + return ptr, err +} diff --git a/openapi2/response.go b/openapi2/response.go index 5306beb1..3a2983ce 100644 --- a/openapi2/response.go +++ b/openapi2/response.go @@ -11,10 +11,10 @@ type Response struct { 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]any `json:"examples,omitempty" yaml:"examples,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Examples map[string]any `json:"examples,omitempty" yaml:"examples,omitempty"` } // MarshalJSON returns the JSON encoding of Response. diff --git a/openapi2/schema.go b/openapi2/schema.go new file mode 100644 index 00000000..64bed275 --- /dev/null +++ b/openapi2/schema.go @@ -0,0 +1,269 @@ +package openapi2 + +import ( + "encoding/json" + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +type ( + Schemas map[string]*SchemaRef + SchemaRefs []*SchemaRef +) + +// Schema is specified by OpenAPI/Swagger 2.0 standard. +// See https://swagger.io/specification/v2/#schema-object +type Schema struct { + Extensions map[string]any `json:"-" yaml:"-"` + + AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"` + Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"` + Type *openapi3.Types `json:"type,omitempty" yaml:"type,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` + ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + + // Array-related, here for struct compactness + UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + // Number-related, here for struct compactness + ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + // Properties + ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` + WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + XML *openapi3.XML `json:"xml,omitempty" yaml:"xml,omitempty"` + + // Number + Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + Max *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + + // String + MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MaxLength *uint64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` + + // Array + MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + 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"` + AdditionalProperties openapi3.AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + Discriminator string `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` +} + +// MarshalJSON returns the JSON encoding of Schema. +func (schema Schema) MarshalJSON() ([]byte, error) { + m, err := schema.MarshalYAML() + if err != nil { + return nil, err + } + + return json.Marshal(m) +} + +// MarshalYAML returns the YAML encoding of Schema. +func (schema Schema) MarshalYAML() (any, error) { + m := make(map[string]any, 36+len(schema.Extensions)) + for k, v := range schema.Extensions { + m[k] = v + } + + if x := schema.AllOf; len(x) != 0 { + m["allOf"] = x + } + if x := schema.Not; x != nil { + m["not"] = x + } + if x := schema.Type; x != nil { + 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 + } + 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 != "" { + m["discriminator"] = x + } + + return m, nil +} + +// UnmarshalJSON sets Schema to a copy of data. +func (schema *Schema) UnmarshalJSON(data []byte) error { + type SchemaBis Schema + var x SchemaBis + if err := json.Unmarshal(data, &x); err != nil { + return unmarshalError(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") + + if len(x.Extensions) == 0 { + x.Extensions = nil + } + + *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 nil +} diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index fa161bca..ef0a5edd 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -272,7 +272,6 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete MinLength: parameter.MinLength, MaxLength: parameter.MaxLength, Default: parameter.Default, - Items: parameter.Items, MinItems: parameter.MinItems, MaxItems: parameter.MaxItems, Pattern: parameter.Pattern, @@ -281,6 +280,10 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete MultipleOf: parameter.MultipleOf, Required: required, }} + if parameter.Items != nil { + schemaRef.Value.Items = ToV3SchemaRef(parameter.Items) + } + schemaRefMap := make(map[string]*openapi3.SchemaRef, 1) schemaRefMap[parameter.Name] = schemaRef return nil, nil, schemaRefMap, nil @@ -301,7 +304,7 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete Description: parameter.Description, Required: required, Extensions: stripNonExtensions(parameter.Extensions), - Schema: ToV3SchemaRef(&openapi3.SchemaRef{Value: &openapi3.Schema{ + Schema: ToV3SchemaRef(&openapi2.SchemaRef{Value: &openapi2.Schema{ Type: parameter.Type, Format: parameter.Format, Enum: parameter.Enum, @@ -349,7 +352,7 @@ func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool, c sort.Strings(requireds) schema := &openapi3.Schema{ Type: &openapi3.Types{"object"}, - Properties: ToV3Schemas(bodies), + Properties: bodies, Required: requireds, } return &openapi3.RequestBodyRef{ @@ -456,7 +459,7 @@ func ToV3Headers(defs map[string]*openapi2.Header) openapi3.Headers { return headers } -func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.SchemaRef { +func ToV3Schemas(defs map[string]*openapi2.SchemaRef) map[string]*openapi3.SchemaRef { schemas := make(map[string]*openapi3.SchemaRef, len(defs)) for name, schema := range defs { schemas[name] = ToV3SchemaRef(schema) @@ -464,34 +467,84 @@ func ToV3Schemas(defs map[string]*openapi3.SchemaRef) map[string]*openapi3.Schem return schemas } -func ToV3SchemaRef(schema *openapi3.SchemaRef) *openapi3.SchemaRef { +func ToV3SchemaRef(schema *openapi2.SchemaRef) *openapi3.SchemaRef { + if schema == nil { + return &openapi3.SchemaRef{} + } + if ref := schema.Ref; ref != "" { return &openapi3.SchemaRef{Ref: ToV3Ref(ref)} } + if schema.Value == nil { - return schema + return &openapi3.SchemaRef{ + Extensions: schema.Extensions, + } + } + + v3Schema := &openapi3.Schema{ + Extensions: schema.Extensions, + Type: schema.Value.Type, + Title: schema.Value.Title, + Format: schema.Value.Format, + Description: schema.Value.Description, + Enum: schema.Value.Enum, + Default: schema.Value.Default, + Example: schema.Value.Example, + ExternalDocs: schema.Value.ExternalDocs, + UniqueItems: schema.Value.UniqueItems, + ExclusiveMin: schema.Value.ExclusiveMin, + ExclusiveMax: schema.Value.ExclusiveMax, + ReadOnly: schema.Value.ReadOnly, + WriteOnly: schema.Value.WriteOnly, + AllowEmptyValue: schema.Value.AllowEmptyValue, + Deprecated: schema.Value.Deprecated, + XML: schema.Value.XML, + Min: schema.Value.Min, + Max: schema.Value.Max, + MultipleOf: schema.Value.MultipleOf, + MinLength: schema.Value.MinLength, + MaxLength: schema.Value.MaxLength, + Pattern: schema.Value.Pattern, + MinItems: schema.Value.MinItems, + MaxItems: schema.Value.MaxItems, + Required: schema.Value.Required, + MinProps: schema.Value.MinProps, + MaxProps: schema.Value.MaxProps, + AllOf: make(openapi3.SchemaRefs, len(schema.Value.AllOf)), + Properties: make(openapi3.Schemas), + AdditionalProperties: schema.Value.AdditionalProperties, + } + + if schema.Value.Discriminator != "" { + v3Schema.Discriminator = &openapi3.Discriminator{ + PropertyName: schema.Value.Discriminator, + } } + if schema.Value.Items != nil { - schema.Value.Items = ToV3SchemaRef(schema.Value.Items) + v3Schema.Items = ToV3SchemaRef(schema.Value.Items) } if schema.Value.Type.Is("file") { - schema.Value.Format, schema.Value.Type = "binary", &openapi3.Types{"string"} + v3Schema.Format, v3Schema.Type = "binary", &openapi3.Types{"string"} } for k, v := range schema.Value.Properties { - schema.Value.Properties[k] = ToV3SchemaRef(v) - } - if v := schema.Value.AdditionalProperties.Schema; v != nil { - schema.Value.AdditionalProperties.Schema = ToV3SchemaRef(v) + v3Schema.Properties[k] = ToV3SchemaRef(v) } for i, v := range schema.Value.AllOf { - schema.Value.AllOf[i] = ToV3SchemaRef(v) + v3Schema.AllOf[i] = ToV3SchemaRef(v) } if val, ok := schema.Value.Extensions["x-nullable"]; ok { - schema.Value.Nullable, _ = val.(bool) - delete(schema.Value.Extensions, "x-nullable") + if nullable, valid := val.(bool); valid { + v3Schema.Nullable = nullable + delete(v3Schema.Extensions, "x-nullable") + } } - return schema + return &openapi3.SchemaRef{ + Extensions: schema.Extensions, + Value: v3Schema, + } } var ref2To3 = map[string]string{ @@ -742,8 +795,8 @@ func fromV3RequestBodies(name string, requestBodyRef *openapi3.RequestBodyRef, c return } -func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi3.SchemaRef, map[string]*openapi2.Parameter) { - v2Defs := make(map[string]*openapi3.SchemaRef) +func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3.Components) (map[string]*openapi2.SchemaRef, map[string]*openapi2.Parameter) { + v2Defs := make(map[string]*openapi2.SchemaRef) v2Params := make(map[string]*openapi2.Parameter) for name, schema := range schemas { schemaConv, parameterConv := FromV3SchemaRef(schema, components) @@ -759,7 +812,7 @@ func FromV3Schemas(schemas map[string]*openapi3.SchemaRef, components *openapi3. return v2Defs, v2Params } -func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi3.SchemaRef, *openapi2.Parameter) { +func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components) (*openapi2.SchemaRef, *openapi2.Parameter) { if ref := schema.Ref; ref != "" { name := getParameterNameFromNewRef(ref) if val, ok := components.Schemas[name]; ok { @@ -769,10 +822,12 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components } } - return &openapi3.SchemaRef{Ref: FromV3Ref(ref)}, nil + return &openapi2.SchemaRef{Ref: FromV3Ref(ref)}, nil } if schema.Value == nil { - return schema, nil + return &openapi2.SchemaRef{ + Extensions: schema.Extensions, + }, nil } if schema.Value != nil { @@ -789,19 +844,19 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components } } return nil, &openapi2.Parameter{ - In: "formData", - Name: originalName, - Description: schema.Value.Description, - Type: paramType, - Enum: schema.Value.Enum, - Minimum: schema.Value.Min, - Maximum: schema.Value.Max, - ExclusiveMin: schema.Value.ExclusiveMin, - ExclusiveMax: schema.Value.ExclusiveMax, - MinLength: schema.Value.MinLength, - MaxLength: schema.Value.MaxLength, - Default: schema.Value.Default, - Items: schema.Value.Items, + In: "formData", + Name: originalName, + Description: schema.Value.Description, + Type: paramType, + Enum: schema.Value.Enum, + Minimum: schema.Value.Min, + Maximum: schema.Value.Max, + ExclusiveMin: schema.Value.ExclusiveMin, + ExclusiveMax: schema.Value.ExclusiveMax, + MinLength: schema.Value.MinLength, + MaxLength: schema.Value.MaxLength, + Default: schema.Value.Default, + // Items: schema.Value.Items, MinItems: schema.Value.MinItems, MaxItems: schema.Value.MaxItems, AllowEmptyValue: schema.Value.AllowEmptyValue, @@ -812,32 +867,72 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components } } } + + v2Schema := &openapi2.Schema{ + Extensions: schema.Value.Extensions, + Type: schema.Value.Type, + Title: schema.Value.Title, + Format: schema.Value.Format, + Description: schema.Value.Description, + Enum: schema.Value.Enum, + Default: schema.Value.Default, + Example: schema.Value.Example, + ExternalDocs: schema.Value.ExternalDocs, + UniqueItems: schema.Value.UniqueItems, + ExclusiveMin: schema.Value.ExclusiveMin, + ExclusiveMax: schema.Value.ExclusiveMax, + ReadOnly: schema.Value.ReadOnly, + WriteOnly: schema.Value.WriteOnly, + AllowEmptyValue: schema.Value.AllowEmptyValue, + Deprecated: schema.Value.Deprecated, + XML: schema.Value.XML, + Min: schema.Value.Min, + Max: schema.Value.Max, + MultipleOf: schema.Value.MultipleOf, + MinLength: schema.Value.MinLength, + MaxLength: schema.Value.MaxLength, + Pattern: schema.Value.Pattern, + MinItems: schema.Value.MinItems, + MaxItems: schema.Value.MaxItems, + Required: schema.Value.Required, + MinProps: schema.Value.MinProps, + MaxProps: schema.Value.MaxProps, + Properties: make(openapi2.Schemas), + AllOf: make(openapi2.SchemaRefs, len(schema.Value.AllOf)), + AdditionalProperties: schema.Value.AdditionalProperties, + } + if v := schema.Value.Items; v != nil { - schema.Value.Items, _ = FromV3SchemaRef(v, components) + v2Schema.Items, _ = FromV3SchemaRef(v, components) } + keys := make([]string, 0, len(schema.Value.Properties)) for k := range schema.Value.Properties { keys = append(keys, k) } sort.Strings(keys) for _, key := range keys { - schema.Value.Properties[key], _ = FromV3SchemaRef(schema.Value.Properties[key], components) - } - if v := schema.Value.AdditionalProperties.Schema; v != nil { - schema.Value.AdditionalProperties.Schema, _ = FromV3SchemaRef(v, components) + property, _ := FromV3SchemaRef(schema.Value.Properties[key], components) + if property != nil { + v2Schema.Properties[key] = property + } } + for i, v := range schema.Value.AllOf { - schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components) + v2Schema.AllOf[i], _ = FromV3SchemaRef(v, components) } if schema.Value.PermitsNull() { schema.Value.Nullable = false if schema.Value.Extensions == nil { - schema.Value.Extensions = make(map[string]any) + v2Schema.Extensions = make(map[string]any) } - schema.Value.Extensions["x-nullable"] = true + v2Schema.Extensions["x-nullable"] = true } - return schema, nil + return &openapi2.SchemaRef{ + Extensions: schema.Extensions, + Value: v2Schema, + }, nil } func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) openapi2.SecurityRequirements { @@ -906,6 +1001,11 @@ func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameter break } } + + var v2Items *openapi2.SchemaRef + if val.Items != nil { + v2Items, _ = FromV3SchemaRef(val.Items, nil) + } parameter := &openapi2.Parameter{ Name: propName, Description: val.Description, @@ -918,7 +1018,7 @@ func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameter MinLength: val.MinLength, MaxLength: val.MaxLength, Default: val.Default, - Items: val.Items, + Items: v2Items, MinItems: val.MinItems, MaxItems: val.MaxItems, Maximum: val.Max, @@ -1029,12 +1129,12 @@ func FromV3Parameter(ref *openapi3.ParameterRef, components *openapi3.Components Extensions: stripNonExtensions(parameter.Extensions), } if schemaRef := parameter.Schema; schemaRef != nil { - schemaRef, _ = FromV3SchemaRef(schemaRef, components) - if ref := schemaRef.Ref; ref != "" { - result.Schema = &openapi3.SchemaRef{Ref: FromV3Ref(ref)} + schemaRefV2, _ := FromV3SchemaRef(schemaRef, components) + if ref := schemaRefV2.Ref; ref != "" { + result.Schema = &openapi2.SchemaRef{Ref: FromV3Ref(ref)} return result, nil } - schema := schemaRef.Value + schema := schemaRefV2.Value result.Type = schema.Type result.Format = schema.Format result.Enum = schema.Enum