diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index cfccb97a..47aaeadf 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -35,10 +35,10 @@ const ( FormatOfStringByte = `(^$|^[a-zA-Z0-9+/\-_]*=*$)` // FormatOfStringDate is a RFC3339 date format regexp, for example "2017-07-21". - FormatOfStringDate = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$` + FormatOfStringDate = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])$` // FormatOfStringDateTime is a RFC3339 date-time format regexp, for example "2017-07-21T17:32:28Z". - FormatOfStringDateTime = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$` + FormatOfStringDateTime = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$` ) const ( SerializationSimple = "simple" @@ -128,13 +128,16 @@ func DefineNumberFormatValidator(name string, validator NumberFormatValidator) number format. func DefineStringFormat(name string, pattern string) - DefineStringFormat defines a regexp pattern for a given string - format Deprecated: Use openapi3.DefineStringFormatValidator(name, + DefineStringFormat defines a regexp pattern for a given string format + + Deprecated: Use openapi3.DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) instead. func DefineStringFormatCallback(name string, callback func(string) error) - DefineStringFormatCallback defines a callback function for a given - string format Deprecated: Use openapi3.DefineStringFormatValidator(name, + DefineStringFormatCallback defines a callback function for a given string + format + + Deprecated: Use openapi3.DefineStringFormatValidator(name, NewCallbackValidator(fn)) instead. func DefineStringFormatValidator(name string, validator StringFormatValidator) @@ -226,6 +229,7 @@ func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error type Callback struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` // Has unexported fields. } @@ -274,6 +278,7 @@ type CallbackRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Callback @@ -316,6 +321,9 @@ func (m Callbacks) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (callbacks *Callbacks) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Callbacks to a copy of data. + type ComponentRef interface { RefString() string RefPath() *url.URL @@ -324,6 +332,7 @@ type ComponentRef interface { type Components struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` @@ -355,6 +364,7 @@ func (components *Components) Validate(ctx context.Context, opts ...ValidationOp type Contact struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -394,14 +404,18 @@ func NewContentWithSchemaRef(schema *SchemaRef, consumes []string) Content func (content Content) Get(mime string) *MediaType +func (content *Content) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Content to a copy of data. + func (content Content) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Content does not comply with the OpenAPI spec. type Discriminator struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - PropertyName string `json:"propertyName" yaml:"propertyName"` // required - Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` + PropertyName string `json:"propertyName" yaml:"propertyName"` // required + Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"` } 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 @@ -421,6 +435,7 @@ func (discriminator *Discriminator) Validate(ctx context.Context, opts ...Valida type Encoding struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -456,6 +471,7 @@ func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding type Example struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -483,6 +499,7 @@ type ExampleRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Example @@ -525,8 +542,12 @@ func (m Examples) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (examples *Examples) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Examples to a copy of data. + type ExternalDocs struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -593,6 +614,7 @@ type HeaderRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Header @@ -635,8 +657,12 @@ func (m Headers) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (headers *Headers) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Headers to a copy of data. + type Info struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -665,6 +691,7 @@ type IntegerFormatValidator = FormatValidator[int64] type License struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -686,6 +713,7 @@ func (license *License) Validate(ctx context.Context, opts ...ValidationOption) type Link struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` @@ -713,6 +741,7 @@ type LinkRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Link @@ -754,10 +783,16 @@ func (m Links) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (links *Links) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Links to a copy of data. + type Loader struct { // IsExternalRefsAllowed enables visiting other files IsExternalRefsAllowed bool + // IncludeOrigin specifies whether to include the origin of the OpenAPI elements + IncludeOrigin bool + // ReadFromURIFunc allows overriding the any file/URL reading func ReadFromURIFunc ReadFromURIFunc @@ -793,8 +828,15 @@ func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) ResolveRefsIn expands references if for instance spec was just unmarshaled +type Location struct { + Line int `json:"line,omitempty" yaml:"line,omitempty"` + Column int `json:"column,omitempty" yaml:"column,omitempty"` +} + Location is a struct that contains the location of a field. + type MediaType struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Example any `json:"example,omitempty" yaml:"example,omitempty"` @@ -872,11 +914,12 @@ type NumberFormatValidator = FormatValidator[float64] type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - 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"` // required + 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 StringMap `json:"scopes" yaml:"scopes"` // required } 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 @@ -896,6 +939,7 @@ func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) e type OAuthFlows struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` @@ -920,6 +964,7 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) type Operation struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` // Optional tags for documentation. Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` @@ -982,8 +1027,17 @@ func (operation *Operation) Validate(ctx context.Context, opts ...ValidationOpti Validate returns an error if Operation does not comply with the OpenAPI spec. +type Origin struct { + Key *Location `json:"key,omitempty" yaml:"key,omitempty"` + Fields map[string]Location `json:"fields,omitempty" yaml:"fields,omitempty"` +} + Origin contains the origin of a collection. Key is the location of the + collection itself. Fields is a map of the location of each field in the + collection. + type Parameter struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` @@ -1042,6 +1096,7 @@ type ParameterRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Parameter @@ -1099,8 +1154,12 @@ func (m ParametersMap) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (parametersMap *ParametersMap) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets ParametersMap to a copy of data. + type PathItem struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` @@ -1140,6 +1199,7 @@ func (pathItem *PathItem) Validate(ctx context.Context, opts ...ValidationOption type Paths struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` // Has unexported fields. } @@ -1228,7 +1288,8 @@ func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc documents. type Ref struct { - Ref string `json:"$ref" yaml:"$ref"` + Ref string `json:"$ref" yaml:"$ref"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` } 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 @@ -1252,8 +1313,12 @@ func (m RequestBodies) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (requestBodies *RequestBodies) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets RequestBodies to a copy of data. + type RequestBody struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` @@ -1301,6 +1366,7 @@ type RequestBodyRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *RequestBody @@ -1339,6 +1405,7 @@ func (x *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) type Response struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -1376,10 +1443,14 @@ func (m ResponseBodies) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (responseBodies *ResponseBodies) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets ResponseBodies to a copy of data. + type ResponseRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Response @@ -1418,6 +1489,7 @@ func (x *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) er type Responses struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"-" yaml:"-"` // Has unexported fields. } @@ -1476,6 +1548,7 @@ func (responses *Responses) Value(key string) *ResponseRef type Schema struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` @@ -1690,6 +1763,7 @@ type SchemaRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Schema @@ -1784,6 +1858,9 @@ func (m Schemas) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (schemas *Schemas) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets Schemas to a copy of data. + type SecurityRequirement map[string][]string SecurityRequirement is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-requirement-object @@ -1792,6 +1869,9 @@ func NewSecurityRequirement() SecurityRequirement func (security SecurityRequirement) Authenticate(provider string, scopes ...string) SecurityRequirement +func (security *SecurityRequirement) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets SecurityRequirement to a copy of data. + func (security *SecurityRequirement) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if SecurityRequirement does not comply with the OpenAPI spec. @@ -1808,6 +1888,7 @@ func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) * type SecurityScheme struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -1858,6 +1939,7 @@ type SecuritySchemeRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *SecurityScheme @@ -1901,6 +1983,9 @@ func (m SecuritySchemes) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable +func (securitySchemes *SecuritySchemes) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets SecuritySchemes to a copy of data. + type SerializationMethod struct { Style string Explode bool @@ -1910,6 +1995,7 @@ type SerializationMethod struct { type Server struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` URL string `json:"url" yaml:"url"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -1940,6 +2026,7 @@ func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (e type ServerVariable struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` @@ -1983,6 +2070,13 @@ func NewRegexpFormatValidator(pattern string) StringFormatValidator NewRegexpFormatValidator creates a new FormatValidator that uses a regular expression to validate the value. +type StringMap map[string]string + StringMap is a map[string]string that ignores the origin in the underlying + json representation. + +func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error) + UnmarshalJSON sets StringMap to a copy of data. + type T struct { Extensions map[string]any `json:"-" yaml:"-"` @@ -2038,6 +2132,7 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error type Tag struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -2149,6 +2244,7 @@ type ValidationOptions struct { type XML struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt index d44ba5ab..1eb4d41e 100644 --- a/.github/docs/openapi3filter.txt +++ b/.github/docs/openapi3filter.txt @@ -93,7 +93,7 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param defined. The function returns RequestError with a openapi3.SchemaError cause when a value is invalid by JSON schema. -func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err error) +func ValidateRequest(ctx context.Context, input *RequestValidationInput) error ValidateRequest is used to validate the given input according to previous loaded OpenAPIv3 spec. If the input does not match the OpenAPIv3 spec, a non-nil error will be returned. diff --git a/README.md b/README.md index 690ef903..d135ffe9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ The project has received pull requests [from many people](https://github.com/get Please, [give back to this project](https://github.com/sponsors/fenollp) by becoming a sponsor. Here's some projects that depend on _kin-openapi_: + * [github.com/go-fuego/fuego](https://github.com/go-fuego/fuego) - "Framework generating OpenAPI 3 spec from source code" * [github.com/a-h/rest](https://github.com/a-h/rest) - "Generate OpenAPI 3.0 specifications from Go code without annotations or magic comments" * [github.com/Tufin/oasdiff](https://github.com/Tufin/oasdiff) - "A diff tool for OpenAPI Specification 3" * [github.com/danielgtaylor/apisprout](https://github.com/danielgtaylor/apisprout) - "Lightweight, blazing fast, cross-platform OpenAPI 3 mock server with validation" @@ -294,6 +295,9 @@ for _, path := range doc.Paths.InMatchingOrder() { ## CHANGELOG: Sub-v1 breaking API changes +### v0.129.0 +* `openapi3.Discriminator.Mapping` and `openapi3.OAuthFlow.Scopes` fields went from a `map[string]string` to the new type `StringMap` + ### v0.127.0 * Downgraded `github.com/gorilla/mux` dep from `1.8.1` to `1.8.0`. diff --git a/cmd/validate/main.go b/cmd/validate/main.go index 874f30c2..75194070 100644 --- a/cmd/validate/main.go +++ b/cmd/validate/main.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" "github.com/getkin/kin-openapi/openapi2" "github.com/getkin/kin-openapi/openapi3" diff --git a/go.mod b/go.mod index 11bc6d02..48550601 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module github.com/getkin/kin-openapi -go 1.20 +go 1.22.5 require ( github.com/go-openapi/jsonpointer v0.21.0 github.com/gorilla/mux v1.8.0 - github.com/invopop/yaml v0.3.1 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 + github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 + github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 github.com/perimeterx/marshmallow v1.1.5 github.com/stretchr/testify v1.9.0 - gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -19,4 +19,5 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6b91d0dc..422452cf 100644 --- a/go.sum +++ b/go.sum @@ -5,18 +5,23 @@ github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kO github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/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.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= -github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU= +github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc= +github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -26,7 +31,9 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/maps.sh b/maps.sh index 9cfd0ffd..e2915bf4 100755 --- a/maps.sh +++ b/maps.sh @@ -209,6 +209,17 @@ func (${name} ${type}) UnmarshalJSON(data []byte) (err error) { continue } + if k == originKey { + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + if err = json.Unmarshal(data, &x.Origin); err != nil { + return + } + continue + } + var data []byte if data, err = json.Marshal(v); err != nil { return diff --git a/openapi2/marsh.go b/openapi2/marsh.go index 1a4b0d1d..1745734b 100644 --- a/openapi2/marsh.go +++ b/openapi2/marsh.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" ) func unmarshalError(jsonUnmarshalErr error) error { diff --git a/openapi2/openapi2_test.go b/openapi2/openapi2_test.go index 8a6bf968..d1b3aa56 100644 --- a/openapi2/openapi2_test.go +++ b/openapi2/openapi2_test.go @@ -6,7 +6,7 @@ import ( "os" "reflect" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" "github.com/getkin/kin-openapi/openapi2" ) diff --git a/openapi2conv/issue1016_test.go b/openapi2conv/issue1016_test.go new file mode 100644 index 00000000..acd87d07 --- /dev/null +++ b/openapi2conv/issue1016_test.go @@ -0,0 +1,81 @@ +package openapi2conv + +import ( + "context" + "encoding/json" + "testing" + + "github.com/getkin/kin-openapi/openapi2" + "github.com/stretchr/testify/require" +) + +func TestIssue1016(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/PetDirectory" + } + }, + "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" + } + } + }, + "PetDirectory":{ + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Pet" + } + } + } +} +`) + + var doc2 openapi2.T + err := json.Unmarshal(v2, &doc2) + require.NoError(t, err) + + doc3, err := v2v3YAML(v2) + require.NoError(t, err) + + err = doc3.Validate(context.Background()) + require.NoError(t, err) + require.Equal(t, "#/components/schemas/Pet", doc3.Components.Schemas["PetDirectory"].Value.AdditionalProperties.Schema.Ref) +} diff --git a/openapi2conv/issue187_test.go b/openapi2conv/issue187_test.go index 93914d9f..4cec8753 100644 --- a/openapi2conv/issue187_test.go +++ b/openapi2conv/issue187_test.go @@ -5,7 +5,7 @@ import ( "encoding/json" "testing" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" "github.com/stretchr/testify/require" "github.com/getkin/kin-openapi/openapi2" @@ -104,7 +104,7 @@ func TestIssue187(t *testing.T) { spec3, err := json.Marshal(doc3) require.NoError(t, err) const expected = `{"components":{"schemas":{"model.ProductSearchAttributeRequest":{"properties":{"filterField":{"type":"string"},"filterKey":{"type":"string"},"type":{"type":"string"},"values":{"$ref":"#/components/schemas/model.ProductSearchAttributeValueRequest"}},"title":"model.ProductSearchAttributeRequest","type":"object"},"model.ProductSearchAttributeValueRequest":{"properties":{"imageUrl":{"type":"string"},"text":{"type":"string"}},"title":"model.ProductSearchAttributeValueRequest","type":"object"}}},"info":{"contact":{"email":"test@test.com","name":"Test"},"description":"Test Golang Application","title":"Test","version":"1.0"},"openapi":"3.0.3","paths":{"/me":{"get":{"operationId":"someTest","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/model.ProductSearchAttributeRequest"}}},"description":"successful operation"}},"summary":"Some test","tags":["probe"]}}}}` - require.JSONEq(t, string(spec3), expected) + require.JSONEq(t, expected, string(spec3)) err = doc3.Validate(context.Background()) require.NoError(t, err) @@ -163,7 +163,7 @@ paths: "200": description: description ` - require.YAMLEq(t, string(spec3), expected) + require.YAMLEq(t, expected, string(spec3)) err = doc3.Validate(context.Background()) require.NoError(t, err) @@ -190,5 +190,5 @@ securityDefinitions: doc2, err := FromV3(doc3) require.NoError(t, err) - require.Equal(t, doc2.SecurityDefinitions["OAuth2Application"].Flow, "application") + require.Equal(t, "application", doc2.SecurityDefinitions["OAuth2Application"].Flow) } diff --git a/openapi2conv/issue558_test.go b/openapi2conv/issue558_test.go index 20673012..b7fb9dc5 100644 --- a/openapi2conv/issue558_test.go +++ b/openapi2conv/issue558_test.go @@ -3,7 +3,7 @@ package openapi2conv import ( "testing" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" "github.com/stretchr/testify/require" ) diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index ef0a5edd..8aa45255 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -513,7 +513,7 @@ func ToV3SchemaRef(schema *openapi2.SchemaRef) *openapi3.SchemaRef { MaxProps: schema.Value.MaxProps, AllOf: make(openapi3.SchemaRefs, len(schema.Value.AllOf)), Properties: make(openapi3.Schemas), - AdditionalProperties: schema.Value.AdditionalProperties, + AdditionalProperties: toV3AdditionalProperties(schema.Value.AdditionalProperties), } if schema.Value.Discriminator != "" { @@ -547,6 +547,27 @@ func ToV3SchemaRef(schema *openapi2.SchemaRef) *openapi3.SchemaRef { } } +func toV3AdditionalProperties(from openapi3.AdditionalProperties) openapi3.AdditionalProperties { + return openapi3.AdditionalProperties{ + Has: from.Has, + Schema: convertRefsInV3SchemaRef(from.Schema), + } +} + +func convertRefsInV3SchemaRef(from *openapi3.SchemaRef) *openapi3.SchemaRef { + if from == nil { + return nil + } + to := *from + to.Ref = ToV3Ref(to.Ref) + if to.Value != nil { + v := *from.Value + to.Value = &v + to.Value.AdditionalProperties = toV3AdditionalProperties(to.Value.AdditionalProperties) + } + return &to +} + var ref2To3 = map[string]string{ "#/definitions/": "#/components/schemas/", "#/responses/": "#/components/responses/", diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go index 31777215..659ccb69 100644 --- a/openapi2conv/openapi2_conv_test.go +++ b/openapi2conv/openapi2_conv_test.go @@ -65,6 +65,134 @@ func TestConvOpenAPIV2ToV3(t *testing.T) { require.JSONEq(t, exampleV3, string(data)) } +func TestConvOpenAPIV2ToV3WithAdditionalPropertiesSchemaRef(t *testing.T) { + v2 := []byte(` +{ + "basePath": "/v2", + "host": "test.example.com", + "info": { + "title": "MyAPI", + "version": "0.1" + }, + "paths": { + "/foo": { + "get": { + "operationId": "getFoo", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "returns all information", + "schema":{ + "type":"object", + "additionalProperties":{ + "$ref":"#/definitions/Foo" + } + } + } + }, + "summary": "get foo" + } + } + }, + "definitions": { + "Foo": { + "type": "object", + "properties": { + "a": { + "type": "string" + } + } + } + }, + "schemes": [ + "http" + ], + "swagger": "2.0" +} +`) + + var doc2 openapi2.T + err := json.Unmarshal(v2, &doc2) + require.NoError(t, err) + + doc3, err := ToV3(&doc2) + require.NoError(t, err) + err = doc3.Validate(context.Background()) + require.NoError(t, err) + + responseSchema := doc3.Paths.Value("/foo").Get.Responses.Value("200").Value.Content.Get("application/json").Schema.Value + require.Equal(t, &openapi3.Types{"object"}, responseSchema.Type) + require.Equal(t, "#/components/schemas/Foo", responseSchema.AdditionalProperties.Schema.Ref) +} + +func TestConvOpenAPIV2ToV3WithNestedAdditionalPropertiesSchemaRef(t *testing.T) { + v2 := []byte(` +{ + "basePath": "/v2", + "host": "test.example.com", + "info": { + "title": "MyAPI", + "version": "0.1" + }, + "paths": { + "/foo": { + "get": { + "operationId": "getFoo", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "returns all information", + "schema":{ + "type":"object", + "additionalProperties":{ + "type":"object", + "additionalProperties":{ + "$ref":"#/definitions/Foo" + } + } + } + } + }, + "summary": "get foo" + } + } + }, + "definitions": { + "Foo": { + "type": "object", + "properties": { + "a": { + "type": "string" + } + } + } + }, + "schemes": [ + "http" + ], + "swagger": "2.0" +} +`) + + var doc2 openapi2.T + err := json.Unmarshal(v2, &doc2) + require.NoError(t, err) + + doc3, err := ToV3(&doc2) + require.NoError(t, err) + err = doc3.Validate(context.Background()) + require.NoError(t, err) + + responseSchema := doc3.Paths.Value("/foo").Get.Responses.Value("200").Value.Content.Get("application/json").Schema.Value + require.Equal(t, &openapi3.Types{"object"}, responseSchema.Type) + require.Equal(t, &openapi3.Types{"object"}, responseSchema.AdditionalProperties.Schema.Value.Type) + require.Equal(t, "#/components/schemas/Foo", responseSchema.AdditionalProperties.Schema.Value.AdditionalProperties.Schema.Ref) +} + const exampleV2 = ` { "basePath": "/v2", diff --git a/openapi3/additionalProperties_test.go b/openapi3/additionalProperties_test.go index 7a9ec747..46cf0dbe 100644 --- a/openapi3/additionalProperties_test.go +++ b/openapi3/additionalProperties_test.go @@ -6,8 +6,8 @@ import ( "os" "testing" + "github.com/oasdiff/yaml3" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" "github.com/getkin/kin-openapi/openapi3" ) diff --git a/openapi3/callback.go b/openapi3/callback.go index 34a6bea3..3ae6da69 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -9,6 +9,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callback-object type Callback struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` m map[string]*PathItem } @@ -52,3 +53,9 @@ func (callback *Callback) Validate(ctx context.Context, opts ...ValidationOption return validateExtensions(ctx, callback.Extensions) } + +// UnmarshalJSON sets Callbacks to a copy of data. +func (callbacks *Callbacks) UnmarshalJSON(data []byte) (err error) { + *callbacks, _, err = unmarshalStringMapP[CallbackRef](data) + return +} diff --git a/openapi3/components.go b/openapi3/components.go index 98c4b96c..aecf8b64 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -25,6 +25,7 @@ type ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#components-object type Components struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` @@ -94,6 +95,7 @@ func (components *Components) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "schemas") delete(x.Extensions, "parameters") delete(x.Extensions, "headers") diff --git a/openapi3/contact.go b/openapi3/contact.go index 6c76a6fb..57cc801d 100644 --- a/openapi3/contact.go +++ b/openapi3/contact.go @@ -9,6 +9,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contact-object type Contact struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -50,6 +51,8 @@ func (contact *Contact) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, originKey) delete(x.Extensions, "name") delete(x.Extensions, "url") delete(x.Extensions, "email") diff --git a/openapi3/content.go b/openapi3/content.go index 81b070ee..73e301e0 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -122,3 +122,9 @@ func (content Content) Validate(ctx context.Context, opts ...ValidationOption) e } return nil } + +// UnmarshalJSON sets Content to a copy of data. +func (content *Content) UnmarshalJSON(data []byte) (err error) { + *content, _, err = unmarshalStringMapP[MediaType](data) + return +} diff --git a/openapi3/datetime_schema_test.go b/openapi3/datetime_schema_test.go new file mode 100644 index 00000000..fec60576 --- /dev/null +++ b/openapi3/datetime_schema_test.go @@ -0,0 +1,167 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +var DateSpec = []byte(` +components: + schemas: + Server: + properties: + date: + $ref: "#/components/schemas/timestamp" + name: + type: string + type: object + timestamp: + type: string + format: date +openapi: "3.0.1" +paths: {} +info: + version: 1.1.1 + title: title +`[1:]) + +var DateTimeSpec = []byte(` +components: + schemas: + Server: + properties: + datetime: + $ref: "#/components/schemas/timestamp" + name: + type: string + type: object + timestamp: + type: string + format: date-time +openapi: "3.0.1" +paths: {} +info: + version: 1.1.1 + title: title +`[1:]) + +func TestDateZeroMonth(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "date": "2001-00-03", + }) + require.EqualError(t, err, `Error at "/date": string doesn't match the format "date": string doesn't match pattern "`+FormatOfStringDate+`"`) +} + +func TestDateZeroDay(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "date": "2001-02-00", + }) + require.EqualError(t, err, `Error at "/date": string doesn't match the format "date": string doesn't match pattern "`+FormatOfStringDate+`"`) +} + +func TestDateTimeZeroMonth(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2001-00-03T04:05:06.789Z", + }) + require.EqualError(t, err, `Error at "/datetime": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) +} + +func TestDateTimeZeroDay(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2001-02-00T04:05:06.789Z", + }) + require.EqualError(t, err, `Error at "/datetime": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) +} + +func TestDateTimeLeapSecond(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2016-12-31T23:59:60.000Z", // exact time of the most recent leap second + }) + require.NoError(t, err) +} + +func TestDateTimeHourOutOfBounds(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2016-12-31T24:00:00.000Z", + }) + require.EqualError(t, err, `Error at "/datetime": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) +} + +func TestDateTimeMinuteOutOfBounds(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2016-12-31T23:60:00.000Z", + }) + require.EqualError(t, err, `Error at "/datetime": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) +} + +func TestDateTimeSecondOutOfBounds(t *testing.T) { + loader := NewLoader() + doc, err := loader.LoadFromData(DateTimeSpec) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ + "name": "kin-openapi", + "datetime": "2016-12-31T23:59:61.000Z", + }) + require.EqualError(t, err, `Error at "/datetime": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) +} diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index e8193bd9..a8ab07b4 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -9,9 +9,10 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object type Discriminator struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - PropertyName string `json:"propertyName" yaml:"propertyName"` // required - Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` + PropertyName string `json:"propertyName" yaml:"propertyName"` // required + Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"` } // MarshalJSON returns the JSON encoding of Discriminator. @@ -44,6 +45,8 @@ func (discriminator *Discriminator) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, originKey) delete(x.Extensions, "propertyName") delete(x.Extensions, "mapping") if len(x.Extensions) == 0 { diff --git a/openapi3/encoding.go b/openapi3/encoding.go index 1bcdaea5..7eb507e5 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -11,6 +11,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encoding-object type Encoding struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -80,6 +81,8 @@ func (encoding *Encoding) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, originKey) delete(x.Extensions, "contentType") delete(x.Extensions, "headers") delete(x.Extensions, "style") diff --git a/openapi3/encoding_test.go b/openapi3/encoding_test.go index 955b21b2..cbf4738e 100644 --- a/openapi3/encoding_test.go +++ b/openapi3/encoding_test.go @@ -91,7 +91,7 @@ func TestEncodingSerializationMethod(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got := tc.enc.SerializationMethod() - require.EqualValues(t, got, tc.want, "got %#v, want %#v", got, tc.want) + require.EqualValues(t, tc.want, got, "got %#v, want %#v", got, tc.want) }) } } diff --git a/openapi3/example.go b/openapi3/example.go index f9a7a6b0..56aee378 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -10,6 +10,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object type Example struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -59,6 +60,7 @@ func (example *Example) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "summary") delete(x.Extensions, "description") delete(x.Extensions, "value") @@ -83,3 +85,9 @@ func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) return validateExtensions(ctx, example.Extensions) } + +// UnmarshalJSON sets Examples to a copy of data. +func (examples *Examples) UnmarshalJSON(data []byte) (err error) { + *examples, _, err = unmarshalStringMapP[ExampleRef](data) + return +} diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index bd99511a..f6794141 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -12,6 +12,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object type ExternalDocs struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -49,6 +50,7 @@ func (e *ExternalDocs) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "description") delete(x.Extensions, "url") if len(x.Extensions) == 0 { diff --git a/openapi3/header.go b/openapi3/header.go index dc542874..6b23db52 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -94,3 +94,9 @@ func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) er } return nil } + +// UnmarshalJSON sets Headers to a copy of data. +func (headers *Headers) UnmarshalJSON(data []byte) (err error) { + *headers, _, err = unmarshalStringMapP[HeaderRef](data) + return +} diff --git a/openapi3/info.go b/openapi3/info.go index e2468285..acca2f4d 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -10,6 +10,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#info-object type Info struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -62,6 +63,7 @@ func (info *Info) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "title") delete(x.Extensions, "description") delete(x.Extensions, "termsOfService") diff --git a/openapi3/issue241_test.go b/openapi3/issue241_test.go index 0b37b0a9..9a643ede 100644 --- a/openapi3/issue241_test.go +++ b/openapi3/issue241_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" + "github.com/oasdiff/yaml3" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" "github.com/getkin/kin-openapi/openapi3" ) diff --git a/openapi3/issue495_test.go b/openapi3/issue495_test.go index ee4cad50..b4d4a202 100644 --- a/openapi3/issue495_test.go +++ b/openapi3/issue495_test.go @@ -1,6 +1,7 @@ package openapi3 import ( + "os" "testing" "github.com/stretchr/testify/require" @@ -113,6 +114,10 @@ paths: sl := NewLoader() sl.IsExternalRefsAllowed = true + if os.Getenv("CI") == "true" { + t.Skip("Running in CI: skipping so we avoid 403 error from remote schema server") + } + doc, err := sl.LoadFromData(spec) require.NoError(t, err) diff --git a/openapi3/issue652_test.go b/openapi3/issue652_test.go index 4036b481..738569a4 100644 --- a/openapi3/issue652_test.go +++ b/openapi3/issue652_test.go @@ -23,7 +23,7 @@ func TestIssue652(t *testing.T) { require.Contains(t, spec.Components.Schemas, schemaName) schema := spec.Components.Schemas[schemaName] - assert.Equal(t, schema.Ref, "../definitions.yml#/components/schemas/TestSchema") - assert.Equal(t, schema.Value.Type, &openapi3.Types{"string"}) + assert.Equal(t, "../definitions.yml#/components/schemas/TestSchema", schema.Ref) + assert.Equal(t, &openapi3.Types{"string"}, schema.Value.Type) }) } diff --git a/openapi3/issue82_test.go b/openapi3/issue82_test.go index 3bf242bc..27b72e28 100644 --- a/openapi3/issue82_test.go +++ b/openapi3/issue82_test.go @@ -8,7 +8,7 @@ import ( ) func TestIssue82(t *testing.T) { - payload := map[string]interface{}{ + payload := map[string]any{ "prop1": "val", "prop3": "val", } diff --git a/openapi3/issue883_test.go b/openapi3/issue883_test.go index 28d71638..7609e6ac 100644 --- a/openapi3/issue883_test.go +++ b/openapi3/issue883_test.go @@ -3,9 +3,9 @@ package openapi3_test import ( "testing" - invopopYaml "github.com/invopop/yaml" + yaml "github.com/oasdiff/yaml" + yamlv3 "github.com/oasdiff/yaml3" "github.com/stretchr/testify/require" - v3 "gopkg.in/yaml.v3" "github.com/getkin/kin-openapi/openapi3" ) @@ -38,8 +38,8 @@ paths: require.NoError(t, err) require.NotNil(t, doc.Paths) - t.Run("Roundtrip invopop/yaml", func(t *testing.T) { - justPaths, err := invopopYaml.Marshal(doc.Paths) + t.Run("Roundtrip using yaml pkg", func(t *testing.T) { + justPaths, err := yaml.Marshal(doc.Paths) require.NoError(t, err) require.NotNil(t, doc.Paths) require.YAMLEq(t, ` @@ -51,13 +51,13 @@ paths: description: OK `[1:], string(justPaths)) - marshalledYaml, err := invopopYaml.Marshal(doc) + marshalledYaml, err := yaml.Marshal(doc) require.NoError(t, err) require.NotNil(t, doc.Paths) require.YAMLEq(t, spec, string(marshalledYaml)) var newDoc openapi3.T - err = invopopYaml.Unmarshal(marshalledYaml, &newDoc) + err = yaml.Unmarshal(marshalledYaml, &newDoc) require.NoError(t, err) require.NotNil(t, newDoc.Paths) require.Equal(t, doc, &newDoc) @@ -76,7 +76,7 @@ paths: description: OK `[1:], string(justPaths)) - justPaths, err = v3.Marshal(doc.Paths) + justPaths, err = yamlv3.Marshal(doc.Paths) require.NoError(t, err) require.NotNil(t, doc.Paths) require.YAMLEq(t, ` @@ -88,14 +88,14 @@ paths: description: OK `[1:], string(justPaths)) - marshalledYaml, err := v3.Marshal(doc) + marshalledYaml, err := yamlv3.Marshal(doc) require.NoError(t, err) require.NotNil(t, doc.Paths) require.YAMLEq(t, spec, string(marshalledYaml)) - t.Skip("TODO: impl https://pkg.go.dev/gopkg.in/yaml.v3#Unmarshaler on maplike types") + t.Skip("TODO: impl https://pkg.go.dev/github.com/oasdiff/yaml3#Unmarshaler on maplike types") var newDoc openapi3.T - err = v3.Unmarshal(marshalledYaml, &newDoc) + err = yamlv3.Unmarshal(marshalledYaml, &newDoc) require.NoError(t, err) require.NotNil(t, newDoc.Paths) require.Equal(t, doc, &newDoc) diff --git a/openapi3/issue972_test.go b/openapi3/issue972_test.go index 3575adc9..5aaaae22 100644 --- a/openapi3/issue972_test.go +++ b/openapi3/issue972_test.go @@ -3,8 +3,8 @@ package openapi3 import ( "testing" + "github.com/oasdiff/yaml3" "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" ) func TestIssue972(t *testing.T) { diff --git a/openapi3/license.go b/openapi3/license.go index c4f6c8dc..df9d6c44 100644 --- a/openapi3/license.go +++ b/openapi3/license.go @@ -10,6 +10,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#license-object type License struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -45,6 +46,7 @@ func (license *License) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "name") delete(x.Extensions, "url") if len(x.Extensions) == 0 { diff --git a/openapi3/link.go b/openapi3/link.go index 132f6780..dfd320f1 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -11,6 +11,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#link-object type Link struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` @@ -66,6 +67,8 @@ func (link *Link) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, originKey) delete(x.Extensions, "operationRef") delete(x.Extensions, "operationId") delete(x.Extensions, "description") @@ -92,3 +95,9 @@ func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error return validateExtensions(ctx, link.Extensions) } + +// UnmarshalJSON sets Links to a copy of data. +func (links *Links) UnmarshalJSON(data []byte) (err error) { + *links, _, err = unmarshalStringMapP[LinkRef](data) + return +} diff --git a/openapi3/loader.go b/openapi3/loader.go index 31c34076..0f3b8cbd 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -28,6 +28,9 @@ type Loader struct { // IsExternalRefsAllowed enables visiting other files IsExternalRefsAllowed bool + // IncludeOrigin specifies whether to include the origin of the OpenAPI elements + IncludeOrigin bool + // ReadFromURIFunc allows overriding the any file/URL reading func ReadFromURIFunc ReadFromURIFunc @@ -103,7 +106,7 @@ func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, el if err != nil { return nil, err } - if err := unmarshal(data, element); err != nil { + if err := unmarshal(data, element, loader.IncludeOrigin); err != nil { return nil, err } @@ -139,7 +142,7 @@ func (loader *Loader) LoadFromIoReader(reader io.Reader) (*T, error) { func (loader *Loader) LoadFromData(data []byte) (*T, error) { loader.resetVisitedPathItemRefs() doc := &T{} - if err := unmarshal(data, doc); err != nil { + if err := unmarshal(data, doc, loader.IncludeOrigin); err != nil { return nil, err } if err := loader.ResolveRefsIn(doc, nil); err != nil { @@ -168,7 +171,7 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR doc := &T{} loader.visitedDocuments[uri] = doc - if err := unmarshal(data, doc); err != nil { + if err := unmarshal(data, doc, loader.IncludeOrigin); err != nil { return nil, err } @@ -422,7 +425,7 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv if err2 != nil { return nil, nil, err } - if err2 = unmarshal(data, &cursor); err2 != nil { + if err2 = unmarshal(data, &cursor, loader.IncludeOrigin); err2 != nil { return nil, nil, err } if cursor, err2 = drill(cursor); err2 != nil || cursor == nil { diff --git a/openapi3/loader_relative_refs_test.go b/openapi3/loader_relative_refs_test.go index cca44ba3..05f9d0e4 100644 --- a/openapi3/loader_relative_refs_test.go +++ b/openapi3/loader_relative_refs_test.go @@ -927,7 +927,7 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { require.Equal(t, "example request", nestedDirPath.Patch.RequestBody.Value.Description) // check response schema and example - require.Equal(t, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type, &Types{"string"}) + require.Equal(t, &Types{"string"}, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) expectedExample := "hello" require.Equal(t, expectedExample, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Examples["CustomTestExample"].Value.Value) @@ -948,5 +948,5 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { // check response schema and example require.Equal(t, &Types{"string"}, moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) - require.Equal(t, moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Examples["CustomTestExample"].Value.Value, expectedExample) + require.Equal(t, expectedExample, moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Examples["CustomTestExample"].Value.Value) } diff --git a/openapi3/maplike.go b/openapi3/maplike.go index 7b8045c6..35b33657 100644 --- a/openapi3/maplike.go +++ b/openapi3/maplike.go @@ -125,6 +125,17 @@ func (responses *Responses) UnmarshalJSON(data []byte) (err error) { continue } + if k == originKey { + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + if err = json.Unmarshal(data, &x.Origin); err != nil { + return + } + continue + } + var data []byte if data, err = json.Marshal(v); err != nil { return @@ -256,6 +267,17 @@ func (callback *Callback) UnmarshalJSON(data []byte) (err error) { continue } + if k == originKey { + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + if err = json.Unmarshal(data, &x.Origin); err != nil { + return + } + continue + } + var data []byte if data, err = json.Marshal(v); err != nil { return @@ -387,6 +409,17 @@ func (paths *Paths) UnmarshalJSON(data []byte) (err error) { continue } + if k == originKey { + var data []byte + if data, err = json.Marshal(v); err != nil { + return + } + if err = json.Unmarshal(data, &x.Origin); err != nil { + return + } + continue + } + var data []byte if data, err = json.Marshal(v); err != nil { return diff --git a/openapi3/mapping_test.go b/openapi3/mapping_test.go new file mode 100644 index 00000000..0c1c1995 --- /dev/null +++ b/openapi3/mapping_test.go @@ -0,0 +1,58 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMapping(t *testing.T) { + + schema := ` +openapi: 3.0.0 +info: + title: ACME + version: 1.0.0 +components: + schemas: + Pet: + type: object + required: + - petType + properties: + petType: + type: string + discriminator: + propertyName: petType + mapping: + dog: Dog + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + # all other properties specific to a Cat + properties: + name: + type: string + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + # all other properties specific to a Dog + properties: + bark: + type: string + Lizard: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + # all other properties specific to a Lizard + properties: + lovesRocks: + type: boolean +` + loader := NewLoader() + loader.IsExternalRefsAllowed = true + _, err := loader.LoadFromData([]byte(schema)) + require.NoError(t, err) +} diff --git a/openapi3/marsh.go b/openapi3/marsh.go index daa93755..2f00828a 100644 --- a/openapi3/marsh.go +++ b/openapi3/marsh.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" ) func unmarshalError(jsonUnmarshalErr error) error { @@ -16,7 +16,7 @@ func unmarshalError(jsonUnmarshalErr error) error { return jsonUnmarshalErr } -func unmarshal(data []byte, v any) error { +func unmarshal(data []byte, v any, includeOrigin bool) error { var jsonErr, yamlErr error // See https://github.com/getkin/kin-openapi/issues/680 @@ -25,7 +25,7 @@ func unmarshal(data []byte, v any) error { } // UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys - if yamlErr = yaml.Unmarshal(data, v); yamlErr == nil { + if yamlErr = yaml.UnmarshalWithOrigin(data, v, includeOrigin); yamlErr == nil { return nil } diff --git a/openapi3/media_type.go b/openapi3/media_type.go index d4466bcf..d6edaf4d 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -14,6 +14,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#media-type-object type MediaType struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Example any `json:"example,omitempty" yaml:"example,omitempty"` @@ -101,6 +102,7 @@ func (mediaType *MediaType) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "schema") delete(x.Extensions, "example") delete(x.Extensions, "examples") diff --git a/openapi3/openapi3_test.go b/openapi3/openapi3_test.go index 3b68c169..bb42df56 100644 --- a/openapi3/openapi3_test.go +++ b/openapi3/openapi3_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/invopop/yaml" + "github.com/oasdiff/yaml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/openapi3/operation.go b/openapi3/operation.go index 40abf73c..7b57e847 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -14,6 +14,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object type Operation struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` // Optional tags for documentation. Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` @@ -116,6 +117,7 @@ func (operation *Operation) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "tags") delete(x.Extensions, "summary") delete(x.Extensions, "description") diff --git a/openapi3/origin.go b/openapi3/origin.go new file mode 100644 index 00000000..b0e4a934 --- /dev/null +++ b/openapi3/origin.go @@ -0,0 +1,17 @@ +package openapi3 + +const originKey = "origin" + +// Origin contains the origin of a collection. +// Key is the location of the collection itself. +// Fields is a map of the location of each field in the collection. +type Origin struct { + Key *Location `json:"key,omitempty" yaml:"key,omitempty"` + Fields map[string]Location `json:"fields,omitempty" yaml:"fields,omitempty"` +} + +// Location is a struct that contains the location of a field. +type Location struct { + Line int `json:"line,omitempty" yaml:"line,omitempty"` + Column int `json:"column,omitempty" yaml:"column,omitempty"` +} diff --git a/openapi3/origin_test.go b/openapi3/origin_test.go new file mode 100644 index 00000000..d955056a --- /dev/null +++ b/openapi3/origin_test.go @@ -0,0 +1,350 @@ +package openapi3 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOrigin_Info(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/simple.yaml") + require.NoError(t, err) + + require.NotNil(t, doc.Info.Origin) + require.Equal(t, + &Location{ + Line: 2, + Column: 1, + }, + doc.Info.Origin.Key) + + require.Equal(t, + Location{ + Line: 3, + Column: 3, + }, + doc.Info.Origin.Fields["title"]) + + require.Equal(t, + Location{ + Line: 4, + Column: 3, + }, + doc.Info.Origin.Fields["version"]) +} + +func TestOrigin_Paths(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/simple.yaml") + require.NoError(t, err) + + require.NotNil(t, doc.Paths.Origin) + require.Equal(t, + &Location{ + Line: 5, + Column: 1, + }, + doc.Paths.Origin.Key) + + base := doc.Paths.Find("/partner-api/test/another-method") + + require.NotNil(t, base.Origin) + require.Equal(t, + &Location{ + Line: 13, + Column: 3, + }, + base.Origin.Key) + + require.NotNil(t, base.Get.Origin) + require.Equal(t, + &Location{ + Line: 14, + Column: 5, + }, + base.Get.Origin.Key) +} + +func TestOrigin_RequestBody(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/request_body.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/subscribe").Post.RequestBody.Value + require.NotNil(t, base.Origin) + require.Equal(t, + &Location{ + Line: 8, + Column: 7, + }, + base.Origin.Key) + + require.NotNil(t, base.Content["application/json"].Origin) + require.Equal(t, + &Location{ + Line: 10, + Column: 11, + }, + base.Content["application/json"].Origin.Key) +} + +func TestOrigin_Responses(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/simple.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/partner-api/test/another-method").Get.Responses + require.NotNil(t, base.Origin) + require.Equal(t, + &Location{ + Line: 17, + Column: 7, + }, + base.Origin.Key) + + require.NotNil(t, base.Origin) + require.Nil(t, base.Value("200").Origin) + require.Equal(t, + &Location{ + Line: 18, + Column: 9, + }, + base.Value("200").Value.Origin.Key) + + require.Equal(t, + Location{ + Line: 19, + Column: 11, + }, + base.Value("200").Value.Origin.Fields["description"]) +} + +func TestOrigin_Parameters(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/parameters.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/api/test").Get.Parameters[0].Value + require.NotNil(t, base) + require.Equal(t, + &Location{ + Line: 9, + Column: 11, + }, + base.Origin.Key) + + require.Equal(t, + Location{ + Line: 10, + Column: 11, + }, + base.Origin.Fields["in"]) + + require.Equal(t, + Location{ + Line: 9, + Column: 11, + }, + base.Origin.Fields["name"]) +} + +func TestOrigin_SchemaInAdditionalProperties(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/additional_properties.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/partner-api/test/some-method").Get.Responses.Value("200").Value.Content["application/json"].Schema.Value.AdditionalProperties + require.NotNil(t, base) + + require.NotNil(t, base.Schema.Value.Origin) + require.Equal(t, + &Location{ + Line: 14, + Column: 17, + }, + base.Schema.Value.Origin.Key) + + require.Equal(t, + Location{ + Line: 15, + Column: 19, + }, + base.Schema.Value.Origin.Fields["type"]) +} + +func TestOrigin_ExternalDocs(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/external_docs.yaml") + require.NoError(t, err) + + base := doc.ExternalDocs + require.NotNil(t, base.Origin) + + require.Equal(t, + &Location{ + Line: 13, + Column: 1, + }, + base.Origin.Key) + + require.Equal(t, + Location{ + Line: 14, + Column: 3, + }, + base.Origin.Fields["description"]) + + require.Equal(t, + Location{ + Line: 15, + Column: 3, + }, + base.Origin.Fields["url"]) +} + +func TestOrigin_Security(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/security.yaml") + require.NoError(t, err) + + base := doc.Components.SecuritySchemes["petstore_auth"].Value + require.NotNil(t, base) + + require.Equal(t, + &Location{ + Line: 29, + Column: 5, + }, + base.Origin.Key) + + require.Equal(t, + Location{ + Line: 30, + Column: 7, + }, + base.Origin.Fields["type"]) + + require.Equal(t, + &Location{ + Line: 31, + Column: 7, + }, + base.Flows.Origin.Key) + + require.Equal(t, + &Location{ + Line: 32, + Column: 9, + }, + base.Flows.Implicit.Origin.Key) + + require.Equal(t, + Location{ + Line: 33, + Column: 11, + }, + base.Flows.Implicit.Origin.Fields["authorizationUrl"]) +} + +func TestOrigin_Example(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/example.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/subscribe").Post.RequestBody.Value.Content["application/json"].Examples["bar"].Value + require.NotNil(t, base.Origin) + require.Equal(t, + &Location{ + Line: 14, + Column: 15, + }, + base.Origin.Key) + + require.Equal(t, + Location{ + Line: 15, + Column: 17, + }, + base.Origin.Fields["summary"]) + + // Note: + // Example.Value contains an extra field: "origin". + // + // Explanation: + // The example value is defined in the original yaml file as a json object: {"bar": "baz"} + // This json object is also valid in YAML, so yaml.3 decodes it as a map and adds an "origin" field. + require.Contains(t, + base.Value, + originKey) +} + +func TestOrigin_XML(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + loader.IncludeOrigin = true + loader.Context = context.Background() + + doc, err := loader.LoadFromFile("testdata/origin/xml.yaml") + require.NoError(t, err) + + base := doc.Paths.Find("/subscribe").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["name"].Value.XML + require.NotNil(t, base.Origin) + require.Equal(t, + &Location{ + Line: 21, + Column: 19, + }, + base.Origin.Key) + + require.Equal(t, + Location{ + Line: 22, + Column: 21, + }, + base.Origin.Fields["namespace"]) + + require.Equal(t, + Location{ + Line: 23, + Column: 21, + }, + base.Origin.Fields["prefix"]) +} diff --git a/openapi3/parameter.go b/openapi3/parameter.go index 34fe2911..d69bd5f5 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -73,6 +73,7 @@ func (parameters Parameters) Validate(ctx context.Context, opts ...ValidationOpt // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object type Parameter struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` In string `json:"in,omitempty" yaml:"in,omitempty"` @@ -216,6 +217,7 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error { } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "name") delete(x.Extensions, "in") delete(x.Extensions, "description") @@ -414,3 +416,9 @@ func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOpti return validateExtensions(ctx, parameter.Extensions) } + +// UnmarshalJSON sets ParametersMap to a copy of data. +func (parametersMap *ParametersMap) UnmarshalJSON(data []byte) (err error) { + *parametersMap, _, err = unmarshalStringMapP[ParameterRef](data) + return +} diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 859634fe..2c60472a 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -12,6 +12,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#path-item-object type PathItem struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` @@ -98,6 +99,7 @@ func (pathItem *PathItem) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "$ref") delete(x.Extensions, "summary") delete(x.Extensions, "description") diff --git a/openapi3/paths.go b/openapi3/paths.go index 76747412..80675047 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -11,6 +11,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object type Paths struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` m map[string]*PathItem } diff --git a/openapi3/ref.go b/openapi3/ref.go index 07060731..d24f4c9b 100644 --- a/openapi3/ref.go +++ b/openapi3/ref.go @@ -5,5 +5,6 @@ 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"` + Ref string `json:"$ref" yaml:"$ref"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` } diff --git a/openapi3/refs.go b/openapi3/refs.go index d337b0e3..fc11164a 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -19,6 +19,7 @@ type CallbackRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Callback @@ -72,6 +73,7 @@ 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 + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -155,6 +157,7 @@ type ExampleRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Example @@ -208,6 +211,7 @@ 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 + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -291,6 +295,7 @@ type HeaderRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Header @@ -344,6 +349,7 @@ 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 + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -427,6 +433,7 @@ type LinkRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Link @@ -480,6 +487,7 @@ 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 + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -563,6 +571,7 @@ type ParameterRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Parameter @@ -616,6 +625,7 @@ 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 + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -699,6 +709,7 @@ type RequestBodyRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *RequestBody @@ -752,6 +763,7 @@ 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 + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -835,6 +847,7 @@ type ResponseRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Response @@ -888,6 +901,7 @@ 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 + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -971,6 +985,7 @@ type SchemaRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *Schema @@ -1024,6 +1039,7 @@ 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 + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { @@ -1107,6 +1123,7 @@ type SecuritySchemeRef struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *SecurityScheme @@ -1160,6 +1177,7 @@ 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 + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { diff --git a/openapi3/refs.tmpl b/openapi3/refs.tmpl index a3f5bdab..028deba7 100644 --- a/openapi3/refs.tmpl +++ b/openapi3/refs.tmpl @@ -19,6 +19,7 @@ type {{ $type.Name }}Ref struct { // Extensions only captures fields starting with 'x-' as no other fields // are allowed by the openapi spec. Extensions map[string]any + Origin *Origin Ref string Value *{{ $type.Name }} @@ -72,6 +73,7 @@ func (x *{{ $type.Name }}Ref) UnmarshalJSON(data []byte) error { var refOnly Ref if extra, err := marshmallow.Unmarshal(data, &refOnly, marshmallow.WithExcludeKnownFieldsFromMap(true)); err == nil && refOnly.Ref != "" { x.Ref = refOnly.Ref + x.Origin = refOnly.Origin if len(extra) != 0 { x.extra = make([]string, 0, len(extra)) for key := range extra { diff --git a/openapi3/refs_issue247_test.go b/openapi3/refs_issue247_test.go index 62f056d8..ae6d0c36 100644 --- a/openapi3/refs_issue247_test.go +++ b/openapi3/refs_issue247_test.go @@ -120,7 +120,7 @@ components: require.NoError(t, err) var ptr jsonpointer.Pointer - var v interface{} + var v any var kind reflect.Kind ptr, err = jsonpointer.New("/paths") diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 6d4b8185..39288ee7 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -10,6 +10,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#request-body-object type RequestBody struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` @@ -108,6 +109,7 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "description") delete(x.Extensions, "required") delete(x.Extensions, "content") @@ -136,3 +138,9 @@ func (requestBody *RequestBody) Validate(ctx context.Context, opts ...Validation return validateExtensions(ctx, requestBody.Extensions) } + +// UnmarshalJSON sets RequestBodies to a copy of data. +func (requestBodies *RequestBodies) UnmarshalJSON(data []byte) (err error) { + *requestBodies, _, err = unmarshalStringMapP[RequestBodyRef](data) + return +} diff --git a/openapi3/response.go b/openapi3/response.go index af8fda6f..d4bfe42e 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -12,6 +12,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responses-object type Responses struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"-" yaml:"-"` m map[string]*ResponseRef } @@ -102,6 +103,7 @@ func (responses *Responses) Validate(ctx context.Context, opts ...ValidationOpti // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#response-object type Response struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -171,6 +173,7 @@ func (response *Response) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "description") delete(x.Extensions, "headers") delete(x.Extensions, "content") @@ -225,3 +228,9 @@ func (response *Response) Validate(ctx context.Context, opts ...ValidationOption return validateExtensions(ctx, response.Extensions) } + +// UnmarshalJSON sets ResponseBodies to a copy of data. +func (responseBodies *ResponseBodies) UnmarshalJSON(data []byte) (err error) { + *responseBodies, _, err = unmarshalStringMapP[ResponseRef](data) + return +} diff --git a/openapi3/response_issue224_test.go b/openapi3/response_issue224_test.go index 2edf894d..265f88a2 100644 --- a/openapi3/response_issue224_test.go +++ b/openapi3/response_issue224_test.go @@ -458,7 +458,7 @@ func TestEmptyResponsesAreInvalid(t *testing.T) { doc, err := loader.LoadFromData(spec) require.NoError(t, err) - require.Equal(t, doc.ExternalDocs.Description, "See AsyncAPI example") + require.Equal(t, "See AsyncAPI example", doc.ExternalDocs.Description) err = doc.Validate(context.Background()) require.EqualError(t, err, `invalid paths: invalid path /pet: invalid operation POST: the responses object MUST contain at least one response code`) diff --git a/openapi3/schema.go b/openapi3/schema.go index f8119606..10a00eac 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -81,6 +81,7 @@ func (s SchemaRefs) JSONLookup(token string) (any, error) { // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object type Schema struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` @@ -411,6 +412,7 @@ func (schema *Schema) UnmarshalJSON(data []byte) error { } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "oneOf") delete(x.Extensions, "anyOf") delete(x.Extensions, "allOf") @@ -2244,3 +2246,9 @@ func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) { func unsupportedFormat(format string) error { return fmt.Errorf("unsupported 'format' value %q", format) } + +// UnmarshalJSON sets Schemas to a copy of data. +func (schemas *Schemas) UnmarshalJSON(data []byte) (err error) { + *schemas, _, err = unmarshalStringMapP[SchemaRef](data) + return +} diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index 023c2669..489105b4 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -41,10 +41,10 @@ const ( FormatOfStringByte = `(^$|^[a-zA-Z0-9+/\-_]*=*$)` // FormatOfStringDate is a RFC3339 date format regexp, for example "2017-07-21". - FormatOfStringDate = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$` + FormatOfStringDate = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])$` // FormatOfStringDateTime is a RFC3339 date-time format regexp, for example "2017-07-21T17:32:28Z". - FormatOfStringDateTime = `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$` + FormatOfStringDateTime = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$` ) func init() { @@ -131,12 +131,14 @@ func DefineIntegerFormatValidator(name string, validator IntegerFormatValidator) } // DefineStringFormat defines a regexp pattern for a given string format +// // Deprecated: Use openapi3.DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) instead. func DefineStringFormat(name string, pattern string) { DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) } // DefineStringFormatCallback defines a callback function for a given string format +// // Deprecated: Use openapi3.DefineStringFormatValidator(name, NewCallbackValidator(fn)) instead. func DefineStringFormatCallback(name string, callback func(string) error) { DefineStringFormatValidator(name, NewCallbackValidator(callback)) diff --git a/openapi3/schema_issue492_test.go b/openapi3/schema_issue492_test.go index 2fdfcbce..15156722 100644 --- a/openapi3/schema_issue492_test.go +++ b/openapi3/schema_issue492_test.go @@ -46,5 +46,5 @@ info: "name": "kin-openapi", "time": "2001-02-03T04:05:06:789Z", }) - require.ErrorContains(t, err, `Error at "/time": string doesn't match the format "date-time": string doesn't match pattern "^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$"`) + require.EqualError(t, err, `Error at "/time": string doesn't match the format "date-time": string doesn't match pattern "`+FormatOfStringDateTime+`"`) } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index d678361b..4332983c 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" + "github.com/oasdiff/yaml3" "github.com/stretchr/testify/require" - "gopkg.in/yaml.v3" ) type schemaExample struct { diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index 87891c95..6af80e8b 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -49,3 +49,9 @@ func (security *SecurityRequirement) Validate(ctx context.Context, opts ...Valid return nil } + +// UnmarshalJSON sets SecurityRequirement to a copy of data. +func (security *SecurityRequirement) UnmarshalJSON(data []byte) (err error) { + *security, _, err = unmarshalStringMap[[]string](data) + return +} diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index b5c94b61..7bc889de 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -12,6 +12,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object type SecurityScheme struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -100,6 +101,7 @@ func (ss *SecurityScheme) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "type") delete(x.Extensions, "description") delete(x.Extensions, "name") @@ -216,6 +218,7 @@ func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object type OAuthFlows struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` @@ -270,6 +273,7 @@ func (flows *OAuthFlows) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "implicit") delete(x.Extensions, "password") delete(x.Extensions, "clientCredentials") @@ -316,11 +320,12 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object type OAuthFlow struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` - 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"` // required + 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 StringMap `json:"scopes" yaml:"scopes"` // required } // MarshalJSON returns the JSON encoding of OAuthFlow. @@ -359,6 +364,8 @@ func (flow *OAuthFlow) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + + delete(x.Extensions, originKey) delete(x.Extensions, "authorizationUrl") delete(x.Extensions, "tokenUrl") delete(x.Extensions, "refreshUrl") @@ -427,3 +434,9 @@ func (flow *OAuthFlow) validate(ctx context.Context, typ oAuthFlowType, opts ... return flow.Validate(ctx, opts...) } + +// UnmarshalJSON sets SecuritySchemes to a copy of data. +func (securitySchemes *SecuritySchemes) UnmarshalJSON(data []byte) (err error) { + *securitySchemes, _, err = unmarshalStringMapP[SecuritySchemeRef](data) + return +} diff --git a/openapi3/server.go b/openapi3/server.go index 7a2007f2..e438e920 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "math" "net/url" "sort" "strings" @@ -52,6 +51,7 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-object type Server struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` URL string `json:"url" yaml:"url"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -115,6 +115,7 @@ func (server *Server) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "url") delete(x.Extensions, "description") delete(x.Extensions, "variables") @@ -172,7 +173,7 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) { } else if ns < 0 { i = np } else { - i = int(math.Min(float64(np), float64(ns))) + i = min(np, ns) } if i < 0 { i = len(input) @@ -235,6 +236,7 @@ func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (e // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object type ServerVariable struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` @@ -276,6 +278,7 @@ func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "enum") delete(x.Extensions, "default") delete(x.Extensions, "description") diff --git a/openapi3/stringmap.go b/openapi3/stringmap.go new file mode 100644 index 00000000..354a4c8e --- /dev/null +++ b/openapi3/stringmap.go @@ -0,0 +1,76 @@ +package openapi3 + +import "encoding/json" + +// StringMap is a map[string]string that ignores the origin in the underlying json representation. +type StringMap map[string]string + +// UnmarshalJSON sets StringMap to a copy of data. +func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error) { + *stringMap, _, err = unmarshalStringMap[string](data) + return +} + +// unmarshalStringMapP unmarshals given json into a map[string]*V +func unmarshalStringMapP[V any](data []byte) (map[string]*V, *Origin, error) { + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + return nil, nil, err + } + + origin, err := deepCast[Origin](m[originKey]) + if err != nil { + return nil, nil, err + } + delete(m, originKey) + + result := make(map[string]*V, len(m)) + for k, v := range m { + value, err := deepCast[V](v) + if err != nil { + return nil, nil, err + } + result[k] = value + } + + return result, origin, nil +} + +// unmarshalStringMap unmarshals given json into a map[string]V +func unmarshalStringMap[V any](data []byte) (map[string]V, *Origin, error) { + var m map[string]any + if err := json.Unmarshal(data, &m); err != nil { + return nil, nil, err + } + + origin, err := deepCast[Origin](m[originKey]) + if err != nil { + return nil, nil, err + } + delete(m, originKey) + + result := make(map[string]V, len(m)) + for k, v := range m { + value, err := deepCast[V](v) + if err != nil { + return nil, nil, err + } + result[k] = *value + } + + return result, origin, nil +} + +// deepCast casts any value to a value of type V. +func deepCast[V any](value any) (*V, error) { + data, err := json.Marshal(value) + if err != nil { + return nil, err + } + + var result V + if err = json.Unmarshal(data, &result); err != nil { + return nil, err + } + return &result, nil +} diff --git a/openapi3/tag.go b/openapi3/tag.go index 182d0502..5e4086fe 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -34,6 +34,7 @@ func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error { // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tag-object type Tag struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -75,6 +76,7 @@ func (t *Tag) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "name") delete(x.Extensions, "description") delete(x.Extensions, "externalDocs") diff --git a/openapi3/testdata/origin/additional_properties.yaml b/openapi3/testdata/origin/additional_properties.yaml new file mode 100644 index 00000000..8e865bc9 --- /dev/null +++ b/openapi3/testdata/origin/additional_properties.yaml @@ -0,0 +1,20 @@ +openapi: 3.0.1 +info: + title: Test API + version: v1 +paths: + /partner-api/test/some-method: + get: + responses: + "200": + description: Success + content: + application/json: + schema: + additionalProperties: + type: object + properties: + code: + type: integer + text: + type: string \ No newline at end of file diff --git a/openapi3/testdata/origin/example.yaml b/openapi3/testdata/origin/example.yaml new file mode 100644 index 00000000..8b50ef26 --- /dev/null +++ b/openapi3/testdata/origin/example.yaml @@ -0,0 +1,19 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + examples: + bar: + summary: A bar example + value: {"bar": "baz"} + responses: + "200": + description: OK diff --git a/openapi3/testdata/origin/external_docs.yaml b/openapi3/testdata/origin/external_docs.yaml new file mode 100644 index 00000000..bfc5ba09 --- /dev/null +++ b/openapi3/testdata/origin/external_docs.yaml @@ -0,0 +1,15 @@ +openapi: 3.0.1 +info: + title: Test API + version: v1 +paths: + /partner-api/test/some-method: + get: + tags: + - Test + responses: + "200": + description: Success +externalDocs: + description: API Documentation + url: https://openweathermap.org/api \ No newline at end of file diff --git a/openapi3/testdata/origin/parameters.yaml b/openapi3/testdata/origin/parameters.yaml new file mode 100644 index 00000000..4d4299e2 --- /dev/null +++ b/openapi3/testdata/origin/parameters.yaml @@ -0,0 +1,19 @@ +info: + title: Tufin + version: 1.0.0 +openapi: 3.0.3 +paths: + /api/test: + get: + parameters: + - name: a + in: query + schema: + type: integer + responses: + 200: + description: OK + post: + responses: + 201: + description: OK diff --git a/openapi3/testdata/origin/request_body.yaml b/openapi3/testdata/origin/request_body.yaml new file mode 100644 index 00000000..09436384 --- /dev/null +++ b/openapi3/testdata/origin/request_body.yaml @@ -0,0 +1,22 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK diff --git a/openapi3/testdata/origin/security.yaml b/openapi3/testdata/origin/security.yaml new file mode 100644 index 00000000..7594e961 --- /dev/null +++ b/openapi3/testdata/origin/security.yaml @@ -0,0 +1,36 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + inProgressUrl: + type: string + failedUrl: + type: string + successUrl: + type: string + responses: + "200": + description: OK + security: + - petstore_auth: + - write:pets + - read:pets +components: + securitySchemes: + petstore_auth: + type: oauth2 + flows: + implicit: + authorizationUrl: http://example.org/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets \ No newline at end of file diff --git a/openapi3/testdata/origin/simple.yaml b/openapi3/testdata/origin/simple.yaml new file mode 100644 index 00000000..53f30805 --- /dev/null +++ b/openapi3/testdata/origin/simple.yaml @@ -0,0 +1,19 @@ +openapi: 3.0.1 +info: + title: Test API + version: v1 +paths: + /partner-api/test/some-method: + get: + tags: + - Test + responses: + "200": + description: Success + /partner-api/test/another-method: + get: + tags: + - Test + responses: + "200": + description: Success diff --git a/openapi3/testdata/origin/xml.yaml b/openapi3/testdata/origin/xml.yaml new file mode 100644 index 00000000..3193f32f --- /dev/null +++ b/openapi3/testdata/origin/xml.yaml @@ -0,0 +1,26 @@ +openapi: 3.0.0 +info: + title: Security Requirement Example + version: 1.0.0 +paths: + /subscribe: + post: + requestBody: + content: + application/json: + schema: + type: object + properties: + id: + type: integer + format: int32 + xml: + attribute: true + name: + type: string + xml: + namespace: http://example.com/schema/sample + prefix: sample + responses: + "200": + description: OK diff --git a/openapi3/xml.go b/openapi3/xml.go index 69d1b348..7acd8188 100644 --- a/openapi3/xml.go +++ b/openapi3/xml.go @@ -9,6 +9,7 @@ import ( // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xml-object type XML struct { Extensions map[string]any `json:"-" yaml:"-"` + Origin *Origin `json:"origin,omitempty" yaml:"origin,omitempty"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` @@ -58,6 +59,7 @@ func (xml *XML) UnmarshalJSON(data []byte) error { return unmarshalError(err) } _ = json.Unmarshal(data, &x.Extensions) + delete(x.Extensions, originKey) delete(x.Extensions, "name") delete(x.Extensions, "namespace") delete(x.Extensions, "prefix") diff --git a/openapi3filter/issue1045_test.go b/openapi3filter/issue1045_test.go new file mode 100644 index 00000000..048dbd37 --- /dev/null +++ b/openapi3filter/issue1045_test.go @@ -0,0 +1,142 @@ +package openapi3filter + +import ( + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/getkin/kin-openapi/routers/gorillamux" +) + +func TestIssue1045(t *testing.T) { + spec := ` +openapi: 3.0.3 +info: + version: 1.0.0 + title: sample api + description: api service paths to test the issue +paths: + /api/path: + post: + summary: path + tags: + - api + requestBody: + required: true + content: + application/json: + schema: { $ref: '#/components/schemas/PathRequest' } + application/x-www-form-urlencoded: + schema: { $ref: '#/components/schemas/PathRequest' } + responses: + '200': + description: Ok + content: + application/json: + schema: { $ref: '#/components/schemas/PathResponse' } +components: + schemas: + Msg_Opt: + properties: + msg: { type: string } + Msg: + allOf: + - $ref: '#/components/schemas/Msg_Opt' + - required: [ msg ] + Name: + properties: + name: { type: string } + required: [ name ] + Id: + properties: + id: + type: string + format: uint64 + required: [ id ] + PathRequest: + type: object + allOf: + - $ref: '#/components/schemas/Msg' + - $ref: '#/components/schemas/Name' + PathResponse: + type: object + allOf: + - $ref: '#/components/schemas/PathRequest' + - $ref: '#/components/schemas/Id' + `[1:] + + loader := openapi3.NewLoader() + + doc, err := loader.LoadFromData([]byte(spec)) + require.NoError(t, err) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + router, err := gorillamux.NewRouter(doc) + require.NoError(t, err) + + for _, testcase := range []struct { + name string + endpoint string + ct string + data string + shouldFail bool + }{ + { + name: "json success", + endpoint: "/api/path", + ct: "application/json", + data: `{"msg":"message", "name":"some+name"}`, + shouldFail: false, + }, + { + name: "json failure", + endpoint: "/api/path", + ct: "application/json", + data: `{"name":"some+name"}`, + shouldFail: true, + }, + + // application/x-www-form-urlencoded + { + name: "form success", + endpoint: "/api/path", + ct: "application/x-www-form-urlencoded", + data: "msg=message&name=some+name", + shouldFail: false, + }, + { + name: "form failure", + endpoint: "/api/path", + ct: "application/x-www-form-urlencoded", + data: "name=some+name", + shouldFail: true, + }, + } { + t.Run(testcase.name, func(t *testing.T) { + data := strings.NewReader(testcase.data) + req, err := http.NewRequest("POST", testcase.endpoint, data) + require.NoError(t, err) + req.Header.Add("Content-Type", testcase.ct) + + route, pathParams, err := router.FindRoute(req) + require.NoError(t, err) + + validationInput := &RequestValidationInput{ + Request: req, + PathParams: pathParams, + Route: route, + } + err = ValidateRequest(loader.Context, validationInput) + if testcase.shouldFail { + require.Error(t, err, "This test case should fail "+testcase.data) + } else { + require.NoError(t, err, "This test case should pass "+testcase.data) + } + }) + } +} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index e151c9ee..998a91f8 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -17,7 +17,7 @@ import ( "strconv" "strings" - "gopkg.in/yaml.v3" + "github.com/oasdiff/yaml3" "github.com/getkin/kin-openapi/openapi3" ) @@ -1264,6 +1264,9 @@ func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, func init() { RegisterBodyDecoder("application/json", JSONBodyDecoder) RegisterBodyDecoder("application/json-patch+json", JSONBodyDecoder) + RegisterBodyDecoder("application/ld+json", JSONBodyDecoder) + RegisterBodyDecoder("application/hal+json", JSONBodyDecoder) + RegisterBodyDecoder("application/vnd.api+json", JSONBodyDecoder) RegisterBodyDecoder("application/octet-stream", FileBodyDecoder) RegisterBodyDecoder("application/problem+json", JSONBodyDecoder) RegisterBodyDecoder("application/x-www-form-urlencoded", urlencodedBodyDecoder) @@ -1337,18 +1340,6 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. obj := make(map[string]any) dec := &urlValuesDecoder{values: values} - // Decode schema constructs (allOf, anyOf, oneOf) - if err := decodeSchemaConstructs(dec, schema.Value.AllOf, obj, encFn); err != nil { - return nil, err - } - if err := decodeSchemaConstructs(dec, schema.Value.AnyOf, obj, encFn); err != nil { - return nil, err - } - if err := decodeSchemaConstructs(dec, schema.Value.OneOf, obj, encFn); err != nil { - return nil, err - } - - // Decode properties from the main schema if err := decodeSchemaConstructs(dec, []*openapi3.SchemaRef{schema}, obj, encFn); err != nil { return nil, err } @@ -1360,6 +1351,18 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. // This function is for decoding purposes only and not for validation. func decodeSchemaConstructs(dec *urlValuesDecoder, schemas []*openapi3.SchemaRef, obj map[string]any, encFn EncodingFn) error { for _, schemaRef := range schemas { + + // Decode schema constructs (allOf, anyOf, oneOf) + if err := decodeSchemaConstructs(dec, schemaRef.Value.AllOf, obj, encFn); err != nil { + return err + } + if err := decodeSchemaConstructs(dec, schemaRef.Value.AnyOf, obj, encFn); err != nil { + return err + } + if err := decodeSchemaConstructs(dec, schemaRef.Value.OneOf, obj, encFn); err != nil { + return err + } + for name, prop := range schemaRef.Value.Properties { value, _, err := decodeProperty(dec, name, prop, encFn) if err != nil { diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index bf4771a9..a28fbccd 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -29,7 +29,7 @@ var ErrInvalidEmptyValue = errors.New("empty value is not allowed") // // Note: One can tune the behavior of uniqueItems: true verification // by registering a custom function with openapi3.RegisterArrayUniqueItemsChecker -func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err error) { +func ValidateRequest(ctx context.Context, input *RequestValidationInput) error { var me openapi3.MultiError options := input.Options @@ -49,10 +49,10 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er security = &route.Spec.Security } if security != nil { - if err = ValidateSecurityRequirements(ctx, input, *security); err != nil && !options.MultiError { - return - } - if err != nil { + if err := ValidateSecurityRequirements(ctx, input, *security); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -66,10 +66,10 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er } } - if err = ValidateParameter(ctx, input, parameter); err != nil && !options.MultiError { - return - } - if err != nil { + if err := ValidateParameter(ctx, input, parameter); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -79,10 +79,10 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er if options.ExcludeRequestQueryParams && parameter.Value.In == openapi3.ParameterInQuery { continue } - if err = ValidateParameter(ctx, input, parameter.Value); err != nil && !options.MultiError { - return - } - if err != nil { + if err := ValidateParameter(ctx, input, parameter.Value); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -90,10 +90,10 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er // RequestBody requestBody := operation.RequestBody if requestBody != nil && !options.ExcludeRequestBody { - if err = ValidateRequestBody(ctx, input, requestBody.Value); err != nil && !options.MultiError { - return - } - if err != nil { + if err := ValidateRequestBody(ctx, input, requestBody.Value); err != nil { + if !options.MultiError { + return err + } me = append(me, err) } } @@ -101,7 +101,7 @@ func ValidateRequest(ctx context.Context, input *RequestValidationInput) (err er if len(me) > 0 { return me } - return + return nil } // appendToQueryValues adds to query parameters each value in the provided slice @@ -114,7 +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 []interface{}: + case []any: 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 474fab36..82b09129 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -212,7 +212,7 @@ components: assert.NoError(t, err, "unable to read request body: %v", err) assert.Equal(t, contentLen, bodySize, "expect ContentLength %d to equal body size %d", contentLen, bodySize) bodyModified := originalBodySize != bodySize - assert.Equal(t, bodyModified, tc.expectedModification, "expect request body modification happened: %t, expected %t", bodyModified, tc.expectedModification) + assert.Equal(t, tc.expectedModification, bodyModified, "expect request body modification happened: %t, expected %t", bodyModified, tc.expectedModification) validationInput.Request.Body, err = validationInput.Request.GetBody() assert.NoError(t, err, "unable to re-generate body by GetBody(): %v", err)