diff --git a/.github/docs/openapi2.txt b/.github/docs/openapi2.txt index 328384100..aec6439de 100644 --- a/.github/docs/openapi2.txt +++ b/.github/docs/openapi2.txt @@ -20,7 +20,7 @@ func (header *Header) UnmarshalJSON(data []byte) error UnmarshalJSON sets Header to a copy of data. type Operation struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -43,7 +43,7 @@ func (operation *Operation) UnmarshalJSON(data []byte) error UnmarshalJSON sets Operation to a copy of data. type Parameter struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` @@ -61,7 +61,7 @@ type Parameter struct { ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` - Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` @@ -69,7 +69,7 @@ type Parameter struct { MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` - Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` } func (parameter Parameter) MarshalJSON() ([]byte, error) @@ -87,7 +87,7 @@ func (ps Parameters) Less(i, j int) bool func (ps Parameters) Swap(i, j int) type PathItem struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` @@ -114,14 +114,14 @@ func (pathItem *PathItem) UnmarshalJSON(data []byte) error UnmarshalJSON sets PathItem to a copy of data. type Response struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` - Examples map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Examples map[string]any `json:"examples,omitempty" yaml:"examples,omitempty"` } func (response Response) MarshalJSON() ([]byte, error) @@ -133,7 +133,7 @@ func (response *Response) UnmarshalJSON(data []byte) error type SecurityRequirements []map[string][]string type SecurityScheme struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` @@ -155,7 +155,7 @@ func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error UnmarshalJSON sets SecurityScheme to a copy of data. type T struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Swagger string `json:"swagger" yaml:"swagger"` // required Info openapi3.Info `json:"info" yaml:"info"` // required diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 7c950937b..3a7caf2c8 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -30,6 +30,15 @@ const ( // FormatOfStringForEmail pattern catches only some suspiciously wrong-looking email addresses. // Use DefineStringFormat(...) if you need something stricter. FormatOfStringForEmail = `^[^@]+@[^@<>",\s]+$` + + // FormatOfStringByte is a regexp for base64-encoded characters, for example, "U3dhZ2dlciByb2Nrcw==" + 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)$` + + // 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})?$` ) const ( SerializationSimple = "simple" @@ -43,6 +52,16 @@ const ( VARIABLES +var ( + IdentifierRegExp = regexp.MustCompile(`^[` + identifierChars + `]+$`) + InvalidIdentifierCharRegExp = regexp.MustCompile(`[^` + identifierChars + `]`) +) + IdentifierRegExp verifies whether Component object key matches contains just + 'identifierChars', according to OpenAPI v3.x. InvalidIdentifierCharRegExp + matches all characters not contained in 'identifierChars'. However, to be + able supporting legacy OpenAPI v2.x, there is a need to customize above + pattern in order not to fail converted v2-v3 validation + var ( // SchemaErrorDetailsDisabled disables printing of details about schema errors. SchemaErrorDetailsDisabled = false @@ -55,8 +74,14 @@ var ( // ErrSchemaInputInf may be returned when validating a number ErrSchemaInputInf = errors.New("floating point Inf is not allowed") ) -var CircularReferenceCounter = 3 -var CircularReferenceError = "kin-openapi bug found: circular schema reference not handled" +var ( + // SchemaStringFormats is a map of custom string format validators. + SchemaStringFormats = make(map[string]StringFormatValidator) + // SchemaNumberFormats is a map of custom number format validators. + SchemaNumberFormats = make(map[string]NumberFormatValidator) + // SchemaIntegerFormats is a map of custom integer format validators. + SchemaIntegerFormats = make(map[string]IntegerFormatValidator) +) var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile)) DefaultReadFromURI returns a caching ReadFromURIFunc which can read remote HTTP URIs and local file URIs. @@ -65,28 +90,28 @@ var ErrURINotSupported = errors.New("unsupported URI") ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a given URI. -var IdentifierRegExp = regexp.MustCompile(identifierPattern) - IdentifierRegExp verifies whether Component object key matches - 'identifierPattern' pattern, according to OpenAPI v3.x. However, to be able - supporting legacy OpenAPI v2.x, there is a need to customize above pattern - in order not to fail converted v2-v3 validation - -var SchemaStringFormats = make(map[string]Format, 4) - SchemaStringFormats allows for validating string formats - FUNCTIONS func BoolPtr(value bool) *bool BoolPtr is a helper for defining OpenAPI schemas. -func DefaultRefNameResolver(ref string) string +func DefaultRefNameResolver(doc *T, ref componentRef) string DefaultRefResolver is a default implementation of refNameResolver for the InternalizeRefs function. - If a reference points to an element inside a document, it returns the last - element in the reference using filepath.Base. Otherwise if the reference - points to a file, it returns the file name trimmed of all extensions. + The external reference is internalized to (hopefully) a unique name. + If the external reference matches (by path) to another reference in the root + document then the name of that component is used. + + The transformation involves: + - Cutting the "#/components/" part. + - Cutting the file extensions (.yaml/.json) from documents. + - Trimming the common directory with the root spec. + - Replace invalid characters with with underscores. + + This is an injective mapping over a "reasonable" amount of the possible + openapi spec domain space but is not perfect. There might be edge cases. func DefineIPv4Format() DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec @@ -94,12 +119,27 @@ func DefineIPv4Format() func DefineIPv6Format() DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec +func DefineIntegerFormatValidator(name string, validator IntegerFormatValidator) + DefineIntegerFormatValidator defines a custom format validator for a given + integer format. + +func DefineNumberFormatValidator(name string, validator NumberFormatValidator) + DefineNumberFormatValidator defines a custom format validator for a given + number format. + func DefineStringFormat(name string, pattern string) - DefineStringFormat defines a new regexp pattern for a given format + DefineStringFormat defines a regexp pattern for a given string + format Deprecated: Use openapi3.DefineStringFormatValidator(name, + NewRegexpFormatValidator(pattern)) instead. -func DefineStringFormatCallback(name string, callback FormatCallback) - DefineStringFormatCallback adds a validation function for a specific schema - format entry +func DefineStringFormatCallback(name string, callback func(string) error) + DefineStringFormatCallback defines a callback function for a given + string format Deprecated: Use openapi3.DefineStringFormatValidator(name, + NewCallbackValidator(fn)) instead. + +func DefineStringFormatValidator(name string, validator StringFormatValidator) + DefineStringFormatValidator defines a custom format validator for a given + string format. func Float64Ptr(value float64) *float64 Float64Ptr is a helper for defining OpenAPI schemas. @@ -110,6 +150,48 @@ func Int64Ptr(value int64) *int64 func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) ReadFromFile is a ReadFromURIFunc which reads local file URIs. +func ReferencesComponentInRootDocument(doc *T, ref componentRef) (string, bool) + ReferencesComponentInRootDocument returns if the given component reference + references the same document or element as another component reference in + the root document's '#/components/'. If it does, it returns the name + of it in the form '#/components//NameXXX' + + Of course given a component from the root document will always match itself. + + https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object + https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#relative-references-in-urls + + Example. Take the spec with directory structure: + + openapi.yaml + schemas/ + ├─ record.yaml + ├─ records.yaml + + In openapi.yaml we have: + + components: + schemas: + Record: + $ref: schemas/record.yaml + + Case 1: records.yml references a component in the root document + + $ref: ../openapi.yaml#/components/schemas/Record + + This would return... + + #/components/schemas/Record + + Case 2: records.yml indirectly refers to the same schema as a schema the + root document's '#/components/schemas'. + + $ref: ./record.yaml + + This would also return... + + #/components/schemas/Record + func RegisterArrayUniqueItemsChecker(fn SliceUniqueItemsChecker) RegisterArrayUniqueItemsChecker is used to register a customized function used to check if JSON array have unique items. @@ -136,14 +218,14 @@ type AdditionalProperties struct { func (addProps AdditionalProperties) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of AdditionalProperties. -func (addProps AdditionalProperties) MarshalYAML() (interface{}, error) +func (addProps AdditionalProperties) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of AdditionalProperties. func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error UnmarshalJSON sets AdditionalProperties to a copy of data. type Callback struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` // Has unexported fields. } @@ -159,7 +241,7 @@ func NewCallbackWithCapacity(cap int) *Callback func (callback *Callback) Delete(key string) Delete removes the entry associated with key 'key' from 'callback'. -func (callback Callback) JSONLookup(token string) (interface{}, error) +func (callback Callback) JSONLookup(token string) (any, error) JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable @@ -172,7 +254,7 @@ func (callback *Callback) Map() (m map[string]*PathItem) func (callback *Callback) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Callback. -func (callback *Callback) MarshalYAML() (interface{}, error) +func (callback *Callback) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Callback. func (callback *Callback) Set(key string, value *PathItem) @@ -189,23 +271,38 @@ func (callback *Callback) Value(key string) *PathItem Value returns the callback for key or nil type CallbackRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Callback + // Has unexported fields. } CallbackRef represents either a Callback or a $ref to a Callback. When serializing and both fields are set, Ref is preferred over Value. -func (x *CallbackRef) JSONLookup(token string) (interface{}, error) +func (x *CallbackRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *CallbackRef) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (x CallbackRef) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of CallbackRef. -func (x CallbackRef) MarshalYAML() (interface{}, error) +func (x CallbackRef) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of CallbackRef. +func (x *CallbackRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *CallbackRef) RefString() string + RefString returns the $ref value. + func (x *CallbackRef) UnmarshalJSON(data []byte) error UnmarshalJSON sets CallbackRef to a copy of data. @@ -215,12 +312,12 @@ func (x *CallbackRef) Validate(ctx context.Context, opts ...ValidationOption) er type Callbacks map[string]*CallbackRef -func (m Callbacks) JSONLookup(token string) (interface{}, error) +func (m Callbacks) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable type Components struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` @@ -240,7 +337,7 @@ func NewComponents() Components func (components Components) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Components. -func (components Components) MarshalYAML() (interface{}, error) +func (components Components) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Components. func (components *Components) UnmarshalJSON(data []byte) error @@ -251,7 +348,7 @@ func (components *Components) Validate(ctx context.Context, opts ...ValidationOp spec. type Contact struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -263,7 +360,7 @@ type Contact struct { func (contact Contact) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Contact. -func (contact Contact) MarshalYAML() (interface{}, error) +func (contact Contact) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Contact. func (contact *Contact) UnmarshalJSON(data []byte) error @@ -295,7 +392,7 @@ func (content Content) Validate(ctx context.Context, opts ...ValidationOption) e Validate returns an error if Content does not comply with the OpenAPI spec. type Discriminator struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` PropertyName string `json:"propertyName" yaml:"propertyName"` // required Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` @@ -306,7 +403,7 @@ type Discriminator struct { func (discriminator Discriminator) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Discriminator. -func (discriminator Discriminator) MarshalYAML() (interface{}, error) +func (discriminator Discriminator) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Discriminator. func (discriminator *Discriminator) UnmarshalJSON(data []byte) error @@ -317,7 +414,7 @@ func (discriminator *Discriminator) Validate(ctx context.Context, opts ...Valida spec. type Encoding struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -333,7 +430,7 @@ func NewEncoding() *Encoding func (encoding Encoding) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Encoding. -func (encoding Encoding) MarshalYAML() (interface{}, error) +func (encoding Encoding) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Encoding. func (encoding *Encoding) SerializationMethod() *SerializationMethod @@ -352,22 +449,22 @@ func (encoding *Encoding) WithHeader(name string, header *Header) *Encoding func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding type Example struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Value interface{} `json:"value,omitempty" yaml:"value,omitempty"` - ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Value any `json:"value,omitempty" yaml:"value,omitempty"` + ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` } Example is specified by OpenAPI/Swagger 3.0 standard. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object -func NewExample(value interface{}) *Example +func NewExample(value any) *Example func (example Example) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Example. -func (example Example) MarshalYAML() (interface{}, error) +func (example Example) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Example. func (example *Example) UnmarshalJSON(data []byte) error @@ -377,23 +474,38 @@ func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) Validate returns an error if Example does not comply with the OpenAPI spec. type ExampleRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Example + // Has unexported fields. } ExampleRef represents either a Example or a $ref to a Example. When serializing and both fields are set, Ref is preferred over Value. -func (x *ExampleRef) JSONLookup(token string) (interface{}, error) +func (x *ExampleRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *ExampleRef) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (x ExampleRef) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of ExampleRef. -func (x ExampleRef) MarshalYAML() (interface{}, error) +func (x ExampleRef) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of ExampleRef. +func (x *ExampleRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *ExampleRef) RefString() string + RefString returns the $ref value. + func (x *ExampleRef) UnmarshalJSON(data []byte) error UnmarshalJSON sets ExampleRef to a copy of data. @@ -403,12 +515,12 @@ func (x *ExampleRef) Validate(ctx context.Context, opts ...ValidationOption) err type Examples map[string]*ExampleRef -func (m Examples) JSONLookup(token string) (interface{}, error) +func (m Examples) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable type ExternalDocs struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Description string `json:"description,omitempty" yaml:"description,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -419,7 +531,7 @@ type ExternalDocs struct { func (e ExternalDocs) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of ExternalDocs. -func (e ExternalDocs) MarshalYAML() (interface{}, error) +func (e ExternalDocs) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of ExternalDocs. func (e *ExternalDocs) UnmarshalJSON(data []byte) error @@ -429,14 +541,22 @@ func (e *ExternalDocs) Validate(ctx context.Context, opts ...ValidationOption) e Validate returns an error if ExternalDocs does not comply with the OpenAPI spec. -type Format struct { - // Has unexported fields. +type FormatValidator[T any] interface { + Validate(value T) error } - Format represents a format validator registered by either DefineStringFormat - or DefineStringFormatCallback + FormatValidator is an interface for custom format validators. + +func NewCallbackValidator[T any](fn func(T) error) FormatValidator[T] + NewCallbackValidator creates a new FormatValidator that uses a callback + function to validate the value. -type FormatCallback func(value string) error - FormatCallback performs custom checks on exotic formats +func NewIPValidator(isIPv4 bool) FormatValidator[string] + NewIPValidator creates a new FormatValidator that validates the value is an + IP address. + +func NewRangeFormatValidator[T int64 | float64](min, max T) FormatValidator[T] + NewRangeFormatValidator creates a new FormatValidator that validates the + value is within a given range. type Header struct { Parameter @@ -444,14 +564,14 @@ type Header struct { Header is specified by OpenAPI/Swagger 3.0 standard. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#header-object -func (header Header) JSONLookup(token string) (interface{}, error) +func (header Header) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (header Header) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Header. -func (header Header) MarshalYAML() (interface{}, error) +func (header Header) MarshalYAML() (any, error) MarshalYAML returns the JSON encoding of Header. func (header *Header) SerializationMethod() (*SerializationMethod, error) @@ -464,23 +584,38 @@ func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) er Validate returns an error if Header does not comply with the OpenAPI spec. type HeaderRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Header + // Has unexported fields. } HeaderRef represents either a Header or a $ref to a Header. When serializing and both fields are set, Ref is preferred over Value. -func (x *HeaderRef) JSONLookup(token string) (interface{}, error) +func (x *HeaderRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *HeaderRef) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (x HeaderRef) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of HeaderRef. -func (x HeaderRef) MarshalYAML() (interface{}, error) +func (x HeaderRef) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of HeaderRef. +func (x *HeaderRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *HeaderRef) RefString() string + RefString returns the $ref value. + func (x *HeaderRef) UnmarshalJSON(data []byte) error UnmarshalJSON sets HeaderRef to a copy of data. @@ -490,12 +625,12 @@ func (x *HeaderRef) Validate(ctx context.Context, opts ...ValidationOption) erro type Headers map[string]*HeaderRef -func (m Headers) JSONLookup(token string) (interface{}, error) +func (m Headers) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable type Info struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -510,7 +645,7 @@ type Info struct { func (info Info) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Info. -func (info Info) MarshalYAML() (interface{}, error) +func (info *Info) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Info. func (info *Info) UnmarshalJSON(data []byte) error @@ -519,8 +654,11 @@ func (info *Info) UnmarshalJSON(data []byte) error func (info *Info) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Info does not comply with the OpenAPI spec. +type IntegerFormatValidator = FormatValidator[int64] + IntegerFormatValidator is a type alias for FormatValidator[int64] + type License struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -531,7 +669,7 @@ type License struct { func (license License) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of License. -func (license License) MarshalYAML() (interface{}, error) +func (license License) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of License. func (license *License) UnmarshalJSON(data []byte) error @@ -541,14 +679,14 @@ func (license *License) Validate(ctx context.Context, opts ...ValidationOption) Validate returns an error if License does not comply with the OpenAPI spec. type Link struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` - - OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` - OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Server *Server `json:"server,omitempty" yaml:"server,omitempty"` - RequestBody interface{} `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` + Extensions map[string]any `json:"-" yaml:"-"` + + OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` + OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Parameters map[string]any `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Server *Server `json:"server,omitempty" yaml:"server,omitempty"` + RequestBody any `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` } Link is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#link-object @@ -556,7 +694,7 @@ type Link struct { func (link Link) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Link. -func (link Link) MarshalYAML() (interface{}, error) +func (link Link) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Link. func (link *Link) UnmarshalJSON(data []byte) error @@ -566,23 +704,38 @@ func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Link does not comply with the OpenAPI spec. type LinkRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Link + // Has unexported fields. } LinkRef represents either a Link or a $ref to a Link. When serializing and both fields are set, Ref is preferred over Value. -func (x *LinkRef) JSONLookup(token string) (interface{}, error) +func (x *LinkRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *LinkRef) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (x LinkRef) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of LinkRef. -func (x LinkRef) MarshalYAML() (interface{}, error) +func (x LinkRef) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of LinkRef. +func (x *LinkRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *LinkRef) RefString() string + RefString returns the $ref value. + func (x *LinkRef) UnmarshalJSON(data []byte) error UnmarshalJSON sets LinkRef to a copy of data. @@ -591,7 +744,7 @@ func (x *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) error type Links map[string]*LinkRef -func (m Links) JSONLookup(token string) (interface{}, error) +func (m Links) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable @@ -635,10 +788,10 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) ResolveRefsIn expands references if for instance spec was just unmarshaled type MediaType struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` } @@ -647,14 +800,14 @@ type MediaType struct { func NewMediaType() *MediaType -func (mediaType MediaType) JSONLookup(token string) (interface{}, error) +func (mediaType MediaType) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (mediaType MediaType) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of MediaType. -func (mediaType MediaType) MarshalYAML() (interface{}, error) +func (mediaType MediaType) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of MediaType. func (mediaType *MediaType) UnmarshalJSON(data []byte) error @@ -666,7 +819,7 @@ func (mediaType *MediaType) Validate(ctx context.Context, opts ...ValidationOpti func (mediaType *MediaType) WithEncoding(name string, enc *Encoding) *MediaType -func (mediaType *MediaType) WithExample(name string, value interface{}) *MediaType +func (mediaType *MediaType) WithExample(name string, value any) *MediaType func (mediaType *MediaType) WithSchema(schema *Schema) *MediaType @@ -676,7 +829,7 @@ type MultiError []error MultiError is a collection of errors, intended for when multiple issues need to be reported upstream -func (me MultiError) As(target interface{}) bool +func (me MultiError) As(target any) bool As allows you to use `errors.As()` to set target to the first error within the multi error that matches the target type @@ -708,8 +861,11 @@ func WithName(name string, response *Response) NewResponsesOption func WithStatus(status int, responseRef *ResponseRef) NewResponsesOption WithStatus adds a status code keyed ResponseRef +type NumberFormatValidator = FormatValidator[float64] + NumberFormatValidator is a type alias for FormatValidator[float64] + type OAuthFlow struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` @@ -722,7 +878,7 @@ type OAuthFlow struct { func (flow OAuthFlow) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of OAuthFlow. -func (flow OAuthFlow) MarshalYAML() (interface{}, error) +func (flow OAuthFlow) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of OAuthFlow. func (flow *OAuthFlow) UnmarshalJSON(data []byte) error @@ -733,7 +889,7 @@ func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) e spec. type OAuthFlows struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` @@ -746,7 +902,7 @@ type OAuthFlows struct { func (flows OAuthFlows) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of OAuthFlows. -func (flows OAuthFlows) MarshalYAML() (interface{}, error) +func (flows OAuthFlows) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of OAuthFlows. func (flows *OAuthFlows) UnmarshalJSON(data []byte) error @@ -757,7 +913,7 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) spec. type Operation struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` // Optional tags for documentation. Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` @@ -803,14 +959,14 @@ func (operation *Operation) AddParameter(p *Parameter) func (operation *Operation) AddResponse(status int, response *Response) -func (operation Operation) JSONLookup(token string) (interface{}, error) +func (operation Operation) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (operation Operation) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Operation. -func (operation Operation) MarshalYAML() (interface{}, error) +func (operation Operation) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Operation. func (operation *Operation) UnmarshalJSON(data []byte) error @@ -821,21 +977,21 @@ func (operation *Operation) Validate(ctx context.Context, opts ...ValidationOpti spec. type Parameter struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` - - Name string `json:"name,omitempty" yaml:"name,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` + Extensions map[string]any `json:"-" yaml:"-"` + + Name string `json:"name,omitempty" yaml:"name,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` + Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` + Content Content `json:"content,omitempty" yaml:"content,omitempty"` } Parameter is specified by OpenAPI/Swagger 3.0 standard. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object @@ -848,14 +1004,14 @@ func NewPathParameter(name string) *Parameter func NewQueryParameter(name string) *Parameter -func (parameter Parameter) JSONLookup(token string) (interface{}, error) +func (parameter Parameter) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (parameter Parameter) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Parameter. -func (parameter Parameter) MarshalYAML() (interface{}, error) +func (parameter Parameter) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Parameter. func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error) @@ -877,23 +1033,38 @@ func (parameter *Parameter) WithRequired(value bool) *Parameter func (parameter *Parameter) WithSchema(value *Schema) *Parameter type ParameterRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Parameter + // Has unexported fields. } ParameterRef represents either a Parameter or a $ref to a Parameter. When serializing and both fields are set, Ref is preferred over Value. -func (x *ParameterRef) JSONLookup(token string) (interface{}, error) +func (x *ParameterRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *ParameterRef) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (x ParameterRef) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of ParameterRef. -func (x ParameterRef) MarshalYAML() (interface{}, error) +func (x ParameterRef) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of ParameterRef. +func (x *ParameterRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *ParameterRef) RefString() string + RefString returns the $ref value. + func (x *ParameterRef) UnmarshalJSON(data []byte) error UnmarshalJSON sets ParameterRef to a copy of data. @@ -908,7 +1079,7 @@ func NewParameters() Parameters func (parameters Parameters) GetByInAndName(in string, name string) *Parameter -func (p Parameters) JSONLookup(token string) (interface{}, error) +func (p Parameters) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable @@ -918,12 +1089,12 @@ func (parameters Parameters) Validate(ctx context.Context, opts ...ValidationOpt type ParametersMap map[string]*ParameterRef -func (m ParametersMap) JSONLookup(token string) (interface{}, error) +func (m ParametersMap) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable type PathItem struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` @@ -948,7 +1119,7 @@ func (pathItem *PathItem) GetOperation(method string) *Operation func (pathItem PathItem) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of PathItem. -func (pathItem PathItem) MarshalYAML() (interface{}, error) +func (pathItem PathItem) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of PathItem. func (pathItem *PathItem) Operations() map[string]*Operation @@ -962,7 +1133,7 @@ func (pathItem *PathItem) Validate(ctx context.Context, opts ...ValidationOption Validate returns an error if PathItem does not comply with the OpenAPI spec. type Paths struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` // Has unexported fields. } @@ -1000,7 +1171,7 @@ func (paths *Paths) InMatchingOrder() []string When matching URLs, concrete (non-templated) paths would be matched before their templated counterparts. -func (paths Paths) JSONLookup(token string) (interface{}, error) +func (paths Paths) JSONLookup(token string) (any, error) JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable @@ -1013,7 +1184,7 @@ func (paths *Paths) Map() (m map[string]*PathItem) func (paths *Paths) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Paths. -func (paths *Paths) MarshalYAML() (interface{}, error) +func (paths *Paths) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Paths. func (paths *Paths) Set(key string, value *PathItem) @@ -1056,16 +1227,21 @@ type Ref struct { Ref is specified by OpenAPI/Swagger 3.0 standard. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object -type RefNameResolver func(string) string +type RefNameResolver func(*T, componentRef) string + RefNameResolver maps a component to an name that is used as it's + internalized name. + + The function should avoid name collisions (i.e. be a injective mapping). It + must only contain characters valid for fixed field names: IdentifierRegExp. type RequestBodies map[string]*RequestBodyRef -func (m RequestBodies) JSONLookup(token string) (interface{}, error) +func (m RequestBodies) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable type RequestBody struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` @@ -1081,7 +1257,7 @@ func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType func (requestBody RequestBody) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of RequestBody. -func (requestBody RequestBody) MarshalYAML() (interface{}, error) +func (requestBody RequestBody) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of RequestBody. func (requestBody *RequestBody) UnmarshalJSON(data []byte) error @@ -1110,23 +1286,38 @@ func (requestBody *RequestBody) WithSchema(value *Schema, consumes []string) *Re func (requestBody *RequestBody) WithSchemaRef(value *SchemaRef, consumes []string) *RequestBody type RequestBodyRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *RequestBody + // Has unexported fields. } RequestBodyRef represents either a RequestBody or a $ref to a RequestBody. When serializing and both fields are set, Ref is preferred over Value. -func (x *RequestBodyRef) JSONLookup(token string) (interface{}, error) +func (x *RequestBodyRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *RequestBodyRef) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (x RequestBodyRef) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of RequestBodyRef. -func (x RequestBodyRef) MarshalYAML() (interface{}, error) +func (x RequestBodyRef) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of RequestBodyRef. +func (x *RequestBodyRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *RequestBodyRef) RefString() string + RefString returns the $ref value. + func (x *RequestBodyRef) UnmarshalJSON(data []byte) error UnmarshalJSON sets RequestBodyRef to a copy of data. @@ -1135,7 +1326,7 @@ func (x *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) spec. type Response struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -1150,7 +1341,7 @@ func NewResponse() *Response func (response Response) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Response. -func (response Response) MarshalYAML() (interface{}, error) +func (response Response) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Response. func (response *Response) UnmarshalJSON(data []byte) error @@ -1169,28 +1360,43 @@ func (response *Response) WithJSONSchemaRef(schema *SchemaRef) *Response type ResponseBodies map[string]*ResponseRef -func (m ResponseBodies) JSONLookup(token string) (interface{}, error) +func (m ResponseBodies) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable type ResponseRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Response + // Has unexported fields. } ResponseRef represents either a Response or a $ref to a Response. When serializing and both fields are set, Ref is preferred over Value. -func (x *ResponseRef) JSONLookup(token string) (interface{}, error) +func (x *ResponseRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *ResponseRef) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (x ResponseRef) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of ResponseRef. -func (x ResponseRef) MarshalYAML() (interface{}, error) +func (x ResponseRef) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of ResponseRef. +func (x *ResponseRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *ResponseRef) RefString() string + RefString returns the $ref value. + func (x *ResponseRef) UnmarshalJSON(data []byte) error UnmarshalJSON sets ResponseRef to a copy of data. @@ -1199,7 +1405,7 @@ func (x *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) er spec. type Responses struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` // Has unexported fields. } @@ -1220,7 +1426,7 @@ func (responses *Responses) Default() *ResponseRef func (responses *Responses) Delete(key string) Delete removes the entry associated with key 'key' from 'responses'. -func (responses Responses) JSONLookup(token string) (interface{}, error) +func (responses Responses) JSONLookup(token string) (any, error) JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable @@ -1233,7 +1439,7 @@ func (responses *Responses) Map() (m map[string]*ResponseRef) func (responses *Responses) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Responses. -func (responses *Responses) MarshalYAML() (interface{}, error) +func (responses *Responses) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Responses. func (responses *Responses) Set(key string, value *ResponseRef) @@ -1257,7 +1463,7 @@ func (responses *Responses) Value(key string) *ResponseRef Value returns the responses for key or nil type Schema struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` @@ -1267,9 +1473,9 @@ type Schema struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Format string `json:"format,omitempty" yaml:"format,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` - Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` - Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` // Array-related, here for struct compactness @@ -1344,26 +1550,26 @@ func NewUUIDSchema() *Schema func (schema *Schema) IsEmpty() bool IsEmpty tells whether schema is equivalent to the empty schema `{}`. -func (schema *Schema) IsMatching(value interface{}) bool +func (schema *Schema) IsMatching(value any) bool -func (schema *Schema) IsMatchingJSONArray(value []interface{}) bool +func (schema *Schema) IsMatchingJSONArray(value []any) bool func (schema *Schema) IsMatchingJSONBoolean(value bool) bool func (schema *Schema) IsMatchingJSONNumber(value float64) bool -func (schema *Schema) IsMatchingJSONObject(value map[string]interface{}) bool +func (schema *Schema) IsMatchingJSONObject(value map[string]any) bool func (schema *Schema) IsMatchingJSONString(value string) bool -func (schema Schema) JSONLookup(token string) (interface{}, error) +func (schema Schema) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (schema Schema) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Schema. -func (schema Schema) MarshalYAML() (interface{}, error) +func (schema Schema) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Schema. func (schema *Schema) NewRef() *SchemaRef @@ -1376,15 +1582,15 @@ func (schema *Schema) UnmarshalJSON(data []byte) error func (schema *Schema) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Schema does not comply with the OpenAPI spec. -func (schema *Schema) VisitJSON(value interface{}, opts ...SchemaValidationOption) error +func (schema *Schema) VisitJSON(value any, opts ...SchemaValidationOption) error -func (schema *Schema) VisitJSONArray(value []interface{}) error +func (schema *Schema) VisitJSONArray(value []any) error func (schema *Schema) VisitJSONBoolean(value bool) error func (schema *Schema) VisitJSONNumber(value float64) error -func (schema *Schema) VisitJSONObject(value map[string]interface{}) error +func (schema *Schema) VisitJSONObject(value map[string]any) error func (schema *Schema) VisitJSONString(value string) error @@ -1392,9 +1598,9 @@ func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema func (schema *Schema) WithAnyAdditionalProperties() *Schema -func (schema *Schema) WithDefault(defaultValue interface{}) *Schema +func (schema *Schema) WithDefault(defaultValue any) *Schema -func (schema *Schema) WithEnum(values ...interface{}) *Schema +func (schema *Schema) WithEnum(values ...any) *Schema func (schema *Schema) WithExclusiveMax(value bool) *Schema @@ -1446,7 +1652,7 @@ func (schema *Schema) WithoutAdditionalProperties() *Schema type SchemaError struct { // Value is the value that failed validation. - Value interface{} + Value any // Schema is the schema that failed validation. Schema *Schema @@ -1469,8 +1675,13 @@ func (err *SchemaError) JSONPointer() []string func (err SchemaError) Unwrap() error type SchemaRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Schema + // Has unexported fields. } SchemaRef represents either a Schema or a $ref to a Schema. When serializing @@ -1479,16 +1690,26 @@ type SchemaRef struct { func NewSchemaRef(ref string, value *Schema) *SchemaRef NewSchemaRef simply builds a SchemaRef -func (x *SchemaRef) JSONLookup(token string) (interface{}, error) +func (x *SchemaRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *SchemaRef) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (x SchemaRef) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of SchemaRef. -func (x SchemaRef) MarshalYAML() (interface{}, error) +func (x SchemaRef) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of SchemaRef. +func (x *SchemaRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *SchemaRef) RefString() string + RefString returns the $ref value. + func (x *SchemaRef) UnmarshalJSON(data []byte) error UnmarshalJSON sets SchemaRef to a copy of data. @@ -1498,7 +1719,7 @@ func (x *SchemaRef) Validate(ctx context.Context, opts ...ValidationOption) erro type SchemaRefs []*SchemaRef -func (s SchemaRefs) JSONLookup(token string) (interface{}, error) +func (s SchemaRefs) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable @@ -1543,7 +1764,7 @@ func VisitAsResponse() SchemaValidationOption type Schemas map[string]*SchemaRef -func (m Schemas) JSONLookup(token string) (interface{}, error) +func (m Schemas) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable @@ -1570,7 +1791,7 @@ func (srs SecurityRequirements) Validate(ctx context.Context, opts ...Validation func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) *SecurityRequirements type SecurityScheme struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -1595,7 +1816,7 @@ func NewSecurityScheme() *SecurityScheme func (ss SecurityScheme) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of SecurityScheme. -func (ss SecurityScheme) MarshalYAML() (interface{}, error) +func (ss SecurityScheme) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of SecurityScheme. func (ss *SecurityScheme) UnmarshalJSON(data []byte) error @@ -1618,24 +1839,39 @@ func (ss *SecurityScheme) WithScheme(value string) *SecurityScheme func (ss *SecurityScheme) WithType(value string) *SecurityScheme type SecuritySchemeRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *SecurityScheme + // Has unexported fields. } SecuritySchemeRef represents either a SecurityScheme or a $ref to a SecurityScheme. When serializing and both fields are set, Ref is preferred over Value. -func (x *SecuritySchemeRef) JSONLookup(token string) (interface{}, error) +func (x *SecuritySchemeRef) CollectionName() string + CollectionName returns the JSON string used for a collection of these + components. + +func (x *SecuritySchemeRef) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (x SecuritySchemeRef) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of SecuritySchemeRef. -func (x SecuritySchemeRef) MarshalYAML() (interface{}, error) +func (x SecuritySchemeRef) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of SecuritySchemeRef. +func (x *SecuritySchemeRef) RefPath() *url.URL + RefPath returns the path of the $ref relative to the root document. + +func (x *SecuritySchemeRef) RefString() string + RefString returns the $ref value. + func (x *SecuritySchemeRef) UnmarshalJSON(data []byte) error UnmarshalJSON sets SecuritySchemeRef to a copy of data. @@ -1645,7 +1881,7 @@ func (x *SecuritySchemeRef) Validate(ctx context.Context, opts ...ValidationOpti type SecuritySchemes map[string]*SecuritySchemeRef -func (m SecuritySchemes) JSONLookup(token string) (interface{}, error) +func (m SecuritySchemes) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable @@ -1657,7 +1893,7 @@ type SerializationMethod struct { parameters and body. type Server struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` URL string `json:"url" yaml:"url"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -1673,7 +1909,7 @@ func (server *Server) BasePath() (string, error) func (server Server) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Server. -func (server Server) MarshalYAML() (interface{}, error) +func (server Server) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Server. func (server Server) MatchRawURL(input string) ([]string, string, bool) @@ -1687,7 +1923,7 @@ func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (e Validate returns an error if Server does not comply with the OpenAPI spec. type ServerVariable struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` @@ -1699,7 +1935,7 @@ type ServerVariable struct { func (serverVariable ServerVariable) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of ServerVariable. -func (serverVariable ServerVariable) MarshalYAML() (interface{}, error) +func (serverVariable ServerVariable) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of ServerVariable. func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error @@ -1720,12 +1956,19 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) func (servers Servers) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Servers does not comply with the OpenAPI spec. -type SliceUniqueItemsChecker func(items []interface{}) bool +type SliceUniqueItemsChecker func(items []any) bool SliceUniqueItemsChecker is an function used to check if an given slice have unique items. +type StringFormatValidator = FormatValidator[string] + StringFormatValidator is a type alias for FormatValidator[string] + +func NewRegexpFormatValidator(pattern string) StringFormatValidator + NewRegexpFormatValidator creates a new FormatValidator that uses a regular + expression to validate the value. + type T struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` OpenAPI string `json:"openapi" yaml:"openapi"` // Required Components *Components `json:"components,omitempty" yaml:"components,omitempty"` @@ -1747,7 +1990,7 @@ func (doc *T) AddServer(server *Server) func (doc *T) AddServers(servers ...*Server) -func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string) +func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(*T, componentRef) string) InternalizeRefs removes all references to external files from the spec and moves them to the components section. @@ -1760,14 +2003,14 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri doc.InternalizeRefs(context.Background(), nil) -func (doc *T) JSONLookup(token string) (interface{}, error) +func (doc *T) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable func (doc *T) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of T. -func (doc *T) MarshalYAML() (interface{}, error) +func (doc *T) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of T. func (doc *T) UnmarshalJSON(data []byte) error @@ -1778,7 +2021,7 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error Validations Options can be provided to modify the validation behavior. type Tag struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -1790,7 +2033,7 @@ type Tag struct { func (t Tag) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of Tag. -func (t Tag) MarshalYAML() (interface{}, error) +func (t Tag) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of Tag. func (t *Tag) UnmarshalJSON(data []byte) error @@ -1815,7 +2058,7 @@ func (types *Types) Is(typ string) bool func (pTypes *Types) MarshalJSON() ([]byte, error) -func (pTypes *Types) MarshalYAML() (interface{}, error) +func (pTypes *Types) MarshalYAML() (any, error) func (types *Types) Permits(typ string) bool @@ -1827,6 +2070,12 @@ type ValidationOption func(options *ValidationOptions) ValidationOption allows the modification of how the OpenAPI document is validated. +func AllowExtensionsWithRef() ValidationOption + AllowExtensionsWithRef allows extensions (fields starting with 'x-') as + siblings for $ref fields. This is the default. Non-extension fields are + prohibited unless allowed explicitly with the AllowExtraSiblingFields + option. + func AllowExtraSiblingFields(fields ...string) ValidationOption AllowExtraSiblingFields called as AllowExtraSiblingFields("description") makes Validate not return an error when said field appears next to a $ref. @@ -1867,13 +2116,19 @@ func EnableSchemaPatternValidation() ValidationOption DisableSchemaPatternValidation. By default, schema pattern validation is enabled. +func ProhibitExtensionsWithRef() ValidationOption + ProhibitExtensionsWithRef causes the validation to return an error if + extensions (fields starting with 'x-') are found as siblings for $ref + fields. Non-extension fields are prohibited unless allowed explicitly with + the AllowExtraSiblingFields option. + type ValidationOptions struct { // Has unexported fields. } ValidationOptions provides configuration for validating OpenAPI documents. type XML struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` @@ -1887,7 +2142,7 @@ type XML struct { func (xml XML) MarshalJSON() ([]byte, error) MarshalJSON returns the JSON encoding of XML. -func (xml XML) MarshalYAML() (interface{}, error) +func (xml XML) MarshalYAML() (any, error) MarshalYAML returns the YAML encoding of XML. func (xml *XML) UnmarshalJSON(data []byte) error diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt index 43540c416..626cd24a7 100644 --- a/.github/docs/openapi3filter.txt +++ b/.github/docs/openapi3filter.txt @@ -51,10 +51,10 @@ func DefaultErrorEncoder(_ context.Context, err error, w http.ResponseWriter) encoded form of the error will be used. If the error implements StatusCoder, the provided StatusCode will be used instead of 500. -func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) +func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (any, error) FileBodyDecoder is a body decoder that decodes a file body to a string. -func JSONBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) +func JSONBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (any, error) JSONBodyDecoder decodes a JSON formatted body. It is public so that is easy to register additional JSON based formats. @@ -139,10 +139,10 @@ type AuthenticationInput struct { func (input *AuthenticationInput) NewError(err error) error -type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (interface{}, error) +type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (any, error) BodyDecoder is an interface to decode a body of a request or response. - An implementation must return a value that is a primitive, []interface{}, - or map[string]interface{}. + An implementation must return a value that is a primitive, []any, + or map[string]any. func RegisteredBodyDecoder(contentType string) BodyDecoder RegisteredBodyDecoder returns the registered body decoder for the given @@ -152,7 +152,7 @@ func RegisteredBodyDecoder(contentType string) BodyDecoder This call is not thread-safe: body decoders should not be created/destroyed by multiple goroutines. -type BodyEncoder func(body interface{}) ([]byte, error) +type BodyEncoder func(body any) ([]byte, error) BodyEncoder really is an (encoding/json).Marshaler func RegisteredBodyEncoder(contentType string) BodyEncoder @@ -161,7 +161,7 @@ func RegisteredBodyEncoder(contentType string) BodyEncoder If no encoder was registered for the given content type, nil is returned. -type ContentParameterDecoder func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error) +type ContentParameterDecoder func(param *openapi3.Parameter, values []string) (any, *openapi3.Schema, error) A ContentParameterDecoder takes a parameter definition from the OpenAPI spec, and the value which we received for it. It is expected to return the value unmarshaled into an interface which can be traversed for validation, @@ -243,7 +243,7 @@ func (o *Options) WithCustomSchemaErrorFunc(f CustomSchemaErrorFunc) type ParseError struct { Kind ParseErrorKind - Value interface{} + Value any Reason string Cause error @@ -254,7 +254,7 @@ type ParseError struct { func (e *ParseError) Error() string -func (e *ParseError) Path() []interface{} +func (e *ParseError) Path() []any Path returns a path to the root cause. func (e *ParseError) RootCause() error diff --git a/.github/docs/openapi3gen.txt b/.github/docs/openapi3gen.txt index 0626e8721..e8543a47d 100644 --- a/.github/docs/openapi3gen.txt +++ b/.github/docs/openapi3gen.txt @@ -9,7 +9,7 @@ var RefSchemaRef = openapi3.NewSchemaRef("Ref", FUNCTIONS -func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) +func NewSchemaRefForValue(value any, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) NewSchemaRefForValue is a shortcut for NewGenerator(...).NewSchemaRefForValue(...) @@ -48,7 +48,7 @@ func NewGenerator(opts ...Option) *Generator func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, error) -func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas) (*openapi3.SchemaRef, error) +func (g *Generator) NewSchemaRefForValue(value any, schemas openapi3.Schemas) (*openapi3.SchemaRef, error) NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef, and updates a supplied map with any dependent component schemas if they lead to cycles diff --git a/.github/docs/routers_legacy_pathpattern.txt b/.github/docs/routers_legacy_pathpattern.txt index 27967330b..db79f7ac7 100644 --- a/.github/docs/routers_legacy_pathpattern.txt +++ b/.github/docs/routers_legacy_pathpattern.txt @@ -50,17 +50,17 @@ TYPES type Node struct { VariableNames []string - Value interface{} + Value any Suffixes SuffixList } -func (currentNode *Node) Add(path string, value interface{}, options *Options) error +func (currentNode *Node) Add(path string, value any, options *Options) error func (currentNode *Node) CreateNode(path string, options *Options) (*Node, error) func (currentNode *Node) Match(path string) (*Node, []string) -func (currentNode *Node) MustAdd(path string, value interface{}, options *Options) +func (currentNode *Node) MustAdd(path string, value any, options *Options) func (currentNode *Node) String() string diff --git a/README.md b/README.md index aea156c0d..976075c00 100644 --- a/README.md +++ b/README.md @@ -167,8 +167,8 @@ func main() { } } -func xmlBodyDecoder(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn openapi3filter.EncodingFn) (decoded interface{}, err error) { - // Decode body to a primitive, []interface{}, or map[string]interface{}. +func xmlBodyDecoder(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn openapi3filter.EncodingFn) (decoded any, err error) { + // Decode body to a primitive, []any, or map[string]any. } ``` @@ -177,7 +177,7 @@ func xmlBodyDecoder(body io.Reader, h http.Header, schema *openapi3.SchemaRef, e By default, the library checks unique items using the following predefined function: ```go -func isSliceOfUniqueItems(xs []interface{}) bool { +func isSliceOfUniqueItems(xs []any) bool { s := len(xs) m := make(map[string]struct{}, s) for _, x := range xs { @@ -203,7 +203,7 @@ func main() { // ... other validate codes } -func arrayUniqueItemsChecker(items []interface{}) bool { +func arrayUniqueItemsChecker(items []any) bool { // Check the uniqueness of the input slice } ``` @@ -276,11 +276,45 @@ func safeErrorMessage(err *openapi3.SchemaError) string { This will change the schema validation errors to return only the `Reason` field, which is guaranteed to not include the original value. +## Reconciling component $ref types + +`ReferencesComponentInRootDocument` is a useful helper function to check if a component reference +coincides with a reference in the root document's component objects fixed fields. + +This can be used to determine if two schema definitions are of the same structure, helpful for +code generation tools when generating go type models. + +```go +doc, err = loader.LoadFromFile("openapi.yml") + +for _, path := range doc.Paths.InMatchingOrder() { + pathItem := doc.Paths.Find(path) + + if pathItem.Get == nil || pathItem.Get.Responses.Status(200) { + continue + } + + for _, s := range pathItem.Get.Responses.Status(200).Value.Content { + name, match := ReferencesComponentInRootDocument(doc, s.Schema) + fmt.Println(path, match, name) // /record true #/components/schemas/BookRecord + } +} +``` + ## CHANGELOG: Sub-v1 breaking API changes +### v0.126.0 +* `openapi3.CircularReferenceError` and `openapi3.CircularReferenceCounter` are removed. `openapi3.Loader` now implements reference backtracking, so any kind of circular references should be properly resolved. +* `InternalizeRefs` now takes a refNameResolver that has access to `openapi3.T` and more properties of the reference needing resolving. +* The `DefaultRefNameResolver` has been updated, choosing names that will be less likely to collide with each other. Because of this internalized specs will likely change slightly. +* `openapi3.Format` and `openapi3.FormatCallback` are removed and the type of `openapi3.SchemaStringFormats` has changed. + ### v0.125.0 * The `openapi3filter.ErrFunc` and `openapi3filter.LogFunc` func types now take the validated request's context as first argument. +### v0.124.0 +* `openapi3.Schema.Type` & `openapi2.Parameter.Type` fields went from a `string` to the type `*Type` with methods: `Includes`, `Is`, `Permits` & `Slice`. + ### v0.122.0 * `Paths` field of `openapi3.T` is now a pointer * `Responses` field of `openapi3.Operation` is now a pointer @@ -299,7 +333,7 @@ This will change the schema validation errors to return only the `Reason` field, * The string format `email` has been removed by default. To use it please call `openapi3.DefineStringFormat("email", openapi3.FormatOfStringForEmail)`. * Field `openapi3.T.Components` is now a pointer. * Fields `openapi3.Schema.AdditionalProperties` and `openapi3.Schema.AdditionalPropertiesAllowed` are replaced by `openapi3.Schema.AdditionalProperties.Schema` and `openapi3.Schema.AdditionalProperties.Has` respectively. -* Type `openapi3.ExtensionProps` is now just `map[string]interface{}` and extensions are accessible through the `Extensions` field. +* Type `openapi3.ExtensionProps` is now just `map[string]any` and extensions are accessible through the `Extensions` field. ### v0.112.0 * `(openapi3.ValidationOptions).ExamplesValidationDisabled` has been unexported. diff --git a/cmd/validate/main.go b/cmd/validate/main.go index a0580967b..874f30c26 100644 --- a/cmd/validate/main.go +++ b/cmd/validate/main.go @@ -12,11 +12,6 @@ import ( "github.com/getkin/kin-openapi/openapi3" ) -var ( - defaultCircular = openapi3.CircularReferenceCounter - circular = flag.Int("circular", defaultCircular, "bump this (upper) limit when there's trouble with cyclic schema references") -) - var ( defaultDefaults = true defaults = flag.Bool("defaults", defaultDefaults, "when false, disables schemas' default field validation") @@ -59,7 +54,6 @@ func main() { switch { case vd.OpenAPI == "3" || strings.HasPrefix(vd.OpenAPI, "3."): - openapi3.CircularReferenceCounter = *circular loader := openapi3.NewLoader() loader.IsExternalRefsAllowed = *ext @@ -90,9 +84,6 @@ func main() { case vd.OpenAPI == "2" || strings.HasPrefix(vd.OpenAPI, "2."), vd.Swagger == "2" || strings.HasPrefix(vd.Swagger, "2."): - if *circular != defaultCircular { - log.Fatal("Flag --circular is only for OpenAPIv3") - } if *defaults != defaultDefaults { log.Fatal("Flag --defaults is only for OpenAPIv3") } diff --git a/go.mod b/go.mod index 3d6ff7e83..10abe40b4 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,18 @@ module github.com/getkin/kin-openapi go 1.20 require ( - github.com/go-openapi/jsonpointer v0.20.2 + github.com/go-openapi/jsonpointer v0.21.0 github.com/gorilla/mux v1.8.1 - github.com/invopop/yaml v0.2.0 + github.com/invopop/yaml v0.3.1 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/perimeterx/marshmallow v1.1.5 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-openapi/swag v0.22.8 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect diff --git a/go.sum b/go.sum index 82537b7ad..fc84c3190 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= -github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= -github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= -github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= -github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= +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= @@ -23,11 +23,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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= 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/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 7e335b01c..9cfd0ffdc 100755 --- a/maps.sh +++ b/maps.sh @@ -138,7 +138,7 @@ maplike_Pointable() { var _ jsonpointer.JSONPointable = (${type})(nil) // JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable -func (${name} ${type#'*'}) JSONLookup(token string) (interface{}, error) { +func (${name} ${type#'*'}) JSONLookup(token string) (any, error) { if v := ${name}.Value(token); v == nil { vv, _, err := jsonpointer.GetForToken(${name}.Extensions, token) return vv, err @@ -155,10 +155,17 @@ EOF maplike_UnMarsh() { + if [[ "$type" != '*'* ]]; then + echo "TODO: impl non-pointer receiver YAML Marshaler" + exit 2 + fi cat <>"$maplike" // MarshalYAML returns the YAML encoding of ${type#'*'}. -func (${name} ${type}) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, ${name}.Len()+len(${name}.Extensions)) +func (${name} ${type}) MarshalYAML() (any, error) { + if ${name} == nil { + return nil, nil + } + m := make(map[string]any, ${name}.Len()+len(${name}.Extensions)) for k, v := range ${name}.Extensions { m[k] = v } @@ -179,7 +186,7 @@ func (${name} ${type}) MarshalJSON() ([]byte, error) { // UnmarshalJSON sets ${type#'*'} to a copy of data. func (${name} ${type}) UnmarshalJSON(data []byte) (err error) { - var m map[string]interface{} + var m map[string]any if err = json.Unmarshal(data, &m); err != nil { return } @@ -191,7 +198,7 @@ func (${name} ${type}) UnmarshalJSON(data []byte) (err error) { sort.Strings(ks) x := ${type#'*'}{ - Extensions: make(map[string]interface{}), + Extensions: make(map[string]any), m: make(map[string]${value_type}, len(m)), } diff --git a/openapi2/marsh.go b/openapi2/marsh.go index 5aa162a72..1a4b0d1d0 100644 --- a/openapi2/marsh.go +++ b/openapi2/marsh.go @@ -16,7 +16,7 @@ func unmarshalError(jsonUnmarshalErr error) error { return jsonUnmarshalErr } -func unmarshal(data []byte, v interface{}) error { +func unmarshal(data []byte, v any) error { var jsonErr, yamlErr error // See https://github.com/getkin/kin-openapi/issues/680 diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go index 03a72cb73..2d922c639 100644 --- a/openapi2/openapi2.go +++ b/openapi2/openapi2.go @@ -8,7 +8,7 @@ import ( // T is the root of an OpenAPI v2 document type T struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Swagger string `json:"swagger" yaml:"swagger"` // required Info openapi3.Info `json:"info" yaml:"info"` // required @@ -29,7 +29,7 @@ type T struct { // MarshalJSON returns the JSON encoding of T. func (doc T) MarshalJSON() ([]byte, error) { - m := make(map[string]interface{}, 15+len(doc.Extensions)) + m := make(map[string]any, 15+len(doc.Extensions)) for k, v := range doc.Extensions { m[k] = v } diff --git a/openapi2/operation.go b/openapi2/operation.go index e50f21eb0..64f10d1f1 100644 --- a/openapi2/operation.go +++ b/openapi2/operation.go @@ -7,7 +7,7 @@ import ( ) type Operation struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -25,7 +25,7 @@ type Operation struct { // MarshalJSON returns the JSON encoding of Operation. func (operation Operation) MarshalJSON() ([]byte, error) { - m := make(map[string]interface{}, 12+len(operation.Extensions)) + m := make(map[string]any, 12+len(operation.Extensions)) for k, v := range operation.Extensions { m[k] = v } diff --git a/openapi2/parameter.go b/openapi2/parameter.go index 1203853f2..c701705bb 100644 --- a/openapi2/parameter.go +++ b/openapi2/parameter.go @@ -24,7 +24,7 @@ func (ps Parameters) Less(i, j int) bool { } type Parameter struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` @@ -42,7 +42,7 @@ type Parameter struct { ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` Items *openapi3.SchemaRef `json:"items,omitempty" yaml:"items,omitempty"` - Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` @@ -50,7 +50,7 @@ type Parameter struct { MaxItems *uint64 `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` MinLength uint64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` MinItems uint64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` - Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` } // MarshalJSON returns the JSON encoding of Parameter. @@ -59,7 +59,7 @@ func (parameter Parameter) MarshalJSON() ([]byte, error) { return json.Marshal(openapi3.Ref{Ref: ref}) } - m := make(map[string]interface{}, 24+len(parameter.Extensions)) + m := make(map[string]any, 24+len(parameter.Extensions)) for k, v := range parameter.Extensions { m[k] = v } diff --git a/openapi2/path_item.go b/openapi2/path_item.go index a365f0c50..624cc74dc 100644 --- a/openapi2/path_item.go +++ b/openapi2/path_item.go @@ -9,7 +9,7 @@ import ( ) type PathItem struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` @@ -29,7 +29,7 @@ func (pathItem PathItem) MarshalJSON() ([]byte, error) { return json.Marshal(openapi3.Ref{Ref: ref}) } - m := make(map[string]interface{}, 8+len(pathItem.Extensions)) + m := make(map[string]any, 8+len(pathItem.Extensions)) for k, v := range pathItem.Extensions { m[k] = v } diff --git a/openapi2/response.go b/openapi2/response.go index b4aa7fe85..5306beb15 100644 --- a/openapi2/response.go +++ b/openapi2/response.go @@ -7,14 +7,14 @@ import ( ) type Response struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` - Examples map[string]interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Schema *openapi3.SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Headers map[string]*Header `json:"headers,omitempty" yaml:"headers,omitempty"` + Examples map[string]any `json:"examples,omitempty" yaml:"examples,omitempty"` } // MarshalJSON returns the JSON encoding of Response. @@ -23,7 +23,7 @@ func (response Response) MarshalJSON() ([]byte, error) { return json.Marshal(openapi3.Ref{Ref: ref}) } - m := make(map[string]interface{}, 4+len(response.Extensions)) + m := make(map[string]any, 4+len(response.Extensions)) for k, v := range response.Extensions { m[k] = v } diff --git a/openapi2/security_scheme.go b/openapi2/security_scheme.go index d81d8e7c4..cd6e6a5dd 100644 --- a/openapi2/security_scheme.go +++ b/openapi2/security_scheme.go @@ -9,7 +9,7 @@ import ( type SecurityRequirements []map[string][]string type SecurityScheme struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` @@ -30,7 +30,7 @@ func (securityScheme SecurityScheme) MarshalJSON() ([]byte, error) { return json.Marshal(openapi3.Ref{Ref: ref}) } - m := make(map[string]interface{}, 10+len(securityScheme.Extensions)) + m := make(map[string]any, 10+len(securityScheme.Extensions)) for k, v := range securityScheme.Extensions { m[k] = v } diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index a79f72cd3..2e22f1394 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -236,7 +236,7 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete } if parameter.Name != "" { if result.Extensions == nil { - result.Extensions = make(map[string]interface{}, 1) + result.Extensions = make(map[string]any, 1) } result.Extensions["x-originalParamName"] = parameter.Name } @@ -252,7 +252,7 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete format, typ = "binary", &openapi3.Types{"string"} } if parameter.Extensions == nil { - parameter.Extensions = make(map[string]interface{}, 1) + parameter.Extensions = make(map[string]any, 1) } parameter.Extensions["x-formData-name"] = parameter.Name var required []string @@ -828,7 +828,7 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components if schema.Value.PermitsNull() { schema.Value.Nullable = false if schema.Value.Extensions == nil { - schema.Value.Extensions = make(map[string]interface{}) + schema.Value.Extensions = make(map[string]any) } schema.Value.Extensions["x-nullable"] = true } @@ -1184,7 +1184,7 @@ var attemptedBodyParameterNames = []string{ } // stripNonExtensions removes invalid extensions: those not prefixed by "x-" and returns them -func stripNonExtensions(extensions map[string]interface{}) map[string]interface{} { +func stripNonExtensions(extensions map[string]any) map[string]any { for extName := range extensions { if !strings.HasPrefix(extName, "x-") { delete(extensions, extName) @@ -1193,7 +1193,7 @@ func stripNonExtensions(extensions map[string]interface{}) map[string]interface{ return extensions } -func addPathExtensions(doc2 *openapi2.T, path string, extensions map[string]interface{}) { +func addPathExtensions(doc2 *openapi2.T, path string, extensions map[string]any) { if doc2.Paths == nil { doc2.Paths = make(map[string]*openapi2.PathItem) } diff --git a/openapi3/callback.go b/openapi3/callback.go index 13532b15c..34a6bea35 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -8,7 +8,7 @@ import ( // Callback is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callback-object type Callback struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` m map[string]*PathItem } diff --git a/openapi3/components.go b/openapi3/components.go index c46663273..98c4b96c1 100644 --- a/openapi3/components.go +++ b/openapi3/components.go @@ -24,7 +24,7 @@ type ( // Components is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#components-object type Components struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"` Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"` @@ -51,8 +51,8 @@ func (components Components) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Components. -func (components Components) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 9+len(components.Extensions)) +func (components Components) MarshalYAML() (any, error) { + m := make(map[string]any, 9+len(components.Extensions)) for k, v := range components.Extensions { m[k] = v } @@ -255,7 +255,7 @@ func (components *Components) Validate(ctx context.Context, opts ...ValidationOp var _ jsonpointer.JSONPointable = (*Schemas)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (m Schemas) JSONLookup(token string) (interface{}, error) { +func (m Schemas) JSONLookup(token string) (any, error) { if v, ok := m[token]; !ok || v == nil { return nil, fmt.Errorf("no schema %q", token) } else if ref := v.Ref; ref != "" { @@ -268,7 +268,7 @@ func (m Schemas) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*ParametersMap)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (m ParametersMap) JSONLookup(token string) (interface{}, error) { +func (m ParametersMap) JSONLookup(token string) (any, error) { if v, ok := m[token]; !ok || v == nil { return nil, fmt.Errorf("no parameter %q", token) } else if ref := v.Ref; ref != "" { @@ -281,7 +281,7 @@ func (m ParametersMap) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*Headers)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (m Headers) JSONLookup(token string) (interface{}, error) { +func (m Headers) JSONLookup(token string) (any, error) { if v, ok := m[token]; !ok || v == nil { return nil, fmt.Errorf("no header %q", token) } else if ref := v.Ref; ref != "" { @@ -294,7 +294,7 @@ func (m Headers) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (m RequestBodies) JSONLookup(token string) (interface{}, error) { +func (m RequestBodies) JSONLookup(token string) (any, error) { if v, ok := m[token]; !ok || v == nil { return nil, fmt.Errorf("no request body %q", token) } else if ref := v.Ref; ref != "" { @@ -307,7 +307,7 @@ func (m RequestBodies) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (m ResponseBodies) JSONLookup(token string) (interface{}, error) { +func (m ResponseBodies) JSONLookup(token string) (any, error) { if v, ok := m[token]; !ok || v == nil { return nil, fmt.Errorf("no response body %q", token) } else if ref := v.Ref; ref != "" { @@ -320,7 +320,7 @@ func (m ResponseBodies) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (m SecuritySchemes) JSONLookup(token string) (interface{}, error) { +func (m SecuritySchemes) JSONLookup(token string) (any, error) { if v, ok := m[token]; !ok || v == nil { return nil, fmt.Errorf("no security scheme body %q", token) } else if ref := v.Ref; ref != "" { @@ -333,7 +333,7 @@ func (m SecuritySchemes) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*Examples)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (m Examples) JSONLookup(token string) (interface{}, error) { +func (m Examples) JSONLookup(token string) (any, error) { if v, ok := m[token]; !ok || v == nil { return nil, fmt.Errorf("no example body %q", token) } else if ref := v.Ref; ref != "" { @@ -346,7 +346,7 @@ func (m Examples) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*Links)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (m Links) JSONLookup(token string) (interface{}, error) { +func (m Links) JSONLookup(token string) (any, error) { if v, ok := m[token]; !ok || v == nil { return nil, fmt.Errorf("no link body %q", token) } else if ref := v.Ref; ref != "" { @@ -359,7 +359,7 @@ func (m Links) JSONLookup(token string) (interface{}, error) { var _ jsonpointer.JSONPointable = (*Callbacks)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (m Callbacks) JSONLookup(token string) (interface{}, error) { +func (m Callbacks) JSONLookup(token string) (any, error) { if v, ok := m[token]; !ok || v == nil { return nil, fmt.Errorf("no callback body %q", token) } else if ref := v.Ref; ref != "" { diff --git a/openapi3/contact.go b/openapi3/contact.go index 7b707ce39..6c76a6fb6 100644 --- a/openapi3/contact.go +++ b/openapi3/contact.go @@ -8,7 +8,7 @@ import ( // Contact is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contact-object type Contact struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -25,8 +25,8 @@ func (contact Contact) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Contact. -func (contact Contact) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 3+len(contact.Extensions)) +func (contact Contact) MarshalYAML() (any, error) { + m := make(map[string]any, 3+len(contact.Extensions)) for k, v := range contact.Extensions { m[k] = v } diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index ae36f416d..e8193bd90 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -8,7 +8,7 @@ import ( // Discriminator is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object type Discriminator struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` PropertyName string `json:"propertyName" yaml:"propertyName"` // required Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"` @@ -24,8 +24,8 @@ func (discriminator Discriminator) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Discriminator. -func (discriminator Discriminator) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 2+len(discriminator.Extensions)) +func (discriminator Discriminator) MarshalYAML() (any, error) { + m := make(map[string]any, 2+len(discriminator.Extensions)) for k, v := range discriminator.Extensions { m[k] = v } diff --git a/openapi3/encoding.go b/openapi3/encoding.go index 57d20ee3d..1bcdaea5e 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -10,7 +10,7 @@ import ( // Encoding is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encoding-object type Encoding struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -49,8 +49,8 @@ func (encoding Encoding) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Encoding. -func (encoding Encoding) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 5+len(encoding.Extensions)) +func (encoding Encoding) MarshalYAML() (any, error) { + m := make(map[string]any, 5+len(encoding.Extensions)) for k, v := range encoding.Extensions { m[k] = v } diff --git a/openapi3/errors.go b/openapi3/errors.go index 74baab9a5..010dc889a 100644 --- a/openapi3/errors.go +++ b/openapi3/errors.go @@ -39,7 +39,7 @@ func (me MultiError) Is(target error) bool { } // As allows you to use `errors.As()` to set target to the first error within the multi error that matches the target type -func (me MultiError) As(target interface{}) bool { +func (me MultiError) As(target any) bool { for _, e := range me { if errors.As(e, target) { return true diff --git a/openapi3/example.go b/openapi3/example.go index a1a5a2b35..f9a7a6b07 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -9,15 +9,15 @@ import ( // Example is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object type Example struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` - Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Value interface{} `json:"value,omitempty" yaml:"value,omitempty"` - ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` + Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Value any `json:"value,omitempty" yaml:"value,omitempty"` + ExternalValue string `json:"externalValue,omitempty" yaml:"externalValue,omitempty"` } -func NewExample(value interface{}) *Example { +func NewExample(value any) *Example { return &Example{Value: value} } @@ -31,8 +31,8 @@ func (example Example) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Example. -func (example Example) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 4+len(example.Extensions)) +func (example Example) MarshalYAML() (any, error) { + m := make(map[string]any, 4+len(example.Extensions)) for k, v := range example.Extensions { m[k] = v } diff --git a/openapi3/example_validation.go b/openapi3/example_validation.go index fb7a1da16..0d105c92d 100644 --- a/openapi3/example_validation.go +++ b/openapi3/example_validation.go @@ -2,7 +2,7 @@ package openapi3 import "context" -func validateExampleValue(ctx context.Context, input interface{}, schema *Schema) error { +func validateExampleValue(ctx context.Context, input any, schema *Schema) error { opts := make([]SchemaValidationOption, 0, 2) if vo := getValidationOptions(ctx); vo.examplesValidationAsReq { diff --git a/openapi3/extension.go b/openapi3/extension.go index 37f6b01e5..ca86078f2 100644 --- a/openapi3/extension.go +++ b/openapi3/extension.go @@ -7,7 +7,7 @@ import ( "strings" ) -func validateExtensions(ctx context.Context, extensions map[string]interface{}) error { // FIXME: newtype + Validate(...) +func validateExtensions(ctx context.Context, extensions map[string]any) error { // FIXME: newtype + Validate(...) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed var unknowns []string diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go index 40e9f3db0..bd99511a5 100644 --- a/openapi3/external_docs.go +++ b/openapi3/external_docs.go @@ -11,7 +11,7 @@ import ( // ExternalDocs is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object type ExternalDocs struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Description string `json:"description,omitempty" yaml:"description,omitempty"` URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -27,8 +27,8 @@ func (e ExternalDocs) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of ExternalDocs. -func (e ExternalDocs) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 2+len(e.Extensions)) +func (e ExternalDocs) MarshalYAML() (any, error) { + m := make(map[string]any, 2+len(e.Extensions)) for k, v := range e.Extensions { m[k] = v } diff --git a/openapi3/header.go b/openapi3/header.go index e5eee6ccb..dc542874d 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -17,7 +17,7 @@ type Header struct { var _ jsonpointer.JSONPointable = (*Header)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (header Header) JSONLookup(token string) (interface{}, error) { +func (header Header) JSONLookup(token string) (any, error) { return header.Parameter.JSONLookup(token) } @@ -32,7 +32,7 @@ func (header *Header) UnmarshalJSON(data []byte) error { } // MarshalYAML returns the JSON encoding of Header. -func (header Header) MarshalYAML() (interface{}, error) { +func (header Header) MarshalYAML() (any, error) { return header.Parameter, nil } diff --git a/openapi3/helpers.go b/openapi3/helpers.go index d160eb1e8..cb1ed3a9f 100644 --- a/openapi3/helpers.go +++ b/openapi3/helpers.go @@ -2,22 +2,33 @@ package openapi3 import ( "fmt" + "net/url" + "path" + "reflect" "regexp" + "sort" + "strings" + + "github.com/go-openapi/jsonpointer" ) -const identifierPattern = `^[a-zA-Z0-9._-]+$` +const identifierChars = `a-zA-Z0-9._-` -// IdentifierRegExp verifies whether Component object key matches 'identifierPattern' pattern, according to OpenAPI v3.x. +// IdentifierRegExp verifies whether Component object key matches contains just 'identifierChars', according to OpenAPI v3.x. +// InvalidIdentifierCharRegExp matches all characters not contained in 'identifierChars'. // However, to be able supporting legacy OpenAPI v2.x, there is a need to customize above pattern in order not to fail // converted v2-v3 validation -var IdentifierRegExp = regexp.MustCompile(identifierPattern) +var ( + IdentifierRegExp = regexp.MustCompile(`^[` + identifierChars + `]+$`) + InvalidIdentifierCharRegExp = regexp.MustCompile(`[^` + identifierChars + `]`) +) -// ValidateIdentifier returns an error if the given component name does not match IdentifierRegExp. +// ValidateIdentifier returns an error if the given component name does not match [IdentifierRegExp]. func ValidateIdentifier(value string) error { if IdentifierRegExp.MatchString(value) { return nil } - return fmt.Errorf("identifier %q is not supported by OpenAPIv3 standard (regexp: %q)", value, identifierPattern) + return fmt.Errorf("identifier %q is not supported by OpenAPIv3 standard (charset: [%q])", value, identifierChars) } // Float64Ptr is a helper for defining OpenAPI schemas. @@ -39,3 +50,212 @@ func Int64Ptr(value int64) *int64 { func Uint64Ptr(value uint64) *uint64 { return &value } + +// componentNames returns the map keys in a sorted slice. +func componentNames[E any](s map[string]E) []string { + out := make([]string, 0, len(s)) + for i := range s { + out = append(out, i) + } + sort.Strings(out) + return out +} + +// copyURI makes a copy of the pointer. +func copyURI(u *url.URL) *url.URL { + if u == nil { + return nil + } + + c := *u // shallow-copy + return &c +} + +type componentRef interface { + RefString() string + RefPath() *url.URL + CollectionName() string +} + +// refersToSameDocument returns if the $ref refers to the same document. +// +// Documents in different directories will have distinct $ref values that resolve to +// the same document. +// For example, consider the 3 files: +// +// /records.yaml +// /root.yaml $ref: records.yaml +// /schema/other.yaml $ref: ../records.yaml +// +// The records.yaml reference in the 2 latter refers to the same document. +func refersToSameDocument(o1 componentRef, o2 componentRef) bool { + if o1 == nil || o2 == nil { + return false + } + + r1 := o1.RefPath() + r2 := o2.RefPath() + + if r1 == nil || r2 == nil { + return false + } + + // refURL is relative to the working directory & base spec file. + return referenceURIMatch(r1, r2) +} + +// referencesRootDocument returns if the $ref points to the root document of the OpenAPI spec. +// +// If the document has no location, perhaps loaded from data in memory, it always returns false. +func referencesRootDocument(doc *T, ref componentRef) bool { + if doc.url == nil || ref == nil || ref.RefPath() == nil { + return false + } + + refURL := *ref.RefPath() + refURL.Fragment = "" + + // Check referenced element was in the root document. + return referenceURIMatch(doc.url, &refURL) +} + +func referenceURIMatch(u1 *url.URL, u2 *url.URL) bool { + s1, s2 := *u1, *u2 + if s1.Scheme == "" { + s1.Scheme = "file" + } + if s2.Scheme == "" { + s2.Scheme = "file" + } + + return s1.String() == s2.String() +} + +// ReferencesComponentInRootDocument returns if the given component reference references +// the same document or element as another component reference in the root document's +// '#/components/'. If it does, it returns the name of it in the form +// '#/components//NameXXX' +// +// Of course given a component from the root document will always match itself. +// +// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object +// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#relative-references-in-urls +// +// Example. Take the spec with directory structure: +// +// openapi.yaml +// schemas/ +// ├─ record.yaml +// ├─ records.yaml +// +// In openapi.yaml we have: +// +// components: +// schemas: +// Record: +// $ref: schemas/record.yaml +// +// Case 1: records.yml references a component in the root document +// +// $ref: ../openapi.yaml#/components/schemas/Record +// +// This would return... +// +// #/components/schemas/Record +// +// Case 2: records.yml indirectly refers to the same schema +// as a schema the root document's '#/components/schemas'. +// +// $ref: ./record.yaml +// +// This would also return... +// +// #/components/schemas/Record +func ReferencesComponentInRootDocument(doc *T, ref componentRef) (string, bool) { + if ref == nil || ref.RefString() == "" { + return "", false + } + + // Case 1: + // Something like: ../another-folder/document.json#/myElement + if isRemoteReference(ref.RefString()) && isRootComponentReference(ref.RefString(), ref.CollectionName()) { + // Determine if it is *this* root doc. + if referencesRootDocument(doc, ref) { + _, name, _ := strings.Cut(ref.RefString(), path.Join("#/components/", ref.CollectionName())) + + return path.Join("#/components/", ref.CollectionName(), name), true + } + } + + // If there are no schemas defined in the root document return early. + if doc.Components == nil { + return "", false + } + + collection, _, err := jsonpointer.GetForToken(doc.Components, ref.CollectionName()) + if err != nil { + panic(err) // unreachable + } + + var components map[string]componentRef + + componentRefType := reflect.TypeOf(new(componentRef)).Elem() + if t := reflect.TypeOf(collection); t.Kind() == reflect.Map && + t.Key().Kind() == reflect.String && + t.Elem().AssignableTo(componentRefType) { + v := reflect.ValueOf(collection) + + components = make(map[string]componentRef, v.Len()) + for _, key := range v.MapKeys() { + strct := v.MapIndex(key) + // Type assertion safe, already checked via reflection above. + components[key.Interface().(string)] = strct.Interface().(componentRef) + } + } else { + return "", false + } + + // Case 2: + // Something like: ../openapi.yaml#/components/schemas/myElement + for name, s := range components { + // Must be a reference to a YAML file. + if !isWholeDocumentReference(s.RefString()) { + continue + } + + // Is the schema a ref to the same resource. + if !refersToSameDocument(s, ref) { + continue + } + + // Transform the remote ref to the equivalent schema in the root document. + return path.Join("#/components/", ref.CollectionName(), name), true + } + + return "", false +} + +// isElementReference takes a $ref value and checks if it references a specific element. +func isElementReference(ref string) bool { + return ref != "" && !isWholeDocumentReference(ref) +} + +// isSchemaReference takes a $ref value and checks if it references a schema element. +func isRootComponentReference(ref string, compType string) bool { + return isElementReference(ref) && strings.Contains(ref, path.Join("#/components/", compType)) +} + +// isWholeDocumentReference takes a $ref value and checks if it is whole document reference. +func isWholeDocumentReference(ref string) bool { + return ref != "" && !strings.ContainsAny(ref, "#") +} + +// isRemoteReference takes a $ref value and checks if it is remote reference. +func isRemoteReference(ref string) bool { + return ref != "" && !strings.HasPrefix(ref, "#") && !isURLReference(ref) +} + +// isURLReference takes a $ref value and checks if it is URL reference. +func isURLReference(ref string) bool { + return strings.HasPrefix(ref, "http://") || strings.HasPrefix(ref, "https://") || strings.HasPrefix(ref, "//") +} diff --git a/openapi3/helpers_test.go b/openapi3/helpers_test.go new file mode 100644 index 000000000..ff350e394 --- /dev/null +++ b/openapi3/helpers_test.go @@ -0,0 +1,83 @@ +package openapi3 + +import ( + "net/url" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReferencesComponentInRootDocument(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + + runAssertions := func(doc *T) { + // The element type of ./records.yml references a document which is also in the root document. + v, ok := ReferencesComponentInRootDocument(doc, doc.Components.Schemas["BookRecords"].Value.Items) + assert.True(t, ok) + assert.Equal(t, "#/components/schemas/BookRecord", v) + + // The array element type directly references the component in the root document. + v, ok = ReferencesComponentInRootDocument(doc, doc.Components.Schemas["CdRecords"].Value.Items) + assert.True(t, ok) + assert.Equal(t, "#/components/schemas/CdRecord", v) + + // A component from the root document should + v, ok = ReferencesComponentInRootDocument(doc, doc.Components.Schemas["CdRecord"]) + assert.True(t, ok) + assert.Equal(t, "#/components/schemas/CdRecord", v) + + // The error response component is in the root doc. + v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/records").Get.Responses.Value("500")) + assert.True(t, ok) + assert.Equal(t, "#/components/responses/ErrorResponse", v) + + v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/records").Get.Responses.Value("500").Value.Content.Get("application/json").Schema) + assert.False(t, ok) + assert.Empty(t, v) + + // Ref path doesn't include a './' + v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/record").Get.Parameters[0]) + assert.True(t, ok) + assert.Equal(t, "#/components/parameters/BookIDParameter", v) + + v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/record").Get.Responses.Value("200").Value.Content.Get("application/json").Examples["first-example"]) + assert.True(t, ok) + assert.Equal(t, "#/components/examples/RecordResponseExample", v) + + // Matches equivalent paths where string is no equal. + v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/record").Get.Responses.Value("200").Value.Headers["X-Custom-Header"]) + assert.True(t, ok) + assert.Equal(t, "#/components/headers/CustomHeader", v) + + // Same structure distinct definition of the same header + v, ok = ReferencesComponentInRootDocument(doc, doc.Paths.Find("/record").Get.Responses.Value("200").Value.Headers["X-Custom-Header2"]) + assert.False(t, ok) + assert.Empty(t, v) + } + + // Load from the file system + doc, err := loader.LoadFromFile("testdata/refsToRoot/openapi.yml") + require.NoError(t, err) + + runAssertions(doc) + + // Loading from a URL by mocking HTTP calls. + // Loads the data using the URI path from the testdata/ folder. + loader.ReadFromURIFunc = func(loader *Loader, url *url.URL) ([]byte, error) { + localURL := *url + localURL.Scheme = "" + localURL.Host = "" + localURL.Path = filepath.Join("testdata", localURL.Path) + + return ReadFromFile(loader, &localURL) + } + + u, _ := url.Parse("https://example.com/refsToRoot/openapi.yml") + doc, err = loader.LoadFromURI(u) + require.NoError(t, err) + + runAssertions(doc) +} diff --git a/openapi3/info.go b/openapi3/info.go index 51707f852..e2468285c 100644 --- a/openapi3/info.go +++ b/openapi3/info.go @@ -9,7 +9,7 @@ import ( // Info is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#info-object type Info struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Title string `json:"title" yaml:"title"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -29,8 +29,11 @@ func (info Info) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Info. -func (info Info) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 6+len(info.Extensions)) +func (info *Info) MarshalYAML() (any, error) { + if info == nil { + return nil, nil + } + m := make(map[string]any, 6+len(info.Extensions)) for k, v := range info.Extensions { m[k] = v } diff --git a/openapi3/internalize_refs.go b/openapi3/internalize_refs.go index 191c14f7e..b4742864c 100644 --- a/openapi3/internalize_refs.go +++ b/openapi3/internalize_refs.go @@ -2,47 +2,134 @@ package openapi3 import ( "context" - "path/filepath" + "path" "strings" ) -type RefNameResolver func(string) string +// RefNameResolver maps a component to an name that is used as it's internalized name. +// +// The function should avoid name collisions (i.e. be a injective mapping). +// It must only contain characters valid for fixed field names: [IdentifierRegExp]. +type RefNameResolver func(*T, componentRef) string // DefaultRefResolver is a default implementation of refNameResolver for the // InternalizeRefs function. // -// If a reference points to an element inside a document, it returns the last -// element in the reference using filepath.Base. Otherwise if the reference points -// to a file, it returns the file name trimmed of all extensions. -func DefaultRefNameResolver(ref string) string { - if ref == "" { - return "" - } - split := strings.SplitN(ref, "#", 2) - if len(split) == 2 { - return filepath.Base(split[1]) - } - ref = split[0] - for ext := filepath.Ext(ref); len(ext) > 0; ext = filepath.Ext(ref) { - ref = strings.TrimSuffix(ref, ext) - } - return filepath.Base(ref) -} +// The external reference is internalized to (hopefully) a unique name. If +// the external reference matches (by path) to another reference in the root +// document then the name of that component is used. +// +// The transformation involves: +// - Cutting the "#/components/" part. +// - Cutting the file extensions (.yaml/.json) from documents. +// - Trimming the common directory with the root spec. +// - Replace invalid characters with with underscores. +// +// This is an injective mapping over a "reasonable" amount of the possible openapi +// spec domain space but is not perfect. There might be edge cases. +func DefaultRefNameResolver(doc *T, ref componentRef) string { + if ref.RefString() == "" || ref.RefPath() == nil { + panic("unable to resolve reference to name") + } + + name := ref.RefPath() + + // If refering to a component in the root spec, no need to internalize just use + // the existing component. + // XXX(percivalalb): since this function call is iterating over components behind the + // scenes during an internalization call it actually starts interating over + // new & replaced internalized components. This might caused some edge cases, + // haven't found one yet but this might need to actually be used on a frozen copy + // of doc. + if nameInRoot, found := ReferencesComponentInRootDocument(doc, ref); found { + nameInRoot = strings.TrimPrefix(nameInRoot, "#") + + rootCompURI := copyURI(doc.url) + rootCompURI.Fragment = nameInRoot + name = rootCompURI + } + + filePath, componentPath := name.Path, name.Fragment -func schemaNames(s Schemas) []string { - out := make([]string, 0, len(s)) - for i := range s { - out = append(out, i) + // Cut out the "#/components/" to make the names shorter. + // XXX(percivalalb): This might cause collisions but is worth the brevity. + if b, a, ok := strings.Cut(componentPath, path.Join("components", ref.CollectionName(), "")); ok { + componentPath = path.Join(b, a) } - return out + + if filePath != "" { + // If the path is the same as the root doc, just remove. + if doc.url != nil && filePath == doc.url.Path { + filePath = "" + } + + // Remove the path extentions to make this JSON/YAML agnostic. + for ext := path.Ext(filePath); len(ext) > 0; ext = path.Ext(filePath) { + filePath = strings.TrimSuffix(filePath, ext) + } + + // Trim the common prefix with the root doc path. + if doc.url != nil { + commonDir := path.Dir(doc.url.Path) + for { + if commonDir == "." { // no common prefix + break + } + + if p, found := cutDirectories(filePath, commonDir); found { + filePath = p + break + } + + commonDir = path.Dir(commonDir) + } + } + } + + var internalizedName string + + // Trim .'s & slashes from start e.g. otherwise ./doc.yaml would end up as __doc + if filePath != "" { + internalizedName = strings.TrimLeft(filePath, "./") + } + + if componentPath != "" { + if internalizedName != "" { + internalizedName += "_" + } + + internalizedName += strings.TrimLeft(componentPath, "./") + } + + // Replace invalid characters in component fixed field names. + internalizedName = InvalidIdentifierCharRegExp.ReplaceAllString(internalizedName, "_") + + return internalizedName } -func parametersMapNames(s ParametersMap) []string { - out := make([]string, 0, len(s)) - for i := range s { - out = append(out, i) +// cutDirectories removes the given directories from the start of the path if +// the path is a child. +func cutDirectories(p, dirs string) (string, bool) { + if dirs == "" || p == "" { + return p, false } - return out + + p = strings.TrimRight(p, "/") + dirs = strings.TrimRight(dirs, "/") + + var sb strings.Builder + sb.Grow(len(ParameterInHeader)) + for _, segments := range strings.Split(p, "/") { + sb.WriteString(segments) + + if sb.String() == p { + return strings.TrimPrefix(p, dirs), true + } + + sb.WriteRune('/') + } + + return p, false } func isExternalRef(ref string, parentIsExternal bool) bool { @@ -54,7 +141,7 @@ func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver, par return false } - name := refNameResolver(s.Ref) + name := refNameResolver(doc, s) if doc.Components != nil { if _, ok := doc.Components.Schemas[name]; ok { s.Ref = "#/components/schemas/" + name @@ -77,7 +164,7 @@ func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolve if p == nil || !isExternalRef(p.Ref, parentIsExternal) { return false } - name := refNameResolver(p.Ref) + name := refNameResolver(doc, p) if doc.Components != nil { if _, ok := doc.Components.Parameters[name]; ok { p.Ref = "#/components/parameters/" + name @@ -100,7 +187,7 @@ func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver, par if h == nil || !isExternalRef(h.Ref, parentIsExternal) { return false } - name := refNameResolver(h.Ref) + name := refNameResolver(doc, h) if doc.Components != nil { if _, ok := doc.Components.Headers[name]; ok { h.Ref = "#/components/headers/" + name @@ -123,7 +210,7 @@ func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameRes if r == nil || !isExternalRef(r.Ref, parentIsExternal) { return false } - name := refNameResolver(r.Ref) + name := refNameResolver(doc, r) if doc.Components != nil { if _, ok := doc.Components.RequestBodies[name]; ok { r.Ref = "#/components/requestBodies/" + name @@ -146,7 +233,7 @@ func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver, if r == nil || !isExternalRef(r.Ref, parentIsExternal) { return false } - name := refNameResolver(r.Ref) + name := refNameResolver(doc, r) if doc.Components != nil { if _, ok := doc.Components.Responses[name]; ok { r.Ref = "#/components/responses/" + name @@ -169,7 +256,7 @@ func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver Ref if ss == nil || !isExternalRef(ss.Ref, parentIsExternal) { return } - name := refNameResolver(ss.Ref) + name := refNameResolver(doc, ss) if doc.Components != nil { if _, ok := doc.Components.SecuritySchemes[name]; ok { ss.Ref = "#/components/securitySchemes/" + name @@ -192,7 +279,7 @@ func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver, p if e == nil || !isExternalRef(e.Ref, parentIsExternal) { return } - name := refNameResolver(e.Ref) + name := refNameResolver(doc, e) if doc.Components != nil { if _, ok := doc.Components.Examples[name]; ok { e.Ref = "#/components/examples/" + name @@ -215,7 +302,7 @@ func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver, parentI if l == nil || !isExternalRef(l.Ref, parentIsExternal) { return } - name := refNameResolver(l.Ref) + name := refNameResolver(doc, l) if doc.Components != nil { if _, ok := doc.Components.Links[name]; ok { l.Ref = "#/components/links/" + name @@ -238,7 +325,7 @@ func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver, if c == nil || !isExternalRef(c.Ref, parentIsExternal) { return false } - name := refNameResolver(c.Ref) + name := refNameResolver(doc, c) if doc.Components == nil { doc.Components = &Components{} @@ -264,7 +351,9 @@ func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver, parentIsEx } } } - for _, s2 := range s.Properties { + + for _, name := range componentNames(s.Properties) { + s2 := s.Properties[name] isExternal := doc.addSchemaToSpec(s2, refNameResolver, parentIsExternal) if s2 != nil { doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal) @@ -279,7 +368,8 @@ func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver, parentIsEx } func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver, parentIsExternal bool) { - for _, h := range hs { + for _, name := range componentNames(hs) { + h := hs[name] isExternal := doc.addHeaderToSpec(h, refNameResolver, parentIsExternal) if doc.isVisitedHeader(h.Value) { continue @@ -289,26 +379,30 @@ func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver, parentIs } func (doc *T) derefExamples(es Examples, refNameResolver RefNameResolver, parentIsExternal bool) { - for _, e := range es { + for _, name := range componentNames(es) { + e := es[name] doc.addExampleToSpec(e, refNameResolver, parentIsExternal) } } func (doc *T) derefContent(c Content, refNameResolver RefNameResolver, parentIsExternal bool) { - for _, mediatype := range c { + for _, name := range componentNames(c) { + mediatype := c[name] isExternal := doc.addSchemaToSpec(mediatype.Schema, refNameResolver, parentIsExternal) if mediatype.Schema != nil { doc.derefSchema(mediatype.Schema.Value, refNameResolver, isExternal || parentIsExternal) } doc.derefExamples(mediatype.Examples, refNameResolver, parentIsExternal) - for _, e := range mediatype.Encoding { + for _, name := range componentNames(mediatype.Encoding) { + e := mediatype.Encoding[name] doc.derefHeaders(e.Headers, refNameResolver, parentIsExternal) } } } func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver, parentIsExternal bool) { - for _, l := range ls { + for _, name := range componentNames(ls) { + l := ls[name] doc.addLinkToSpec(l, refNameResolver, parentIsExternal) } } @@ -327,7 +421,8 @@ func (doc *T) derefResponses(rs *Responses, refNameResolver RefNameResolver, par } func (doc *T) derefResponseBodies(es ResponseBodies, refNameResolver RefNameResolver, parentIsExternal bool) { - for _, e := range es { + for _, name := range componentNames(es) { + e := es[name] doc.derefResponse(e, refNameResolver, parentIsExternal) } } @@ -345,7 +440,8 @@ func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver, p } func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver, parentIsExternal bool) { - for _, ops := range paths { + for _, name := range componentNames(paths) { + ops := paths[name] pathIsExternal := isExternalRef(ops.Ref, parentIsExternal) // inline full operations ops.Ref = "" @@ -357,12 +453,15 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso } } - for _, op := range ops.Operations() { + opsWithMethod := ops.Operations() + for _, name := range componentNames(opsWithMethod) { + op := opsWithMethod[name] isExternal := doc.addRequestBodyToSpec(op.RequestBody, refNameResolver, pathIsExternal) if op.RequestBody != nil && op.RequestBody.Value != nil { doc.derefRequestBody(*op.RequestBody.Value, refNameResolver, pathIsExternal || isExternal) } - for _, cb := range op.Callbacks { + for _, name := range componentNames(op.Callbacks) { + cb := op.Callbacks[name] isExternal := doc.addCallbackToSpec(cb, refNameResolver, pathIsExternal) if cb.Value != nil { cbValue := (*cb.Value).Map() @@ -391,7 +490,7 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso // Example: // // doc.InternalizeRefs(context.Background(), nil) -func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string) { +func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(*T, componentRef) string) { doc.resetVisited() if refNameResolver == nil { @@ -399,8 +498,7 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri } if components := doc.Components; components != nil { - names := schemaNames(components.Schemas) - for _, name := range names { + for _, name := range componentNames(components.Schemas) { schema := components.Schemas[name] isExternal := doc.addSchemaToSpec(schema, refNameResolver, false) if schema != nil { @@ -408,8 +506,7 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri doc.derefSchema(schema.Value, refNameResolver, isExternal) } } - names = parametersMapNames(components.Parameters) - for _, name := range names { + for _, name := range componentNames(components.Parameters) { p := components.Parameters[name] isExternal := doc.addParameterToSpec(p, refNameResolver, false) if p != nil && p.Value != nil { @@ -418,7 +515,8 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri } } doc.derefHeaders(components.Headers, refNameResolver, false) - for _, req := range components.RequestBodies { + for _, name := range componentNames(components.RequestBodies) { + req := components.RequestBodies[name] isExternal := doc.addRequestBodyToSpec(req, refNameResolver, false) if req != nil && req.Value != nil { req.Ref = "" // always dereference the top level @@ -426,13 +524,15 @@ func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref stri } } doc.derefResponseBodies(components.Responses, refNameResolver, false) - for _, ss := range components.SecuritySchemes { + for _, name := range componentNames(components.SecuritySchemes) { + ss := components.SecuritySchemes[name] doc.addSecuritySchemeToSpec(ss, refNameResolver, false) } doc.derefExamples(components.Examples, refNameResolver, false) doc.derefLinks(components.Links, refNameResolver, false) - for _, cb := range components.Callbacks { + for _, name := range componentNames(components.Callbacks) { + cb := components.Callbacks[name] isExternal := doc.addCallbackToSpec(cb, refNameResolver, false) if cb != nil && cb.Value != nil { cb.Ref = "" // always dereference the top level diff --git a/openapi3/internalize_refs_test.go b/openapi3/internalize_refs_test.go index 90e73f234..6e9853acc 100644 --- a/openapi3/internalize_refs_test.go +++ b/openapi3/internalize_refs_test.go @@ -24,6 +24,7 @@ func TestInternalizeRefs(t *testing.T) { {"testdata/callbacks.yml"}, {"testdata/issue831/testref.internalizepath.openapi.yml"}, {"testdata/issue959/openapi.yml"}, + {"testdata/interalizationNameCollision/api.yml"}, } for _, test := range tests { diff --git a/openapi3/issue341_test.go b/openapi3/issue341_test.go index 6154591e9..55f18e54d 100644 --- a/openapi3/issue341_test.go +++ b/openapi3/issue341_test.go @@ -48,7 +48,7 @@ func TestIssue341(t *testing.T) { require.JSONEq(t, `{ "components": { "responses": { - "testpath_200_response": { + "testpath_testpath_200_response": { "content": { "application/json": { "schema": { @@ -70,7 +70,7 @@ func TestIssue341(t *testing.T) { "get": { "responses": { "200": { - "$ref": "#/components/responses/testpath_200_response" + "$ref": "#/components/responses/testpath_testpath_200_response" } } } diff --git a/openapi3/issue570_test.go b/openapi3/issue570_test.go index 75afb7e3e..1575e5599 100644 --- a/openapi3/issue570_test.go +++ b/openapi3/issue570_test.go @@ -9,5 +9,5 @@ import ( func TestIssue570(t *testing.T) { loader := NewLoader() _, err := loader.LoadFromFile("testdata/issue570.json") - require.ErrorContains(t, err, CircularReferenceError) + require.NoError(t, err) } diff --git a/openapi3/issue615_test.go b/openapi3/issue615_test.go index 67144e9e1..02532bb5a 100644 --- a/openapi3/issue615_test.go +++ b/openapi3/issue615_test.go @@ -9,21 +9,6 @@ import ( ) func TestIssue615(t *testing.T) { - { - var old int - old, openapi3.CircularReferenceCounter = openapi3.CircularReferenceCounter, 1 - defer func() { openapi3.CircularReferenceCounter = old }() - - loader := openapi3.NewLoader() - loader.IsExternalRefsAllowed = true - _, err := loader.LoadFromFile("testdata/recursiveRef/issue615.yml") - require.ErrorContains(t, err, openapi3.CircularReferenceError) - } - - var old int - old, openapi3.CircularReferenceCounter = openapi3.CircularReferenceCounter, 4 - defer func() { openapi3.CircularReferenceCounter = old }() - loader := openapi3.NewLoader() loader.IsExternalRefsAllowed = true doc, err := loader.LoadFromFile("testdata/recursiveRef/issue615.yml") diff --git a/openapi3/issue618_test.go b/openapi3/issue618_test.go index 2085ca0ee..cd6758895 100644 --- a/openapi3/issue618_test.go +++ b/openapi3/issue618_test.go @@ -33,7 +33,7 @@ paths: doc.InternalizeRefs(ctx, nil) - require.Contains(t, doc.Components.Schemas, "JournalEntry") - require.Contains(t, doc.Components.Schemas, "Record") - require.Contains(t, doc.Components.Schemas, "Account") + require.Contains(t, doc.Components.Schemas, "testdata_schema618_JournalEntry") + require.Contains(t, doc.Components.Schemas, "testdata_schema618_Record") + require.Contains(t, doc.Components.Schemas, "testdata_schema618_Account") } diff --git a/openapi3/issue657_test.go b/openapi3/issue657_test.go index 5494ddeca..a745e5606 100644 --- a/openapi3/issue657_test.go +++ b/openapi3/issue657_test.go @@ -52,7 +52,7 @@ components: { name: "no valid value", value: "ABCDE", - checkErr: func(t require.TestingT, err error, i ...interface{}) { + checkErr: func(t require.TestingT, err error, i ...any) { require.ErrorContains(t, err, "doesn't match schema due to: minimum string length is 10") wErr := &openapi3.MultiError{} diff --git a/openapi3/issue689_test.go b/openapi3/issue689_test.go index 44058a825..b7479f9f4 100644 --- a/openapi3/issue689_test.go +++ b/openapi3/issue689_test.go @@ -14,7 +14,7 @@ func TestIssue689(t *testing.T) { tests := [...]struct { name string schema *openapi3.Schema - value map[string]interface{} + value map[string]any opts []openapi3.SchemaValidationOption checkErr require.ErrorAssertionFunc }{ @@ -23,7 +23,7 @@ func TestIssue689(t *testing.T) { name: "read-only property succeeds when read-only validation is disabled", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ "foo": {Type: &openapi3.Types{"boolean"}, ReadOnly: true}}), - value: map[string]interface{}{"foo": true}, + value: map[string]any{"foo": true}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest(), openapi3.DisableReadOnlyValidation()}, @@ -35,7 +35,7 @@ func TestIssue689(t *testing.T) { "foo": {Type: &openapi3.Types{"boolean"}, ReadOnly: false}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest()}, - value: map[string]interface{}{"foo": true}, + value: map[string]any{"foo": true}, checkErr: require.NoError, }, { @@ -44,7 +44,7 @@ func TestIssue689(t *testing.T) { "foo": {Type: &openapi3.Types{"boolean"}, ReadOnly: true}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest()}, - value: map[string]interface{}{"foo": true}, + value: map[string]any{"foo": true}, checkErr: require.Error, }, { @@ -53,7 +53,7 @@ func TestIssue689(t *testing.T) { "foo": {Type: &openapi3.Types{"boolean"}, ReadOnly: false}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest()}, - value: map[string]interface{}{"foo": true}, + value: map[string]any{"foo": true}, checkErr: require.NoError, }, // write-only @@ -61,7 +61,7 @@ func TestIssue689(t *testing.T) { name: "write-only property succeeds when write-only validation is disabled", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ "foo": {Type: &openapi3.Types{"boolean"}, WriteOnly: true}}), - value: map[string]interface{}{"foo": true}, + value: map[string]any{"foo": true}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsResponse(), openapi3.DisableWriteOnlyValidation()}, @@ -73,7 +73,7 @@ func TestIssue689(t *testing.T) { "foo": {Type: &openapi3.Types{"boolean"}, WriteOnly: false}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsResponse()}, - value: map[string]interface{}{"foo": true}, + value: map[string]any{"foo": true}, checkErr: require.NoError, }, { @@ -82,7 +82,7 @@ func TestIssue689(t *testing.T) { "foo": {Type: &openapi3.Types{"boolean"}, WriteOnly: true}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsResponse()}, - value: map[string]interface{}{"foo": true}, + value: map[string]any{"foo": true}, checkErr: require.Error, }, { @@ -91,7 +91,7 @@ func TestIssue689(t *testing.T) { "foo": {Type: &openapi3.Types{"boolean"}, WriteOnly: false}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsResponse()}, - value: map[string]interface{}{"foo": true}, + value: map[string]any{"foo": true}, checkErr: require.NoError, }, } diff --git a/openapi3/issue735_test.go b/openapi3/issue735_test.go index f7e420c5d..a846caa0e 100644 --- a/openapi3/issue735_test.go +++ b/openapi3/issue735_test.go @@ -10,14 +10,14 @@ import ( type testCase struct { name string schema *Schema - value interface{} - extraNotContains []interface{} + value any + extraNotContains []any options []SchemaValidationOption } func TestIssue735(t *testing.T) { - DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) - DefineStringFormat("email", FormatOfStringForEmail) + DefineStringFormatValidator("uuid", NewRegexpFormatValidator(FormatOfStringForUUIDOfRFC4122)) + DefineStringFormatValidator("email", NewRegexpFormatValidator(FormatOfStringForEmail)) DefineIPv4Format() DefineIPv6Format() @@ -100,60 +100,60 @@ func TestIssue735(t *testing.T) { { name: "items", schema: NewSchema().WithItems(NewStringSchema()), - value: []interface{}{42}, - extraNotContains: []interface{}{42}, + value: []any{42}, + extraNotContains: []any{42}, }, { name: "min items", schema: NewSchema().WithMinItems(100), - value: []interface{}{42}, - extraNotContains: []interface{}{42}, + value: []any{42}, + extraNotContains: []any{42}, }, { name: "max items", schema: NewSchema().WithMaxItems(0), - value: []interface{}{42}, - extraNotContains: []interface{}{42}, + value: []any{42}, + extraNotContains: []any{42}, }, { name: "unique items", schema: NewSchema().WithUniqueItems(true), - value: []interface{}{42, 42}, - extraNotContains: []interface{}{42}, + value: []any{42, 42}, + extraNotContains: []any{42}, }, { name: "min properties", schema: NewSchema().WithMinProperties(100), - value: map[string]interface{}{"foo": 42}, - extraNotContains: []interface{}{42}, + value: map[string]any{"foo": 42}, + extraNotContains: []any{42}, }, { name: "max properties", schema: NewSchema().WithMaxProperties(0), - value: map[string]interface{}{"foo": 42}, - extraNotContains: []interface{}{42}, + value: map[string]any{"foo": 42}, + extraNotContains: []any{42}, }, { name: "additional properties other schema type", schema: NewSchema().WithAdditionalProperties(NewStringSchema()), - value: map[string]interface{}{"foo": 42}, - extraNotContains: []interface{}{42}, + value: map[string]any{"foo": 42}, + extraNotContains: []any{42}, }, { name: "additional properties false", schema: &Schema{AdditionalProperties: AdditionalProperties{ Has: BoolPtr(false), }}, - value: map[string]interface{}{"foo": 42}, - extraNotContains: []interface{}{42}, + value: map[string]any{"foo": 42}, + extraNotContains: []any{42}, }, { name: "invalid properties schema", schema: NewSchema().WithProperties(map[string]*Schema{ "foo": NewStringSchema(), }), - value: map[string]interface{}{"foo": 42}, - extraNotContains: []interface{}{42}, + value: map[string]any{"foo": 42}, + extraNotContains: []any{42}, }, // TODO: uncomment when https://github.com/getkin/kin-openapi/issues/502 is fixed //{ @@ -161,8 +161,8 @@ func TestIssue735(t *testing.T) { // schema: NewSchema().WithProperties(map[string]*Schema{ // "foo": {ReadOnly: true}, // }).WithoutAdditionalProperties(), - // value: map[string]interface{}{"foo": 42}, - // extraNotContains: []interface{}{42}, + // value: map[string]any{"foo": 42}, + // extraNotContains: []any{42}, // options: []SchemaValidationOption{VisitAsRequest()}, //}, //{ @@ -170,8 +170,8 @@ func TestIssue735(t *testing.T) { // schema: NewSchema().WithProperties(map[string]*Schema{ // "foo": {WriteOnly: true}, // }).WithoutAdditionalProperties(), - // value: map[string]interface{}{"foo": 42}, - // extraNotContains: []interface{}{42}, + // value: map[string]any{"foo": 42}, + // extraNotContains: []any{42}, // options: []SchemaValidationOption{VisitAsResponse()}, //}, { @@ -182,8 +182,8 @@ func TestIssue735(t *testing.T) { }, Required: []string{"bar"}, }, - value: map[string]interface{}{"foo": 42}, - extraNotContains: []interface{}{42}, + value: map[string]any{"foo": 42}, + extraNotContains: []any{42}, }, { name: "one of (matches more then one)", diff --git a/openapi3/issue746_test.go b/openapi3/issue746_test.go index 390a34848..55d34456f 100644 --- a/openapi3/issue746_test.go +++ b/openapi3/issue746_test.go @@ -12,7 +12,7 @@ func TestIssue746(t *testing.T) { err := schema.UnmarshalJSON([]byte(`{"additionalProperties": false}`)) require.NoError(t, err) - var value interface{} + var value any err = json.Unmarshal([]byte(`{"foo": "bar"}`), &value) require.NoError(t, err) diff --git a/openapi3/issue767_test.go b/openapi3/issue767_test.go index 55fb52f44..04fd9d03f 100644 --- a/openapi3/issue767_test.go +++ b/openapi3/issue767_test.go @@ -14,7 +14,7 @@ func TestIssue767(t *testing.T) { tests := [...]struct { name string schema *openapi3.Schema - value map[string]interface{} + value map[string]any opts []openapi3.SchemaValidationOption checkErr require.ErrorAssertionFunc }{ @@ -22,7 +22,7 @@ func TestIssue767(t *testing.T) { name: "default values disabled should fail with minProps 1", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ "foo": {Type: &openapi3.Types{"boolean"}, Default: true}}).WithMinProperties(1), - value: map[string]interface{}{}, + value: map[string]any{}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest(), }, @@ -32,7 +32,7 @@ func TestIssue767(t *testing.T) { name: "default values enabled should pass with minProps 1", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ "foo": {Type: &openapi3.Types{"boolean"}, Default: true}}).WithMinProperties(1), - value: map[string]interface{}{}, + value: map[string]any{}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest(), openapi3.DefaultsSet(func() {}), @@ -45,7 +45,7 @@ func TestIssue767(t *testing.T) { "foo": {Type: &openapi3.Types{"boolean"}, Default: true}, "bar": {Type: &openapi3.Types{"boolean"}}, }).WithMinProperties(2), - value: map[string]interface{}{"bar": false}, + value: map[string]any{"bar": false}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest(), openapi3.DefaultsSet(func() {}), @@ -58,7 +58,7 @@ func TestIssue767(t *testing.T) { "foo": {Type: &openapi3.Types{"boolean"}, Default: true}, "bar": {Type: &openapi3.Types{"boolean"}}, }).WithMaxProperties(1), - value: map[string]interface{}{"bar": false}, + value: map[string]any{"bar": false}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest(), openapi3.DefaultsSet(func() {}), @@ -71,7 +71,7 @@ func TestIssue767(t *testing.T) { "foo": {Type: &openapi3.Types{"boolean"}, Default: true}, "bar": {Type: &openapi3.Types{"boolean"}}, }).WithMaxProperties(1), - value: map[string]interface{}{"bar": false}, + value: map[string]any{"bar": false}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest(), }, diff --git a/openapi3/issue796_test.go b/openapi3/issue796_test.go index 0900ee5b9..9c8be17f2 100644 --- a/openapi3/issue796_test.go +++ b/openapi3/issue796_test.go @@ -7,11 +7,6 @@ import ( ) func TestIssue796(t *testing.T) { - var old int - // Need to set CircularReferenceCounter to > 10 - old, CircularReferenceCounter = CircularReferenceCounter, 20 - defer func() { CircularReferenceCounter = old }() - loader := NewLoader() doc, err := loader.LoadFromFile("testdata/issue796.yml") require.NoError(t, err) diff --git a/openapi3/issue82_test.go b/openapi3/issue82_test.go new file mode 100644 index 000000000..3bf242bc7 --- /dev/null +++ b/openapi3/issue82_test.go @@ -0,0 +1,103 @@ +package openapi3 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue82(t *testing.T) { + payload := map[string]interface{}{ + "prop1": "val", + "prop3": "val", + } + + schemas := []string{` +{ + "type": "object", + "additionalProperties": false, + "required": ["prop1"], + "properties": { + "prop1": { + "type": "string" + } + } +}`, `{ + "anyOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["prop1"], + "properties": { + "prop1": { + "type": "string" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "prop2": { + "type": "string" + } + } + } + ] +}`, `{ + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["prop1"], + "properties": { + "prop1": { + "type": "string" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "prop2": { + "type": "string" + } + } + } + ] +}`, `{ + "allOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["prop1"], + "properties": { + "prop1": { + "type": "string" + } + } + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "prop2": { + "type": "string" + } + } + } + ] + } +`} + + for _, jsonSchema := range schemas { + var dataSchema Schema + err := json.Unmarshal([]byte(jsonSchema), &dataSchema) + require.NoError(t, err) + + err = dataSchema.VisitJSON(payload) + require.Error(t, err) + } +} diff --git a/openapi3/issue972_test.go b/openapi3/issue972_test.go new file mode 100644 index 000000000..3575adc9d --- /dev/null +++ b/openapi3/issue972_test.go @@ -0,0 +1,66 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestIssue972(t *testing.T) { + type testcase struct { + spec string + validationErrorContains string + } + + base := ` +openapi: 3.0.2 +paths: {} +components: {} +` + + for _, tc := range []testcase{{ + spec: base, + validationErrorContains: "invalid info: must be an object", + }, { + spec: base + ` +info: + title: "Hello World REST APIs" + version: "1.0" +`, + }, { + spec: base + ` +info: null +`, + validationErrorContains: "invalid info: must be an object", + }, { + spec: base + ` +info: {} +`, + validationErrorContains: "invalid info: value of version must be a non-empty string", + }, { + spec: base + ` +info: + title: "Hello World REST APIs" +`, + validationErrorContains: "invalid info: value of version must be a non-empty string", + }} { + t.Logf("spec: %s", tc.spec) + + loader := &Loader{} + doc, err := loader.LoadFromData([]byte(tc.spec)) + assert.NoError(t, err) + assert.NotNil(t, doc) + + err = doc.Validate(loader.Context) + if e := tc.validationErrorContains; e != "" { + assert.ErrorContains(t, err, e) + } else { + assert.NoError(t, err) + } + + txt, err := yaml.Marshal(doc) + assert.NoError(t, err) + assert.NotEmpty(t, txt) + } +} diff --git a/openapi3/license.go b/openapi3/license.go index e845ed832..c4f6c8dc3 100644 --- a/openapi3/license.go +++ b/openapi3/license.go @@ -9,7 +9,7 @@ import ( // License is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#license-object type License struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Name string `json:"name" yaml:"name"` // Required URL string `json:"url,omitempty" yaml:"url,omitempty"` @@ -25,8 +25,8 @@ func (license License) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of License. -func (license License) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 2+len(license.Extensions)) +func (license License) MarshalYAML() (any, error) { + m := make(map[string]any, 2+len(license.Extensions)) for k, v := range license.Extensions { m[k] = v } diff --git a/openapi3/link.go b/openapi3/link.go index 961566538..132f67803 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -10,14 +10,14 @@ import ( // Link is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#link-object type Link struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` - OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` - OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Parameters map[string]interface{} `json:"parameters,omitempty" yaml:"parameters,omitempty"` - Server *Server `json:"server,omitempty" yaml:"server,omitempty"` - RequestBody interface{} `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` + OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"` + OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Parameters map[string]any `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Server *Server `json:"server,omitempty" yaml:"server,omitempty"` + RequestBody any `json:"requestBody,omitempty" yaml:"requestBody,omitempty"` } // MarshalJSON returns the JSON encoding of Link. @@ -30,8 +30,8 @@ func (link Link) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Link. -func (link Link) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 6+len(link.Extensions)) +func (link Link) MarshalYAML() (any, error) { + m := make(map[string]any, 6+len(link.Extensions)) for k, v := range link.Extensions { m[k] = v } diff --git a/openapi3/load_cicular_ref_with_external_file_test.go b/openapi3/load_cicular_ref_with_external_file_test.go index 7a99e7600..cff5c01fe 100644 --- a/openapi3/load_cicular_ref_with_external_file_test.go +++ b/openapi3/load_cicular_ref_with_external_file_test.go @@ -47,6 +47,19 @@ func TestLoadCircularRefFromFile(t *testing.T) { bar.Value.Properties["foo"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Foo", Value: foo.Value} foo.Value.Properties["bar"] = &openapi3.SchemaRef{Ref: "#/components/schemas/Bar", Value: bar.Value} + bazNestedRef := &openapi3.SchemaRef{Ref: "./baz.yml#/BazNested"} + array := openapi3.NewArraySchema() + array.Items = bazNestedRef + bazNested := &openapi3.Schema{Properties: map[string]*openapi3.SchemaRef{ + "bazArray": { + Value: &openapi3.Schema{ + Items: bazNestedRef, + }, + }, + "baz": bazNestedRef, + }} + bazNestedRef.Value = bazNested + want := &openapi3.T{ OpenAPI: "3.0.3", Info: &openapi3.Info{ @@ -57,6 +70,7 @@ func TestLoadCircularRefFromFile(t *testing.T) { Schemas: openapi3.Schemas{ "Foo": foo, "Bar": bar, + "Baz": bazNestedRef, }, }, } diff --git a/openapi3/loader.go b/openapi3/loader.go index 88ab566ac..4f2766a0f 100644 --- a/openapi3/loader.go +++ b/openapi3/loader.go @@ -11,14 +11,10 @@ import ( "path" "path/filepath" "reflect" - "sort" "strconv" "strings" ) -var CircularReferenceError = "kin-openapi bug found: circular schema reference not handled" -var CircularReferenceCounter = 3 - func foundUnresolvedRef(ref string) error { return fmt.Errorf("found unresolved ref: %q", ref) } @@ -44,15 +40,9 @@ type Loader struct { visitedDocuments map[string]*T - visitedCallback map[*Callback]struct{} - visitedExample map[*Example]struct{} - visitedHeader map[*Header]struct{} - visitedLink map[*Link]struct{} - visitedParameter map[*Parameter]struct{} - visitedRequestBody map[*RequestBody]struct{} - visitedResponse map[*Response]struct{} - visitedSchema map[*Schema]struct{} - visitedSecurityScheme map[*SecurityScheme]struct{} + visitedRefs map[string]struct{} + visitedPath []string + backtrack map[string][]func(value any) } // NewLoader returns an empty Loader @@ -64,6 +54,9 @@ func NewLoader() *Loader { func (loader *Loader) resetVisitedPathItemRefs() { loader.visitedPathItemRefs = make(map[string]struct{}) + loader.visitedRefs = make(map[string]struct{}) + loader.visitedPath = nil + loader.backtrack = make(map[string][]func(value any)) } // LoadFromURI loads a spec from a remote URL @@ -93,7 +86,7 @@ func (loader *Loader) allowsExternalRefs(ref string) (err error) { return } -func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, element interface{}) (*url.URL, error) { +func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, element any) (*url.URL, error) { if err := loader.allowsExternalRefs(ref); err != nil { return nil, err } @@ -178,6 +171,9 @@ func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.UR if err := unmarshal(data, doc); err != nil { return nil, err } + + doc.url = copyURI(location) + if err := loader.ResolveRefsIn(doc, location); err != nil { return nil, err } @@ -196,50 +192,50 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { } if components := doc.Components; components != nil { - for _, component := range components.Headers { + for _, name := range componentNames(components.Headers) { + component := components.Headers[name] if err = loader.resolveHeaderRef(doc, component, location); err != nil { return } } - for _, component := range components.Parameters { + for _, name := range componentNames(components.Parameters) { + component := components.Parameters[name] if err = loader.resolveParameterRef(doc, component, location); err != nil { return } } - for _, component := range components.RequestBodies { + for _, name := range componentNames(components.RequestBodies) { + component := components.RequestBodies[name] if err = loader.resolveRequestBodyRef(doc, component, location); err != nil { return } } - for _, component := range components.Responses { + for _, name := range componentNames(components.Responses) { + component := components.Responses[name] if err = loader.resolveResponseRef(doc, component, location); err != nil { return } } - for _, component := range components.Schemas { + for _, name := range componentNames(components.Schemas) { + component := components.Schemas[name] if err = loader.resolveSchemaRef(doc, component, location, []string{}); err != nil { return } } - for _, component := range components.SecuritySchemes { + for _, name := range componentNames(components.SecuritySchemes) { + component := components.SecuritySchemes[name] if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil { return } } - - examples := make([]string, 0, len(components.Examples)) - for name := range components.Examples { - examples = append(examples, name) - } - sort.Strings(examples) - for _, name := range examples { + for _, name := range componentNames(components.Examples) { component := components.Examples[name] if err = loader.resolveExampleRef(doc, component, location); err != nil { return } } - - for _, component := range components.Callbacks { + for _, name := range componentNames(components.Callbacks) { + component := components.Callbacks[name] if err = loader.resolveCallbackRef(doc, component, location); err != nil { return } @@ -247,7 +243,9 @@ func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) { } // Visit all operations - for _, pathItem := range doc.Paths.Map() { + pathItems := doc.Paths.Map() + for _, name := range componentNames(pathItems) { + pathItem := pathItems[name] if pathItem == nil { continue } @@ -271,7 +269,7 @@ func join(basePath *url.URL, relativePath *url.URL) *url.URL { func resolvePath(basePath *url.URL, componentPath *url.URL) *url.URL { if is_file(componentPath) { // support absolute paths - if componentPath.Path[0] == '/' { + if filepath.IsAbs(componentPath.Path) { return componentPath } return join(basePath, componentPath) @@ -294,7 +292,35 @@ func isSingleRefElement(ref string) bool { return !strings.Contains(ref, "#") } -func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolved interface{}) ( +func (loader *Loader) visitRef(ref string) { + if loader.visitedRefs == nil { + loader.visitedRefs = make(map[string]struct{}) + loader.backtrack = make(map[string][]func(value any)) + } + loader.visitedPath = append(loader.visitedPath, ref) + loader.visitedRefs[ref] = struct{}{} +} + +func (loader *Loader) unvisitRef(ref string, value any) { + if value != nil { + for _, fn := range loader.backtrack[ref] { + fn(value) + } + } + delete(loader.visitedRefs, ref) + delete(loader.backtrack, ref) + loader.visitedPath = loader.visitedPath[:len(loader.visitedPath)-1] +} + +func (loader *Loader) shouldVisitRef(ref string, fn func(value any)) bool { + if _, ok := loader.visitedRefs[ref]; ok { + loader.backtrack[ref] = append(loader.backtrack[ref], fn) + return false + } + return true +} + +func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolved any) ( componentDoc *T, componentPath *url.URL, err error, @@ -315,7 +341,7 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv return nil, nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref) } - drill := func(cursor interface{}) (interface{}, error) { + drill := func(cursor any) (any, error) { for _, pathPart := range strings.Split(fragment[1:], "/") { pathPart = unescapeRefString(pathPart) attempted := false @@ -361,7 +387,7 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv } return cursor, nil } - var cursor interface{} + var cursor any if cursor, err = drill(componentDoc); err != nil { if path == nil { return nil, nil, err @@ -380,13 +406,27 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv err = nil } + setComponent := func(target any) { + if componentPath != nil { + if i, ok := target.(interface { + setRefPath(*url.URL) + }); ok { + copy := *componentPath + copy.Fragment = parsedURL.Fragment + i.setRefPath(©) + } + } + } + switch { case reflect.TypeOf(cursor) == reflect.TypeOf(resolved): + setComponent(cursor) + reflect.ValueOf(resolved).Elem().Set(reflect.ValueOf(cursor).Elem()) return componentDoc, componentPath, nil - case reflect.TypeOf(cursor) == reflect.TypeOf(map[string]interface{}{}): - codec := func(got, expect interface{}) error { + case reflect.TypeOf(cursor) == reflect.TypeOf(map[string]any{}): + codec := func(got, expect any) error { enc, err := json.Marshal(got) if err != nil { return err @@ -394,6 +434,8 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv if err = json.Unmarshal(enc, expect); err != nil { return err } + + setComponent(expect) return nil } if err := codec(cursor, resolved); err != nil { @@ -406,7 +448,7 @@ func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolv } } -func readableType(x interface{}) string { +func readableType(x any) string { switch x.(type) { case *Callback: return "callback object" @@ -435,7 +477,7 @@ func readableType(x interface{}) string { } } -func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { +func drillIntoField(cursor any, fieldName string) (any, error) { switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() { case reflect.Map: @@ -476,7 +518,7 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) { } if hasFields { if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "Extensions" { - extensions := val.Field(0).Interface().(map[string]interface{}) + extensions := val.Field(0).Interface().(map[string]any) if enc, ok := extensions[fieldName]; ok { return enc, nil } @@ -494,16 +536,10 @@ func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, return doc, ref, path, nil } - if err := loader.allowsExternalRefs(ref); err != nil { - return nil, "", nil, err - } - - resolvedPath, err := resolvePathWithRef(ref, path) + fragment, resolvedPath, err := loader.resolveRefPath(ref, path) if err != nil { return nil, "", nil, err } - fragment := "#" + resolvedPath.Fragment - resolvedPath.Fragment = "" if doc, err = loader.loadFromURIInternal(resolvedPath); err != nil { return nil, "", nil, fmt.Errorf("error resolving reference %q: %w", ref, err) @@ -512,6 +548,25 @@ func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, return doc, fragment, resolvedPath, nil } +func (loader *Loader) resolveRefPath(ref string, path *url.URL) (string, *url.URL, error) { + if ref != "" && ref[0] == '#' { + return ref, path, nil + } + + if err := loader.allowsExternalRefs(ref); err != nil { + return "", nil, err + } + + resolvedPath, err := resolvePathWithRef(ref, path) + if err != nil { + return "", nil, err + } + + fragment := "#" + resolvedPath.Fragment + resolvedPath.Fragment = "" + return fragment, resolvedPath, nil +} + var ( errMUSTCallback = errors.New("invalid callback: value MUST be an object") errMUSTExample = errors.New("invalid example: value MUST be an object") @@ -530,23 +585,25 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat return errMUSTHeader } - if component.Value != nil { - if loader.visitedHeader == nil { - loader.visitedHeader = make(map[*Header]struct{}) + if ref := component.Ref; ref != "" { + if component.Value != nil { + return nil } - if _, ok := loader.visitedHeader[component.Value]; ok { + if !loader.shouldVisitRef(ref, func(value any) { + component.Value = value.(*Header) + _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refDocPath) + }) { return nil } - loader.visitedHeader[component.Value] = struct{}{} - } - - if ref := component.Ref; ref != "" { + loader.visitRef(ref) if isSingleRefElement(ref) { var header Header if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &header); err != nil { return err } component.Value = &header + component.setRefPath(documentPath) } else { var resolved HeaderRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) @@ -560,7 +617,9 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat return err } component.Value = resolved.Value + component.setRefPath(resolved.RefPath()) } + defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { @@ -580,23 +639,25 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum return errMUSTParameter } - if component.Value != nil { - if loader.visitedParameter == nil { - loader.visitedParameter = make(map[*Parameter]struct{}) + if ref := component.Ref; ref != "" { + if component.Value != nil { + return nil } - if _, ok := loader.visitedParameter[component.Value]; ok { + if !loader.shouldVisitRef(ref, func(value any) { + component.Value = value.(*Parameter) + _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refDocPath) + }) { return nil } - loader.visitedParameter[component.Value] = struct{}{} - } - - if ref := component.Ref; ref != "" { + loader.visitRef(ref) if isSingleRefElement(ref) { var param Parameter if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil { return err } component.Value = ¶m + component.setRefPath(documentPath) } else { var resolved ParameterRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) @@ -610,7 +671,9 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum return err } component.Value = resolved.Value + component.setRefPath(resolved.RefPath()) } + defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { @@ -620,7 +683,8 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum if value.Content != nil && value.Schema != nil { return errors.New("cannot contain both schema and content in a parameter") } - for _, contentType := range value.Content { + for _, name := range componentNames(value.Content) { + contentType := value.Content[name] if schema := contentType.Schema; schema != nil { if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil { return err @@ -640,23 +704,25 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d return errMUSTRequestBody } - if component.Value != nil { - if loader.visitedRequestBody == nil { - loader.visitedRequestBody = make(map[*RequestBody]struct{}) + if ref := component.Ref; ref != "" { + if component.Value != nil { + return nil } - if _, ok := loader.visitedRequestBody[component.Value]; ok { + if !loader.shouldVisitRef(ref, func(value any) { + component.Value = value.(*RequestBody) + _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refDocPath) + }) { return nil } - loader.visitedRequestBody[component.Value] = struct{}{} - } - - if ref := component.Ref; ref != "" { + loader.visitRef(ref) if isSingleRefElement(ref) { var requestBody RequestBody if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil { return err } component.Value = &requestBody + component.setRefPath(documentPath) } else { var resolved RequestBodyRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) @@ -670,23 +736,21 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d return err } component.Value = resolved.Value + component.setRefPath(resolved.RefPath()) } + defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { return nil } - for _, contentType := range value.Content { + for _, name := range componentNames(value.Content) { + contentType := value.Content[name] if contentType == nil { continue } - examples := make([]string, 0, len(contentType.Examples)) - for name := range contentType.Examples { - examples = append(examples, name) - } - sort.Strings(examples) - for _, name := range examples { + for _, name := range componentNames(contentType.Examples) { example := contentType.Examples[name] if err := loader.resolveExampleRef(doc, example, documentPath); err != nil { return err @@ -707,23 +771,25 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen return errMUSTResponse } - if component.Value != nil { - if loader.visitedResponse == nil { - loader.visitedResponse = make(map[*Response]struct{}) + if ref := component.Ref; ref != "" { + if component.Value != nil { + return nil } - if _, ok := loader.visitedResponse[component.Value]; ok { + if !loader.shouldVisitRef(ref, func(value any) { + component.Value = value.(*Response) + _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refDocPath) + }) { return nil } - loader.visitedResponse[component.Value] = struct{}{} - } - - if ref := component.Ref; ref != "" { + loader.visitRef(ref) if isSingleRefElement(ref) { var resp Response if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil { return err } component.Value = &resp + component.setRefPath(documentPath) } else { var resolved ResponseRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) @@ -737,28 +803,27 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen return err } component.Value = resolved.Value + component.setRefPath(resolved.RefPath()) } + defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { return nil } - for _, header := range value.Headers { + for _, name := range componentNames(value.Headers) { + header := value.Headers[name] if err := loader.resolveHeaderRef(doc, header, documentPath); err != nil { return err } } - for _, contentType := range value.Content { + for _, name := range componentNames(value.Content) { + contentType := value.Content[name] if contentType == nil { continue } - examples := make([]string, 0, len(contentType.Examples)) - for name := range contentType.Examples { - examples = append(examples, name) - } - sort.Strings(examples) - for _, name := range examples { + for _, name := range componentNames(contentType.Examples) { example := contentType.Examples[name] if err := loader.resolveExampleRef(doc, example, documentPath); err != nil { return err @@ -772,7 +837,8 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen contentType.Schema = schema } } - for _, link := range value.Links { + for _, name := range componentNames(value.Links) { + link := value.Links[name] if err := loader.resolveLinkRef(doc, link, documentPath); err != nil { return err } @@ -785,30 +851,26 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return errMUSTSchema } - if component.Value != nil { - if loader.visitedSchema == nil { - loader.visitedSchema = make(map[*Schema]struct{}) + if ref := component.Ref; ref != "" { + if component.Value != nil { + return nil } - if _, ok := loader.visitedSchema[component.Value]; ok { + if !loader.shouldVisitRef(ref, func(value any) { + component.Value = value.(*Schema) + _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refDocPath) + }) { return nil } - loader.visitedSchema[component.Value] = struct{}{} - } - - if ref := component.Ref; ref != "" { + loader.visitRef(ref) if isSingleRefElement(ref) { var schema Schema if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil { return err } component.Value = &schema + component.setRefPath(documentPath) } else { - if visitedLimit(visited, ref) { - visited = append(visited, ref) - return fmt.Errorf("%s with length %d - %s", CircularReferenceError, len(visited), strings.Join(visited, " -> ")) - } - visited = append(visited, ref) - var resolved SchemaRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) if err != nil { @@ -821,11 +883,9 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } component.Value = resolved.Value + component.setRefPath(resolved.RefPath()) } - if loader.visitedSchema == nil { - loader.visitedSchema = make(map[*Schema]struct{}) - } - loader.visitedSchema[component.Value] = struct{}{} + defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { @@ -838,7 +898,8 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat return err } } - for _, v := range value.Properties { + for _, name := range componentNames(value.Properties) { + v := value.Properties[name] if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil { return err } @@ -876,23 +937,25 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme return errMUSTSecurityScheme } - if component.Value != nil { - if loader.visitedSecurityScheme == nil { - loader.visitedSecurityScheme = make(map[*SecurityScheme]struct{}) + if ref := component.Ref; ref != "" { + if component.Value != nil { + return nil } - if _, ok := loader.visitedSecurityScheme[component.Value]; ok { + if !loader.shouldVisitRef(ref, func(value any) { + component.Value = value.(*SecurityScheme) + _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refDocPath) + }) { return nil } - loader.visitedSecurityScheme[component.Value] = struct{}{} - } - - if ref := component.Ref; ref != "" { + loader.visitRef(ref) if isSingleRefElement(ref) { var scheme SecurityScheme if _, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil { return err } component.Value = &scheme + component.setRefPath(documentPath) } else { var resolved SecuritySchemeRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) @@ -906,33 +969,33 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme return err } component.Value = resolved.Value + component.setRefPath(resolved.RefPath()) } + defer loader.unvisitRef(ref, component.Value) } return nil } func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentPath *url.URL) (err error) { - if component.isEmpty() { - return errMUSTExample - } - - if component.Value != nil { - if loader.visitedExample == nil { - loader.visitedExample = make(map[*Example]struct{}) + if ref := component.Ref; ref != "" { + if component.Value != nil { + return nil } - if _, ok := loader.visitedExample[component.Value]; ok { + if !loader.shouldVisitRef(ref, func(value any) { + component.Value = value.(*Example) + _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refDocPath) + }) { return nil } - loader.visitedExample[component.Value] = struct{}{} - } - - if ref := component.Ref; ref != "" { + loader.visitRef(ref) if isSingleRefElement(ref) { var example Example if _, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil { return err } component.Value = &example + component.setRefPath(documentPath) } else { var resolved ExampleRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) @@ -946,7 +1009,9 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP return err } component.Value = resolved.Value + component.setRefPath(resolved.RefPath()) } + defer loader.unvisitRef(ref, component.Value) } return nil } @@ -956,23 +1021,25 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen return errMUSTCallback } - if component.Value != nil { - if loader.visitedCallback == nil { - loader.visitedCallback = make(map[*Callback]struct{}) + if ref := component.Ref; ref != "" { + if component.Value != nil { + return nil } - if _, ok := loader.visitedCallback[component.Value]; ok { + if !loader.shouldVisitRef(ref, func(value any) { + component.Value = value.(*Callback) + _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refDocPath) + }) { return nil } - loader.visitedCallback[component.Value] = struct{}{} - } - - if ref := component.Ref; ref != "" { + loader.visitRef(ref) if isSingleRefElement(ref) { var resolved Callback if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resolved); err != nil { return err } component.Value = &resolved + component.setRefPath(documentPath) } else { var resolved CallbackRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) @@ -986,14 +1053,18 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen return err } component.Value = resolved.Value + component.setRefPath(resolved.RefPath()) } + defer loader.unvisitRef(ref, component.Value) } value := component.Value if value == nil { return nil } - for _, pathItem := range value.Map() { + pathItems := value.Map() + for _, name := range componentNames(pathItems) { + pathItem := pathItems[name] if err = loader.resolvePathItemRef(doc, pathItem, documentPath); err != nil { return err } @@ -1006,23 +1077,25 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u return errMUSTLink } - if component.Value != nil { - if loader.visitedLink == nil { - loader.visitedLink = make(map[*Link]struct{}) + if ref := component.Ref; ref != "" { + if component.Value != nil { + return nil } - if _, ok := loader.visitedLink[component.Value]; ok { + if !loader.shouldVisitRef(ref, func(value any) { + component.Value = value.(*Link) + _, refDocPath, _ := loader.resolveRefPath(ref, documentPath) + component.setRefPath(refDocPath) + }) { return nil } - loader.visitedLink[component.Value] = struct{}{} - } - - if ref := component.Ref; ref != "" { + loader.visitRef(ref) if isSingleRefElement(ref) { var link Link if _, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil { return err } component.Value = &link + component.setRefPath(documentPath) } else { var resolved LinkRef doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved) @@ -1036,7 +1109,9 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u return err } component.Value = resolved.Value + component.setRefPath(resolved.RefPath()) } + defer loader.unvisitRef(ref, component.Value) } return nil } @@ -1051,6 +1126,12 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat if !pathItem.isEmpty() { return } + if !loader.shouldVisitRef(ref, func(value any) { + *pathItem = *value.(*PathItem) + }) { + return nil + } + loader.visitRef(ref) if isSingleRefElement(ref) { var p PathItem if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil { @@ -1068,6 +1149,7 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat *pathItem = resolved } pathItem.Ref = ref + defer loader.unvisitRef(ref, pathItem) } for _, parameter := range pathItem.Parameters { @@ -1075,7 +1157,9 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat return } } - for _, operation := range pathItem.Operations() { + operations := pathItem.Operations() + for _, name := range componentNames(operations) { + operation := operations[name] for _, parameter := range operation.Parameters { if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil { return @@ -1086,12 +1170,15 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat return } } - for _, response := range operation.Responses.Map() { + responses := operation.Responses.Map() + for _, name := range componentNames(responses) { + response := responses[name] if err = loader.resolveResponseRef(doc, response, documentPath); err != nil { return } } - for _, callback := range operation.Callbacks { + for _, name := range componentNames(operation.Callbacks) { + callback := operation.Callbacks[name] if err = loader.resolveCallbackRef(doc, callback, documentPath); err != nil { return } @@ -1103,16 +1190,3 @@ func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPat func unescapeRefString(ref string) string { return strings.Replace(strings.Replace(ref, "~1", "/", -1), "~0", "~", -1) } - -func visitedLimit(visited []string, ref string) bool { - visitedCount := 0 - for _, v := range visited { - if v == ref { - visitedCount++ - if visitedCount >= CircularReferenceCounter { - return true - } - } - } - return false -} diff --git a/openapi3/loader_circular_test.go b/openapi3/loader_circular_test.go new file mode 100644 index 000000000..f16b469d9 --- /dev/null +++ b/openapi3/loader_circular_test.go @@ -0,0 +1,41 @@ +package openapi3 + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoadCircular(t *testing.T) { + loader := NewLoader() + loader.IsExternalRefsAllowed = true + doc, err := loader.LoadFromFile("testdata/circularRef2/circular2.yaml") + require.NoError(t, err) + require.NotNil(t, doc) + + ref := "./AwsEnvironmentSettings.yaml" + + arr := NewArraySchema() + obj := NewObjectSchema() + arr.Items = &SchemaRef{ + Ref: ref, + Value: obj, + } + arr.Items.setRefPath(&url.URL{Path: "testdata/circularRef2/AwsEnvironmentSettings.yaml"}) + obj.Description = "test" + obj.Properties = map[string]*SchemaRef{ + "children": { + Value: arr, + }, + } + + expected := &SchemaRef{ + Ref: ref, + Value: obj, + } + + actual := doc.Paths.Map()["/sample"].Put.RequestBody.Value.Content.Get("application/json").Schema + + require.Equal(t, expected.Value, actual.Value) +} diff --git a/openapi3/loader_read_from_uri_func_test.go b/openapi3/loader_read_from_uri_func_test.go index 8269c8ee6..17ea05ef3 100644 --- a/openapi3/loader_read_from_uri_func_test.go +++ b/openapi3/loader_read_from_uri_func_test.go @@ -14,7 +14,7 @@ func TestLoaderReadFromURIFunc(t *testing.T) { loader := NewLoader() loader.IsExternalRefsAllowed = true loader.ReadFromURIFunc = func(loader *Loader, url *url.URL) ([]byte, error) { - return os.ReadFile(filepath.Join("testdata", url.Path)) + return os.ReadFile(filepath.Join("testdata", filepath.FromSlash(url.Path))) } doc, err := loader.LoadFromFile("recursiveRef/openapi.yml") require.NoError(t, err) diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index 7dcb3ed02..67872d6d5 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -171,7 +171,7 @@ paths: example := doc.Paths.Value("/").Get.Responses.Status(200).Value.Content.Get("application/json").Examples["test"] require.NotNil(t, example.Value) - require.Equal(t, example.Value.Value.(map[string]interface{})["error"].(bool), false) + require.Equal(t, example.Value.Value.(map[string]any)["error"].(bool), false) } func TestLoadErrorOnRefMisuse(t *testing.T) { @@ -520,7 +520,7 @@ func TestLinksFromOAISpec(t *testing.T) { require.NoError(t, err) response := doc.Paths.Value("/2.0/repositories/{username}/{slug}").Get.Responses.Status(200).Value link := response.Links[`repositoryPullRequests`].Value - require.Equal(t, map[string]interface{}{ + require.Equal(t, map[string]any{ "username": "$response.body#/owner/username", "slug": "$response.body#/slug", }, link.Parameters) diff --git a/openapi3/loader_uri_reader.go b/openapi3/loader_uri_reader.go index ba7b5f24a..b023dfb29 100644 --- a/openapi3/loader_uri_reader.go +++ b/openapi3/loader_uri_reader.go @@ -79,7 +79,7 @@ func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) { if !is_file(location) { return nil, ErrURINotSupported } - return os.ReadFile(location.Path) + return os.ReadFile(filepath.FromSlash(location.Path)) } // URIMapCache returns a ReadFromURIFunc that caches the contents read from URI diff --git a/openapi3/maplike.go b/openapi3/maplike.go index 8829b8db5..7b8045c67 100644 --- a/openapi3/maplike.go +++ b/openapi3/maplike.go @@ -64,7 +64,7 @@ func (responses *Responses) Map() (m map[string]*ResponseRef) { var _ jsonpointer.JSONPointable = (*Responses)(nil) // JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable -func (responses Responses) JSONLookup(token string) (interface{}, error) { +func (responses Responses) JSONLookup(token string) (any, error) { if v := responses.Value(token); v == nil { vv, _, err := jsonpointer.GetForToken(responses.Extensions, token) return vv, err @@ -77,8 +77,11 @@ func (responses Responses) JSONLookup(token string) (interface{}, error) { } // MarshalYAML returns the YAML encoding of Responses. -func (responses *Responses) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, responses.Len()+len(responses.Extensions)) +func (responses *Responses) MarshalYAML() (any, error) { + if responses == nil { + return nil, nil + } + m := make(map[string]any, responses.Len()+len(responses.Extensions)) for k, v := range responses.Extensions { m[k] = v } @@ -99,7 +102,7 @@ func (responses *Responses) MarshalJSON() ([]byte, error) { // UnmarshalJSON sets Responses to a copy of data. func (responses *Responses) UnmarshalJSON(data []byte) (err error) { - var m map[string]interface{} + var m map[string]any if err = json.Unmarshal(data, &m); err != nil { return } @@ -111,7 +114,7 @@ func (responses *Responses) UnmarshalJSON(data []byte) (err error) { sort.Strings(ks) x := Responses{ - Extensions: make(map[string]interface{}), + Extensions: make(map[string]any), m: make(map[string]*ResponseRef, len(m)), } @@ -192,7 +195,7 @@ func (callback *Callback) Map() (m map[string]*PathItem) { var _ jsonpointer.JSONPointable = (*Callback)(nil) // JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable -func (callback Callback) JSONLookup(token string) (interface{}, error) { +func (callback Callback) JSONLookup(token string) (any, error) { if v := callback.Value(token); v == nil { vv, _, err := jsonpointer.GetForToken(callback.Extensions, token) return vv, err @@ -205,8 +208,11 @@ func (callback Callback) JSONLookup(token string) (interface{}, error) { } // MarshalYAML returns the YAML encoding of Callback. -func (callback *Callback) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, callback.Len()+len(callback.Extensions)) +func (callback *Callback) MarshalYAML() (any, error) { + if callback == nil { + return nil, nil + } + m := make(map[string]any, callback.Len()+len(callback.Extensions)) for k, v := range callback.Extensions { m[k] = v } @@ -227,7 +233,7 @@ func (callback *Callback) MarshalJSON() ([]byte, error) { // UnmarshalJSON sets Callback to a copy of data. func (callback *Callback) UnmarshalJSON(data []byte) (err error) { - var m map[string]interface{} + var m map[string]any if err = json.Unmarshal(data, &m); err != nil { return } @@ -239,7 +245,7 @@ func (callback *Callback) UnmarshalJSON(data []byte) (err error) { sort.Strings(ks) x := Callback{ - Extensions: make(map[string]interface{}), + Extensions: make(map[string]any), m: make(map[string]*PathItem, len(m)), } @@ -320,7 +326,7 @@ func (paths *Paths) Map() (m map[string]*PathItem) { var _ jsonpointer.JSONPointable = (*Paths)(nil) // JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable -func (paths Paths) JSONLookup(token string) (interface{}, error) { +func (paths Paths) JSONLookup(token string) (any, error) { if v := paths.Value(token); v == nil { vv, _, err := jsonpointer.GetForToken(paths.Extensions, token) return vv, err @@ -333,8 +339,11 @@ func (paths Paths) JSONLookup(token string) (interface{}, error) { } // MarshalYAML returns the YAML encoding of Paths. -func (paths *Paths) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, paths.Len()+len(paths.Extensions)) +func (paths *Paths) MarshalYAML() (any, error) { + if paths == nil { + return nil, nil + } + m := make(map[string]any, paths.Len()+len(paths.Extensions)) for k, v := range paths.Extensions { m[k] = v } @@ -355,7 +364,7 @@ func (paths *Paths) MarshalJSON() ([]byte, error) { // UnmarshalJSON sets Paths to a copy of data. func (paths *Paths) UnmarshalJSON(data []byte) (err error) { - var m map[string]interface{} + var m map[string]any if err = json.Unmarshal(data, &m); err != nil { return } @@ -367,7 +376,7 @@ func (paths *Paths) UnmarshalJSON(data []byte) (err error) { sort.Strings(ks) x := Paths{ - Extensions: make(map[string]interface{}), + Extensions: make(map[string]any), m: make(map[string]*PathItem, len(m)), } diff --git a/openapi3/marsh.go b/openapi3/marsh.go index 9be7bb44c..daa937551 100644 --- a/openapi3/marsh.go +++ b/openapi3/marsh.go @@ -16,7 +16,7 @@ func unmarshalError(jsonUnmarshalErr error) error { return jsonUnmarshalErr } -func unmarshal(data []byte, v interface{}) error { +func unmarshal(data []byte, v any) error { var jsonErr, yamlErr error // See https://github.com/getkin/kin-openapi/issues/680 diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 02de1dbc5..d4466bcf5 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -13,10 +13,10 @@ import ( // MediaType is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#media-type-object type MediaType struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` Encoding map[string]*Encoding `json:"encoding,omitempty" yaml:"encoding,omitempty"` } @@ -41,7 +41,7 @@ func (mediaType *MediaType) WithSchemaRef(schema *SchemaRef) *MediaType { return mediaType } -func (mediaType *MediaType) WithExample(name string, value interface{}) *MediaType { +func (mediaType *MediaType) WithExample(name string, value any) *MediaType { example := mediaType.Examples if example == nil { example = make(map[string]*ExampleRef) @@ -73,8 +73,8 @@ func (mediaType MediaType) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of MediaType. -func (mediaType MediaType) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 4+len(mediaType.Extensions)) +func (mediaType MediaType) MarshalYAML() (any, error) { + m := make(map[string]any, 4+len(mediaType.Extensions)) for k, v := range mediaType.Extensions { m[k] = v } @@ -158,7 +158,7 @@ func (mediaType *MediaType) Validate(ctx context.Context, opts ...ValidationOpti } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (mediaType MediaType) JSONLookup(token string) (interface{}, error) { +func (mediaType MediaType) JSONLookup(token string) (any, error) { switch token { case "schema": if mediaType.Schema != nil { diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 04bac8ff7..ef1592e8c 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net/url" "github.com/go-openapi/jsonpointer" ) @@ -12,7 +13,7 @@ import ( // T is the root of an OpenAPI v3 document // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#openapi-object type T struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` OpenAPI string `json:"openapi" yaml:"openapi"` // Required Components *Components `json:"components,omitempty" yaml:"components,omitempty"` @@ -24,12 +25,13 @@ type T struct { ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` visited visitedComponent + url *url.URL } var _ jsonpointer.JSONPointable = (*T)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (doc *T) JSONLookup(token string) (interface{}, error) { +func (doc *T) JSONLookup(token string) (any, error) { switch token { case "openapi": return doc.OpenAPI, nil @@ -63,8 +65,11 @@ func (doc *T) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of T. -func (doc *T) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 4+len(doc.Extensions)) +func (doc *T) MarshalYAML() (any, error) { + if doc == nil { + return nil, nil + } + m := make(map[string]any, 4+len(doc.Extensions)) for k, v := range doc.Extensions { m[k] = v } diff --git a/openapi3/operation.go b/openapi3/operation.go index 41e3c9b99..40abf73c0 100644 --- a/openapi3/operation.go +++ b/openapi3/operation.go @@ -13,7 +13,7 @@ import ( // Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object type Operation struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` // Optional tags for documentation. Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` @@ -66,8 +66,8 @@ func (operation Operation) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Operation. -func (operation Operation) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 12+len(operation.Extensions)) +func (operation Operation) MarshalYAML() (any, error) { + m := make(map[string]any, 12+len(operation.Extensions)) for k, v := range operation.Extensions { m[k] = v } @@ -136,7 +136,7 @@ func (operation *Operation) UnmarshalJSON(data []byte) error { } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (operation Operation) JSONLookup(token string) (interface{}, error) { +func (operation Operation) JSONLookup(token string) (any, error) { switch token { case "requestBody": if operation.RequestBody != nil { diff --git a/openapi3/parameter.go b/openapi3/parameter.go index a13c1121b..34fe29118 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -17,7 +17,7 @@ type Parameters []*ParameterRef var _ jsonpointer.JSONPointable = (*Parameters)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (p Parameters) JSONLookup(token string) (interface{}, error) { +func (p Parameters) JSONLookup(token string) (any, error) { index, err := strconv.Atoi(token) if err != nil { return nil, err @@ -72,21 +72,21 @@ func (parameters Parameters) Validate(ctx context.Context, opts ...ValidationOpt // Parameter is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object type Parameter struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` - - Name string `json:"name,omitempty" yaml:"name,omitempty"` - In string `json:"in,omitempty" yaml:"in,omitempty"` - Description string `json:"description,omitempty" yaml:"description,omitempty"` - Style string `json:"style,omitempty" yaml:"style,omitempty"` - Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` - AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` - Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - Required bool `json:"required,omitempty" yaml:"required,omitempty"` - Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` - Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` - Content Content `json:"content,omitempty" yaml:"content,omitempty"` + Extensions map[string]any `json:"-" yaml:"-"` + + Name string `json:"name,omitempty" yaml:"name,omitempty"` + In string `json:"in,omitempty" yaml:"in,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Style string `json:"style,omitempty" yaml:"style,omitempty"` + Explode *bool `json:"explode,omitempty" yaml:"explode,omitempty"` + AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + AllowReserved bool `json:"allowReserved,omitempty" yaml:"allowReserved,omitempty"` + Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` + Schema *SchemaRef `json:"schema,omitempty" yaml:"schema,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` + Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"` + Content Content `json:"content,omitempty" yaml:"content,omitempty"` } var _ jsonpointer.JSONPointable = (*Parameter)(nil) @@ -158,8 +158,8 @@ func (parameter Parameter) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Parameter. -func (parameter Parameter) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 13+len(parameter.Extensions)) +func (parameter Parameter) MarshalYAML() (any, error) { + m := make(map[string]any, 13+len(parameter.Extensions)) for k, v := range parameter.Extensions { m[k] = v } @@ -238,7 +238,7 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error { } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (parameter Parameter) JSONLookup(token string) (interface{}, error) { +func (parameter Parameter) JSONLookup(token string) (any, error) { switch token { case "schema": if parameter.Schema != nil { diff --git a/openapi3/path_item.go b/openapi3/path_item.go index 0a6493a1f..859634fe6 100644 --- a/openapi3/path_item.go +++ b/openapi3/path_item.go @@ -11,7 +11,7 @@ import ( // PathItem is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#path-item-object type PathItem struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` Summary string `json:"summary,omitempty" yaml:"summary,omitempty"` @@ -39,12 +39,12 @@ func (pathItem PathItem) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of PathItem. -func (pathItem PathItem) MarshalYAML() (interface{}, error) { +func (pathItem PathItem) MarshalYAML() (any, error) { if ref := pathItem.Ref; ref != "" { return Ref{Ref: ref}, nil } - m := make(map[string]interface{}, 13+len(pathItem.Extensions)) + m := make(map[string]any, 13+len(pathItem.Extensions)) for k, v := range pathItem.Extensions { m[k] = v } diff --git a/openapi3/paths.go b/openapi3/paths.go index ac4f58bbb..76747412b 100644 --- a/openapi3/paths.go +++ b/openapi3/paths.go @@ -10,7 +10,7 @@ import ( // Paths is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object type Paths struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` m map[string]*PathItem } diff --git a/openapi3/refs.go b/openapi3/refs.go index 087e5abfe..846dc55a0 100644 --- a/openapi3/refs.go +++ b/openapi3/refs.go @@ -5,7 +5,9 @@ import ( "context" "encoding/json" "fmt" + "net/url" "sort" + "strings" "github.com/go-openapi/jsonpointer" "github.com/perimeterx/marshmallow" @@ -14,17 +16,43 @@ import ( // CallbackRef represents either a Callback or a $ref to a Callback. // When serializing and both fields are set, Ref is preferred over Value. type CallbackRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Callback extra []string + + refPath url.URL } var _ jsonpointer.JSONPointable = (*CallbackRef)(nil) func (x *CallbackRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } +// RefString returns the $ref value. +func (x *CallbackRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *CallbackRef) CollectionName() string { return "callbacks" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *CallbackRef) RefPath() *url.URL { return &x.refPath } + +func (x *CallbackRef) setRefPath(u *url.URL) { + // Do not set to null or override a path already set. + // References can be loaded multiple times not all with access + // to the correct path info. + if u == nil || x.refPath != (url.URL{}) { + return + } + + x.refPath = *u +} + // MarshalYAML returns the YAML encoding of CallbackRef. -func (x CallbackRef) MarshalYAML() (interface{}, error) { +func (x CallbackRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } @@ -51,6 +79,14 @@ func (x *CallbackRef) UnmarshalJSON(data []byte) error { x.extra = append(x.extra, key) } sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } } return nil } @@ -60,8 +96,9 @@ func (x *CallbackRef) UnmarshalJSON(data []byte) error { // Validate returns an error if CallbackRef does not comply with the OpenAPI spec. func (x *CallbackRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) + exProhibited := getValidationOptions(ctx).schemaExtensionsInRefProhibited + var extras []string if extra := x.extra; len(extra) != 0 { - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed for _, ex := range extra { if allowed != nil { @@ -69,23 +106,46 @@ func (x *CallbackRef) Validate(ctx context.Context, opts ...ValidationOption) er continue } } - extras = append(extras, ex) + // extras in the Extensions checked below + if _, ok := x.Extensions[ex]; !ok { + extras = append(extras, ex) + } } - if len(extras) != 0 { - return fmt.Errorf("extra sibling fields: %+v", extras) + } + + if extra := x.Extensions; exProhibited && len(extra) != 0 { + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) } } + + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + if v := x.Value; v != nil { return v.Validate(ctx) } + return foundUnresolvedRef(x.Ref) } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (x *CallbackRef) JSONLookup(token string) (interface{}, error) { +func (x *CallbackRef) JSONLookup(token string) (any, error) { if token == "$ref" { return x.Ref, nil } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -93,17 +153,43 @@ func (x *CallbackRef) JSONLookup(token string) (interface{}, error) { // ExampleRef represents either a Example or a $ref to a Example. // When serializing and both fields are set, Ref is preferred over Value. type ExampleRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Example extra []string + + refPath url.URL } var _ jsonpointer.JSONPointable = (*ExampleRef)(nil) func (x *ExampleRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } +// RefString returns the $ref value. +func (x *ExampleRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *ExampleRef) CollectionName() string { return "examples" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *ExampleRef) RefPath() *url.URL { return &x.refPath } + +func (x *ExampleRef) setRefPath(u *url.URL) { + // Do not set to null or override a path already set. + // References can be loaded multiple times not all with access + // to the correct path info. + if u == nil || x.refPath != (url.URL{}) { + return + } + + x.refPath = *u +} + // MarshalYAML returns the YAML encoding of ExampleRef. -func (x ExampleRef) MarshalYAML() (interface{}, error) { +func (x ExampleRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } @@ -130,6 +216,14 @@ func (x *ExampleRef) UnmarshalJSON(data []byte) error { x.extra = append(x.extra, key) } sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } } return nil } @@ -139,8 +233,9 @@ func (x *ExampleRef) UnmarshalJSON(data []byte) error { // Validate returns an error if ExampleRef does not comply with the OpenAPI spec. func (x *ExampleRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) + exProhibited := getValidationOptions(ctx).schemaExtensionsInRefProhibited + var extras []string if extra := x.extra; len(extra) != 0 { - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed for _, ex := range extra { if allowed != nil { @@ -148,23 +243,46 @@ func (x *ExampleRef) Validate(ctx context.Context, opts ...ValidationOption) err continue } } - extras = append(extras, ex) + // extras in the Extensions checked below + if _, ok := x.Extensions[ex]; !ok { + extras = append(extras, ex) + } } - if len(extras) != 0 { - return fmt.Errorf("extra sibling fields: %+v", extras) + } + + if extra := x.Extensions; exProhibited && len(extra) != 0 { + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) } } + + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + if v := x.Value; v != nil { return v.Validate(ctx) } + return foundUnresolvedRef(x.Ref) } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (x *ExampleRef) JSONLookup(token string) (interface{}, error) { +func (x *ExampleRef) JSONLookup(token string) (any, error) { if token == "$ref" { return x.Ref, nil } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -172,17 +290,43 @@ func (x *ExampleRef) JSONLookup(token string) (interface{}, error) { // HeaderRef represents either a Header or a $ref to a Header. // When serializing and both fields are set, Ref is preferred over Value. type HeaderRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Header extra []string + + refPath url.URL } var _ jsonpointer.JSONPointable = (*HeaderRef)(nil) func (x *HeaderRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } +// RefString returns the $ref value. +func (x *HeaderRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *HeaderRef) CollectionName() string { return "headers" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *HeaderRef) RefPath() *url.URL { return &x.refPath } + +func (x *HeaderRef) setRefPath(u *url.URL) { + // Do not set to null or override a path already set. + // References can be loaded multiple times not all with access + // to the correct path info. + if u == nil || x.refPath != (url.URL{}) { + return + } + + x.refPath = *u +} + // MarshalYAML returns the YAML encoding of HeaderRef. -func (x HeaderRef) MarshalYAML() (interface{}, error) { +func (x HeaderRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } @@ -209,6 +353,14 @@ func (x *HeaderRef) UnmarshalJSON(data []byte) error { x.extra = append(x.extra, key) } sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } } return nil } @@ -218,8 +370,9 @@ func (x *HeaderRef) UnmarshalJSON(data []byte) error { // Validate returns an error if HeaderRef does not comply with the OpenAPI spec. func (x *HeaderRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) + exProhibited := getValidationOptions(ctx).schemaExtensionsInRefProhibited + var extras []string if extra := x.extra; len(extra) != 0 { - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed for _, ex := range extra { if allowed != nil { @@ -227,23 +380,46 @@ func (x *HeaderRef) Validate(ctx context.Context, opts ...ValidationOption) erro continue } } - extras = append(extras, ex) + // extras in the Extensions checked below + if _, ok := x.Extensions[ex]; !ok { + extras = append(extras, ex) + } } - if len(extras) != 0 { - return fmt.Errorf("extra sibling fields: %+v", extras) + } + + if extra := x.Extensions; exProhibited && len(extra) != 0 { + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) } } + + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + if v := x.Value; v != nil { return v.Validate(ctx) } + return foundUnresolvedRef(x.Ref) } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (x *HeaderRef) JSONLookup(token string) (interface{}, error) { +func (x *HeaderRef) JSONLookup(token string) (any, error) { if token == "$ref" { return x.Ref, nil } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -251,17 +427,43 @@ func (x *HeaderRef) JSONLookup(token string) (interface{}, error) { // LinkRef represents either a Link or a $ref to a Link. // When serializing and both fields are set, Ref is preferred over Value. type LinkRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Link extra []string + + refPath url.URL } var _ jsonpointer.JSONPointable = (*LinkRef)(nil) func (x *LinkRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } +// RefString returns the $ref value. +func (x *LinkRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *LinkRef) CollectionName() string { return "links" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *LinkRef) RefPath() *url.URL { return &x.refPath } + +func (x *LinkRef) setRefPath(u *url.URL) { + // Do not set to null or override a path already set. + // References can be loaded multiple times not all with access + // to the correct path info. + if u == nil || x.refPath != (url.URL{}) { + return + } + + x.refPath = *u +} + // MarshalYAML returns the YAML encoding of LinkRef. -func (x LinkRef) MarshalYAML() (interface{}, error) { +func (x LinkRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } @@ -288,6 +490,14 @@ func (x *LinkRef) UnmarshalJSON(data []byte) error { x.extra = append(x.extra, key) } sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } } return nil } @@ -297,8 +507,9 @@ func (x *LinkRef) UnmarshalJSON(data []byte) error { // Validate returns an error if LinkRef does not comply with the OpenAPI spec. func (x *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) + exProhibited := getValidationOptions(ctx).schemaExtensionsInRefProhibited + var extras []string if extra := x.extra; len(extra) != 0 { - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed for _, ex := range extra { if allowed != nil { @@ -306,23 +517,46 @@ func (x *LinkRef) Validate(ctx context.Context, opts ...ValidationOption) error continue } } - extras = append(extras, ex) + // extras in the Extensions checked below + if _, ok := x.Extensions[ex]; !ok { + extras = append(extras, ex) + } } - if len(extras) != 0 { - return fmt.Errorf("extra sibling fields: %+v", extras) + } + + if extra := x.Extensions; exProhibited && len(extra) != 0 { + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) } } + + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + if v := x.Value; v != nil { return v.Validate(ctx) } + return foundUnresolvedRef(x.Ref) } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (x *LinkRef) JSONLookup(token string) (interface{}, error) { +func (x *LinkRef) JSONLookup(token string) (any, error) { if token == "$ref" { return x.Ref, nil } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -330,17 +564,43 @@ func (x *LinkRef) JSONLookup(token string) (interface{}, error) { // ParameterRef represents either a Parameter or a $ref to a Parameter. // When serializing and both fields are set, Ref is preferred over Value. type ParameterRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Parameter extra []string + + refPath url.URL } var _ jsonpointer.JSONPointable = (*ParameterRef)(nil) func (x *ParameterRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } +// RefString returns the $ref value. +func (x *ParameterRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *ParameterRef) CollectionName() string { return "parameters" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *ParameterRef) RefPath() *url.URL { return &x.refPath } + +func (x *ParameterRef) setRefPath(u *url.URL) { + // Do not set to null or override a path already set. + // References can be loaded multiple times not all with access + // to the correct path info. + if u == nil || x.refPath != (url.URL{}) { + return + } + + x.refPath = *u +} + // MarshalYAML returns the YAML encoding of ParameterRef. -func (x ParameterRef) MarshalYAML() (interface{}, error) { +func (x ParameterRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } @@ -367,6 +627,14 @@ func (x *ParameterRef) UnmarshalJSON(data []byte) error { x.extra = append(x.extra, key) } sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } } return nil } @@ -376,8 +644,9 @@ func (x *ParameterRef) UnmarshalJSON(data []byte) error { // Validate returns an error if ParameterRef does not comply with the OpenAPI spec. func (x *ParameterRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) + exProhibited := getValidationOptions(ctx).schemaExtensionsInRefProhibited + var extras []string if extra := x.extra; len(extra) != 0 { - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed for _, ex := range extra { if allowed != nil { @@ -385,23 +654,46 @@ func (x *ParameterRef) Validate(ctx context.Context, opts ...ValidationOption) e continue } } - extras = append(extras, ex) + // extras in the Extensions checked below + if _, ok := x.Extensions[ex]; !ok { + extras = append(extras, ex) + } } - if len(extras) != 0 { - return fmt.Errorf("extra sibling fields: %+v", extras) + } + + if extra := x.Extensions; exProhibited && len(extra) != 0 { + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) } } + + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + if v := x.Value; v != nil { return v.Validate(ctx) } + return foundUnresolvedRef(x.Ref) } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (x *ParameterRef) JSONLookup(token string) (interface{}, error) { +func (x *ParameterRef) JSONLookup(token string) (any, error) { if token == "$ref" { return x.Ref, nil } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -409,17 +701,43 @@ func (x *ParameterRef) JSONLookup(token string) (interface{}, error) { // RequestBodyRef represents either a RequestBody or a $ref to a RequestBody. // When serializing and both fields are set, Ref is preferred over Value. type RequestBodyRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *RequestBody extra []string + + refPath url.URL } var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil) func (x *RequestBodyRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } +// RefString returns the $ref value. +func (x *RequestBodyRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *RequestBodyRef) CollectionName() string { return "requestBodies" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *RequestBodyRef) RefPath() *url.URL { return &x.refPath } + +func (x *RequestBodyRef) setRefPath(u *url.URL) { + // Do not set to null or override a path already set. + // References can be loaded multiple times not all with access + // to the correct path info. + if u == nil || x.refPath != (url.URL{}) { + return + } + + x.refPath = *u +} + // MarshalYAML returns the YAML encoding of RequestBodyRef. -func (x RequestBodyRef) MarshalYAML() (interface{}, error) { +func (x RequestBodyRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } @@ -446,6 +764,14 @@ func (x *RequestBodyRef) UnmarshalJSON(data []byte) error { x.extra = append(x.extra, key) } sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } } return nil } @@ -455,8 +781,9 @@ func (x *RequestBodyRef) UnmarshalJSON(data []byte) error { // Validate returns an error if RequestBodyRef does not comply with the OpenAPI spec. func (x *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) + exProhibited := getValidationOptions(ctx).schemaExtensionsInRefProhibited + var extras []string if extra := x.extra; len(extra) != 0 { - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed for _, ex := range extra { if allowed != nil { @@ -464,23 +791,46 @@ func (x *RequestBodyRef) Validate(ctx context.Context, opts ...ValidationOption) continue } } - extras = append(extras, ex) + // extras in the Extensions checked below + if _, ok := x.Extensions[ex]; !ok { + extras = append(extras, ex) + } } - if len(extras) != 0 { - return fmt.Errorf("extra sibling fields: %+v", extras) + } + + if extra := x.Extensions; exProhibited && len(extra) != 0 { + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) } } + + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + if v := x.Value; v != nil { return v.Validate(ctx) } + return foundUnresolvedRef(x.Ref) } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (x *RequestBodyRef) JSONLookup(token string) (interface{}, error) { +func (x *RequestBodyRef) JSONLookup(token string) (any, error) { if token == "$ref" { return x.Ref, nil } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -488,17 +838,43 @@ func (x *RequestBodyRef) JSONLookup(token string) (interface{}, error) { // ResponseRef represents either a Response or a $ref to a Response. // When serializing and both fields are set, Ref is preferred over Value. type ResponseRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Response extra []string + + refPath url.URL } var _ jsonpointer.JSONPointable = (*ResponseRef)(nil) func (x *ResponseRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } +// RefString returns the $ref value. +func (x *ResponseRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *ResponseRef) CollectionName() string { return "responses" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *ResponseRef) RefPath() *url.URL { return &x.refPath } + +func (x *ResponseRef) setRefPath(u *url.URL) { + // Do not set to null or override a path already set. + // References can be loaded multiple times not all with access + // to the correct path info. + if u == nil || x.refPath != (url.URL{}) { + return + } + + x.refPath = *u +} + // MarshalYAML returns the YAML encoding of ResponseRef. -func (x ResponseRef) MarshalYAML() (interface{}, error) { +func (x ResponseRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } @@ -525,6 +901,14 @@ func (x *ResponseRef) UnmarshalJSON(data []byte) error { x.extra = append(x.extra, key) } sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } } return nil } @@ -534,8 +918,9 @@ func (x *ResponseRef) UnmarshalJSON(data []byte) error { // Validate returns an error if ResponseRef does not comply with the OpenAPI spec. func (x *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) + exProhibited := getValidationOptions(ctx).schemaExtensionsInRefProhibited + var extras []string if extra := x.extra; len(extra) != 0 { - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed for _, ex := range extra { if allowed != nil { @@ -543,23 +928,46 @@ func (x *ResponseRef) Validate(ctx context.Context, opts ...ValidationOption) er continue } } - extras = append(extras, ex) + // extras in the Extensions checked below + if _, ok := x.Extensions[ex]; !ok { + extras = append(extras, ex) + } } - if len(extras) != 0 { - return fmt.Errorf("extra sibling fields: %+v", extras) + } + + if extra := x.Extensions; exProhibited && len(extra) != 0 { + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) } } + + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + if v := x.Value; v != nil { return v.Validate(ctx) } + return foundUnresolvedRef(x.Ref) } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (x *ResponseRef) JSONLookup(token string) (interface{}, error) { +func (x *ResponseRef) JSONLookup(token string) (any, error) { if token == "$ref" { return x.Ref, nil } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -567,17 +975,43 @@ func (x *ResponseRef) JSONLookup(token string) (interface{}, error) { // SchemaRef represents either a Schema or a $ref to a Schema. // When serializing and both fields are set, Ref is preferred over Value. type SchemaRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *Schema extra []string + + refPath url.URL } var _ jsonpointer.JSONPointable = (*SchemaRef)(nil) func (x *SchemaRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } +// RefString returns the $ref value. +func (x *SchemaRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *SchemaRef) CollectionName() string { return "schemas" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *SchemaRef) RefPath() *url.URL { return &x.refPath } + +func (x *SchemaRef) setRefPath(u *url.URL) { + // Do not set to null or override a path already set. + // References can be loaded multiple times not all with access + // to the correct path info. + if u == nil || x.refPath != (url.URL{}) { + return + } + + x.refPath = *u +} + // MarshalYAML returns the YAML encoding of SchemaRef. -func (x SchemaRef) MarshalYAML() (interface{}, error) { +func (x SchemaRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } @@ -604,6 +1038,14 @@ func (x *SchemaRef) UnmarshalJSON(data []byte) error { x.extra = append(x.extra, key) } sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } } return nil } @@ -613,8 +1055,9 @@ func (x *SchemaRef) UnmarshalJSON(data []byte) error { // Validate returns an error if SchemaRef does not comply with the OpenAPI spec. func (x *SchemaRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) + exProhibited := getValidationOptions(ctx).schemaExtensionsInRefProhibited + var extras []string if extra := x.extra; len(extra) != 0 { - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed for _, ex := range extra { if allowed != nil { @@ -622,23 +1065,46 @@ func (x *SchemaRef) Validate(ctx context.Context, opts ...ValidationOption) erro continue } } - extras = append(extras, ex) + // extras in the Extensions checked below + if _, ok := x.Extensions[ex]; !ok { + extras = append(extras, ex) + } } - if len(extras) != 0 { - return fmt.Errorf("extra sibling fields: %+v", extras) + } + + if extra := x.Extensions; exProhibited && len(extra) != 0 { + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) } } + + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + if v := x.Value; v != nil { return v.Validate(ctx) } + return foundUnresolvedRef(x.Ref) } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (x *SchemaRef) JSONLookup(token string) (interface{}, error) { +func (x *SchemaRef) JSONLookup(token string) (any, error) { if token == "$ref" { return x.Ref, nil } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } @@ -646,17 +1112,43 @@ func (x *SchemaRef) JSONLookup(token string) (interface{}, error) { // SecuritySchemeRef represents either a SecurityScheme or a $ref to a SecurityScheme. // When serializing and both fields are set, Ref is preferred over Value. type SecuritySchemeRef struct { + // Extensions only captures fields starting with 'x-' as no other fields + // are allowed by the openapi spec. + Extensions map[string]any + Ref string Value *SecurityScheme extra []string + + refPath url.URL } var _ jsonpointer.JSONPointable = (*SecuritySchemeRef)(nil) func (x *SecuritySchemeRef) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } +// RefString returns the $ref value. +func (x *SecuritySchemeRef) RefString() string { return x.Ref } + +// CollectionName returns the JSON string used for a collection of these components. +func (x *SecuritySchemeRef) CollectionName() string { return "securitySchemes" } + +// RefPath returns the path of the $ref relative to the root document. +func (x *SecuritySchemeRef) RefPath() *url.URL { return &x.refPath } + +func (x *SecuritySchemeRef) setRefPath(u *url.URL) { + // Do not set to null or override a path already set. + // References can be loaded multiple times not all with access + // to the correct path info. + if u == nil || x.refPath != (url.URL{}) { + return + } + + x.refPath = *u +} + // MarshalYAML returns the YAML encoding of SecuritySchemeRef. -func (x SecuritySchemeRef) MarshalYAML() (interface{}, error) { +func (x SecuritySchemeRef) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } @@ -683,6 +1175,14 @@ func (x *SecuritySchemeRef) UnmarshalJSON(data []byte) error { x.extra = append(x.extra, key) } sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } } return nil } @@ -692,8 +1192,9 @@ func (x *SecuritySchemeRef) UnmarshalJSON(data []byte) error { // Validate returns an error if SecuritySchemeRef does not comply with the OpenAPI spec. func (x *SecuritySchemeRef) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) + exProhibited := getValidationOptions(ctx).schemaExtensionsInRefProhibited + var extras []string if extra := x.extra; len(extra) != 0 { - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed for _, ex := range extra { if allowed != nil { @@ -701,23 +1202,46 @@ func (x *SecuritySchemeRef) Validate(ctx context.Context, opts ...ValidationOpti continue } } - extras = append(extras, ex) + // extras in the Extensions checked below + if _, ok := x.Extensions[ex]; !ok { + extras = append(extras, ex) + } } - if len(extras) != 0 { - return fmt.Errorf("extra sibling fields: %+v", extras) + } + + if extra := x.Extensions; exProhibited && len(extra) != 0 { + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) } } + + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + if v := x.Value; v != nil { return v.Validate(ctx) } + return foundUnresolvedRef(x.Ref) } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (x *SecuritySchemeRef) JSONLookup(token string) (interface{}, error) { +func (x *SecuritySchemeRef) JSONLookup(token string) (any, error) { if token == "$ref" { return x.Ref, nil } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } diff --git a/openapi3/refs.tmpl b/openapi3/refs.tmpl index 638d6469d..f9ed1e6e9 100644 --- a/openapi3/refs.tmpl +++ b/openapi3/refs.tmpl @@ -5,34 +5,62 @@ import ( "context" "encoding/json" "fmt" + "net/url" "sort" + "strings" "github.com/go-openapi/jsonpointer" "github.com/perimeterx/marshmallow" ) {{ range $type := .Types }} -// {{ $type }}Ref represents either a {{ $type }} or a $ref to a {{ $type }}. +// {{ $type.Name }}Ref represents either a {{ $type.Name }} or a $ref to a {{ $type.Name }}. // When serializing and both fields are set, Ref is preferred over Value. -type {{ $type }}Ref struct { +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 + Ref string - Value *{{ $type }} + Value *{{ $type.Name }} extra []string + + refPath url.URL } -var _ jsonpointer.JSONPointable = (*{{ $type }}Ref)(nil) +var _ jsonpointer.JSONPointable = (*{{ $type.Name }}Ref)(nil) + +func (x *{{ $type.Name }}Ref) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } + +// RefString returns the $ref value. +func (x *{{ $type.Name }}Ref) RefString() string { return x.Ref } -func (x *{{ $type }}Ref) isEmpty() bool { return x == nil || x.Ref == "" && x.Value == nil } +// CollectionName returns the JSON string used for a collection of these components. +func (x *{{ $type.Name }}Ref) CollectionName() string { return "{{ $type.CollectionName }}" } -// MarshalYAML returns the YAML encoding of {{ $type }}Ref. -func (x {{ $type }}Ref) MarshalYAML() (interface{}, error) { +// RefPath returns the path of the $ref relative to the root document. +func (x *{{ $type.Name }}Ref) RefPath() *url.URL { return &x.refPath } + +func (x *{{ $type.Name }}Ref) setRefPath(u *url.URL) { + // Do not set to null or override a path already set. + // References can be loaded multiple times not all with access + // to the correct path info. + if u == nil || x.refPath != (url.URL{}) { + return + } + + x.refPath = *u +} + +// MarshalYAML returns the YAML encoding of {{ $type.Name }}Ref. +func (x {{ $type.Name }}Ref) MarshalYAML() (any, error) { if ref := x.Ref; ref != "" { return &Ref{Ref: ref}, nil } return x.Value.MarshalYAML() } -// MarshalJSON returns the JSON encoding of {{ $type }}Ref. -func (x {{ $type }}Ref) MarshalJSON() ([]byte, error) { +// MarshalJSON returns the JSON encoding of {{ $type.Name }}Ref. +func (x {{ $type.Name }}Ref) MarshalJSON() ([]byte, error) { y, err := x.MarshalYAML() if err != nil { return nil, err @@ -40,8 +68,8 @@ func (x {{ $type }}Ref) MarshalJSON() ([]byte, error) { return json.Marshal(y) } -// UnmarshalJSON sets {{ $type }}Ref to a copy of data. -func (x *{{ $type }}Ref) UnmarshalJSON(data []byte) error { +// UnmarshalJSON sets {{ $type.Name }}Ref to a copy of data. +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 @@ -51,17 +79,26 @@ func (x *{{ $type }}Ref) UnmarshalJSON(data []byte) error { x.extra = append(x.extra, key) } sort.Strings(x.extra) + for k := range extra { + if !strings.HasPrefix(k, "x-") { + delete(extra, k) + } + } + if len(extra) != 0 { + x.Extensions = extra + } } return nil } return json.Unmarshal(data, &x.Value) } -// Validate returns an error if {{ $type }}Ref does not comply with the OpenAPI spec. -func (x *{{ $type }}Ref) Validate(ctx context.Context, opts ...ValidationOption) error { +// Validate returns an error if {{ $type.Name }}Ref does not comply with the OpenAPI spec. +func (x *{{ $type.Name }}Ref) Validate(ctx context.Context, opts ...ValidationOption) error { ctx = WithValidationOptions(ctx, opts...) + exProhibited := getValidationOptions(ctx).schemaExtensionsInRefProhibited + var extras []string if extra := x.extra; len(extra) != 0 { - extras := make([]string, 0, len(extra)) allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed for _, ex := range extra { if allowed != nil { @@ -69,23 +106,46 @@ func (x *{{ $type }}Ref) Validate(ctx context.Context, opts ...ValidationOption) continue } } - extras = append(extras, ex) + // extras in the Extensions checked below + if _, ok := x.Extensions[ex]; !ok { + extras = append(extras, ex) + } } - if len(extras) != 0 { - return fmt.Errorf("extra sibling fields: %+v", extras) + } + + if extra := x.Extensions; exProhibited && len(extra) != 0 { + allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed + for ex := range extra { + if allowed != nil { + if _, ok := allowed[ex]; ok { + continue + } + } + extras = append(extras, ex) } } + + if len(extras) != 0 { + return fmt.Errorf("extra sibling fields: %+v", extras) + } + if v := x.Value; v != nil { return v.Validate(ctx) } + return foundUnresolvedRef(x.Ref) } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (x *{{ $type }}Ref) JSONLookup(token string) (interface{}, error) { +func (x *{{ $type.Name }}Ref) JSONLookup(token string) (any, error) { if token == "$ref" { return x.Ref, nil } + + if v, ok := x.Extensions[token]; ok { + return v, nil + } + ptr, _, err := jsonpointer.GetForToken(x.Value, token) return ptr, err } diff --git a/openapi3/refs_issue222_test.go b/openapi3/refs_issue222_test.go new file mode 100644 index 000000000..646d48751 --- /dev/null +++ b/openapi3/refs_issue222_test.go @@ -0,0 +1,113 @@ +package openapi3 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIssue222(t *testing.T) { + spec := []byte(` +openapi: 3.0.0 +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +servers: + - url: 'http://petstore.swagger.io/v1' +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + schema: + type: integer + format: int32 + responses: + '200': # <--------------- PANIC HERE + + post: + summary: Create a pet + operationId: createPets + tags: + - pets + responses: + '201': + description: Null response + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '/pets/{petId}': + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - name: petId + in: path + required: true + description: The id of the pet to retrieve + schema: + type: string + responses: + '200': + description: Expected response to a valid request + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + default: + description: unexpected error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string +`[1:]) + + loader := NewLoader() + doc, err := loader.LoadFromData(spec) + require.EqualError(t, err, `invalid response: value MUST be an object`) + require.Nil(t, doc) +} diff --git a/openapi3/refs_issue247_test.go b/openapi3/refs_issue247_test.go new file mode 100644 index 000000000..62f056d87 --- /dev/null +++ b/openapi3/refs_issue247_test.go @@ -0,0 +1,228 @@ +package openapi3 + +import ( + "reflect" + "testing" + + "github.com/go-openapi/jsonpointer" + "github.com/stretchr/testify/require" +) + +func TestIssue247(t *testing.T) { + spec := []byte(` +openapi: 3.0.2 +info: + title: Swagger Petstore - OpenAPI 3.0 + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html + version: 1.0.5 +servers: +- url: /api/v3 +tags: +- name: pet + description: Everything about your Pets + externalDocs: + description: Find out more + url: http://swagger.io +- name: store + description: Operations about user +- name: user + description: Access to Petstore orders + externalDocs: + description: Find out more about our store + url: http://swagger.io +paths: + /pet: + put: + tags: + - pet + summary: Update an existing pet + description: Update an existing pet by Id + operationId: updatePet + requestBody: + description: Update an existent pet in the store + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + "200": + description: Successful operation + content: + application/xml: + schema: + $ref: '#/components/schemas/Pet' + application/json: + schema: + $ref: '#/components/schemas/Pet' + "400": + description: Invalid ID supplied + "404": + description: Pet not found + "405": + description: Validation exception + security: + - petstore_auth: + - write:pets + - read:pets +components: + schemas: + Pet: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/components/schemas/Pet' + Error: + type: object + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string + OneOfTest: + type: object + oneOf: + - type: string + - type: integer + format: int32 +`[1:]) + + loader := NewLoader() + doc, err := loader.LoadFromData(spec) + require.NoError(t, err) + require.NotNil(t, doc) + + err = doc.Validate(loader.Context) + require.NoError(t, err) + + var ptr jsonpointer.Pointer + var v interface{} + var kind reflect.Kind + + ptr, err = jsonpointer.New("/paths") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, &Paths{}, v) + require.Equal(t, reflect.TypeOf(&Paths{}).Kind(), kind) + + ptr, err = jsonpointer.New("/paths/~1pet") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, &PathItem{}, v) + require.Equal(t, reflect.TypeOf(&PathItem{}).Kind(), kind) + + ptr, err = jsonpointer.New("/paths/~1pet/put") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, &Operation{}, v) + require.Equal(t, reflect.TypeOf(&Operation{}).Kind(), kind) + + ptr, err = jsonpointer.New("/paths/~1pet/put/responses") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, &Responses{}, v) + require.Equal(t, reflect.TypeOf(&Responses{}).Kind(), kind) + + ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, &Response{}, v) + require.Equal(t, reflect.TypeOf(&Response{}).Kind(), kind) + + ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200/content") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, Content{}, v) + require.Equal(t, reflect.TypeOf(Content{}).Kind(), kind) + + ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200/content/application~1json/schema") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, &Ref{}, v) + require.Equal(t, reflect.Ptr, kind) + require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) + + ptr, err = jsonpointer.New("/components/schemas/Pets/items") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, &Ref{}, v) + require.Equal(t, reflect.Ptr, kind) + require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) + + ptr, err = jsonpointer.New("/components/schemas/Error/properties/code") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, &Schema{}, v) + require.Equal(t, reflect.Ptr, kind) + require.Equal(t, &Types{"integer"}, v.(*Schema).Type) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/0") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, &Schema{}, v) + require.Equal(t, reflect.Ptr, kind) + require.Equal(t, &Types{"string"}, v.(*Schema).Type) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/1") + require.NoError(t, err) + v, kind, err = ptr.Get(doc) + require.NoError(t, err) + require.NotNil(t, v) + require.IsType(t, &Schema{}, v) + require.Equal(t, reflect.Ptr, kind) + require.Equal(t, &Types{"integer"}, v.(*Schema).Type) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/5") + require.NoError(t, err) + _, _, err = ptr.Get(doc) + require.Error(t, err) + + ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/-1") + require.NoError(t, err) + _, _, err = ptr.Get(doc) + require.Error(t, err) +} diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go index 8a12c33c0..b6de316f0 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -1,334 +1,354 @@ +// Code generated by go generate; DO NOT EDIT. package openapi3 import ( - "reflect" + "context" + "encoding/json" "testing" - "github.com/go-openapi/jsonpointer" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestIssue222(t *testing.T) { - spec := []byte(` -openapi: 3.0.0 -info: - version: 1.0.0 - title: Swagger Petstore - license: - name: MIT -servers: - - url: 'http://petstore.swagger.io/v1' -paths: - /pets: - get: - summary: List all pets - operationId: listPets - tags: - - pets - parameters: - - name: limit - in: query - description: How many items to return at one time (max 100) - required: false - schema: - type: integer - format: int32 - responses: - '200': # <--------------- PANIC HERE - - post: - summary: Create a pet - operationId: createPets - tags: - - pets - responses: - '201': - description: Null response - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '/pets/{petId}': - get: - summary: Info for a specific pet - operationId: showPetById - tags: - - pets - parameters: - - name: petId - in: path - required: true - description: The id of the pet to retrieve - schema: - type: string - responses: - '200': - description: Expected response to a valid request - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - default: - description: unexpected error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' -components: - schemas: - Pet: - type: object - required: - - id - - name - properties: - id: - type: integer - format: int64 - name: - type: string - tag: - type: string - Pets: - type: array - items: - $ref: '#/components/schemas/Pet' - Error: - type: object - required: - - code - - message - properties: - code: - type: integer - format: int32 - message: - type: string -`[1:]) - - loader := NewLoader() - doc, err := loader.LoadFromData(spec) - require.EqualError(t, err, `invalid response: value MUST be an object`) - require.Nil(t, doc) +func TestCallbackRef_Extensions(t *testing.T) { + data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + + ref := CallbackRef{} + err := json.Unmarshal(data, &ref) + assert.NoError(t, err) + + // captures extension + assert.Equal(t, "#/components/schemas/Pet", ref.Ref) + assert.Equal(t, float64(1), ref.Extensions["x-order"]) + + // does not capture non-extensions + assert.Nil(t, ref.Extensions["something"]) + + // validation + err = ref.Validate(context.Background()) + require.EqualError(t, err, "extra sibling fields: [something]") + + err = ref.Validate(context.Background(), ProhibitExtensionsWithRef()) + require.EqualError(t, err, "extra sibling fields: [something x-order]") + + err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) + assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + + // non-extension not json lookable + _, err = ref.JSONLookup("something") + assert.Error(t, err) + + t.Run("extentions in value", func(t *testing.T) { + ref.Value = &Callback{Extensions: map[string]any{}} + ref.Value.Extensions["x-order"] = 2.0 + + // prefers the value next to the \$ref over the one in the \$ref. + v, err := ref.JSONLookup("x-order") + assert.NoError(t, err) + assert.Equal(t, float64(1), v) + }) +} + +func TestExampleRef_Extensions(t *testing.T) { + data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + + ref := ExampleRef{} + err := json.Unmarshal(data, &ref) + assert.NoError(t, err) + + // captures extension + assert.Equal(t, "#/components/schemas/Pet", ref.Ref) + assert.Equal(t, float64(1), ref.Extensions["x-order"]) + + // does not capture non-extensions + assert.Nil(t, ref.Extensions["something"]) + + // validation + err = ref.Validate(context.Background()) + require.EqualError(t, err, "extra sibling fields: [something]") + + err = ref.Validate(context.Background(), ProhibitExtensionsWithRef()) + require.EqualError(t, err, "extra sibling fields: [something x-order]") + + err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) + assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + + // non-extension not json lookable + _, err = ref.JSONLookup("something") + assert.Error(t, err) + + t.Run("extentions in value", func(t *testing.T) { + ref.Value = &Example{Extensions: map[string]any{}} + ref.Value.Extensions["x-order"] = 2.0 + + // prefers the value next to the \$ref over the one in the \$ref. + v, err := ref.JSONLookup("x-order") + assert.NoError(t, err) + assert.Equal(t, float64(1), v) + }) +} + +func TestHeaderRef_Extensions(t *testing.T) { + data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + + ref := HeaderRef{} + err := json.Unmarshal(data, &ref) + assert.NoError(t, err) + + // captures extension + assert.Equal(t, "#/components/schemas/Pet", ref.Ref) + assert.Equal(t, float64(1), ref.Extensions["x-order"]) + + // does not capture non-extensions + assert.Nil(t, ref.Extensions["something"]) + + // validation + err = ref.Validate(context.Background()) + require.EqualError(t, err, "extra sibling fields: [something]") + + err = ref.Validate(context.Background(), ProhibitExtensionsWithRef()) + require.EqualError(t, err, "extra sibling fields: [something x-order]") + + err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) + assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + + // non-extension not json lookable + _, err = ref.JSONLookup("something") + assert.Error(t, err) + + // Header does not have its own extensions. +} + +func TestLinkRef_Extensions(t *testing.T) { + data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + + ref := LinkRef{} + err := json.Unmarshal(data, &ref) + assert.NoError(t, err) + + // captures extension + assert.Equal(t, "#/components/schemas/Pet", ref.Ref) + assert.Equal(t, float64(1), ref.Extensions["x-order"]) + + // does not capture non-extensions + assert.Nil(t, ref.Extensions["something"]) + + // validation + err = ref.Validate(context.Background()) + require.EqualError(t, err, "extra sibling fields: [something]") + + err = ref.Validate(context.Background(), ProhibitExtensionsWithRef()) + require.EqualError(t, err, "extra sibling fields: [something x-order]") + + err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) + assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + + // non-extension not json lookable + _, err = ref.JSONLookup("something") + assert.Error(t, err) + + t.Run("extentions in value", func(t *testing.T) { + ref.Value = &Link{Extensions: map[string]any{}} + ref.Value.Extensions["x-order"] = 2.0 + + // prefers the value next to the \$ref over the one in the \$ref. + v, err := ref.JSONLookup("x-order") + assert.NoError(t, err) + assert.Equal(t, float64(1), v) + }) +} + +func TestParameterRef_Extensions(t *testing.T) { + data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + + ref := ParameterRef{} + err := json.Unmarshal(data, &ref) + assert.NoError(t, err) + + // captures extension + assert.Equal(t, "#/components/schemas/Pet", ref.Ref) + assert.Equal(t, float64(1), ref.Extensions["x-order"]) + + // does not capture non-extensions + assert.Nil(t, ref.Extensions["something"]) + + // validation + err = ref.Validate(context.Background()) + require.EqualError(t, err, "extra sibling fields: [something]") + + err = ref.Validate(context.Background(), ProhibitExtensionsWithRef()) + require.EqualError(t, err, "extra sibling fields: [something x-order]") + + err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) + assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + + // non-extension not json lookable + _, err = ref.JSONLookup("something") + assert.Error(t, err) + + t.Run("extentions in value", func(t *testing.T) { + ref.Value = &Parameter{Extensions: map[string]any{}} + ref.Value.Extensions["x-order"] = 2.0 + + // prefers the value next to the \$ref over the one in the \$ref. + v, err := ref.JSONLookup("x-order") + assert.NoError(t, err) + assert.Equal(t, float64(1), v) + }) +} + +func TestRequestBodyRef_Extensions(t *testing.T) { + data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + + ref := RequestBodyRef{} + err := json.Unmarshal(data, &ref) + assert.NoError(t, err) + + // captures extension + assert.Equal(t, "#/components/schemas/Pet", ref.Ref) + assert.Equal(t, float64(1), ref.Extensions["x-order"]) + + // does not capture non-extensions + assert.Nil(t, ref.Extensions["something"]) + + // validation + err = ref.Validate(context.Background()) + require.EqualError(t, err, "extra sibling fields: [something]") + + err = ref.Validate(context.Background(), ProhibitExtensionsWithRef()) + require.EqualError(t, err, "extra sibling fields: [something x-order]") + + err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) + assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + + // non-extension not json lookable + _, err = ref.JSONLookup("something") + assert.Error(t, err) + + t.Run("extentions in value", func(t *testing.T) { + ref.Value = &RequestBody{Extensions: map[string]any{}} + ref.Value.Extensions["x-order"] = 2.0 + + // prefers the value next to the \$ref over the one in the \$ref. + v, err := ref.JSONLookup("x-order") + assert.NoError(t, err) + assert.Equal(t, float64(1), v) + }) } -func TestIssue247(t *testing.T) { - spec := []byte(` -openapi: 3.0.2 -info: - title: Swagger Petstore - OpenAPI 3.0 - license: - name: Apache 2.0 - url: http://www.apache.org/licenses/LICENSE-2.0.html - version: 1.0.5 -servers: -- url: /api/v3 -tags: -- name: pet - description: Everything about your Pets - externalDocs: - description: Find out more - url: http://swagger.io -- name: store - description: Operations about user -- name: user - description: Access to Petstore orders - externalDocs: - description: Find out more about our store - url: http://swagger.io -paths: - /pet: - put: - tags: - - pet - summary: Update an existing pet - description: Update an existing pet by Id - operationId: updatePet - requestBody: - description: Update an existent pet in the store - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - $ref: '#/components/schemas/Pet' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Pet' - required: true - responses: - "200": - description: Successful operation - content: - application/xml: - schema: - $ref: '#/components/schemas/Pet' - application/json: - schema: - $ref: '#/components/schemas/Pet' - "400": - description: Invalid ID supplied - "404": - description: Pet not found - "405": - description: Validation exception - security: - - petstore_auth: - - write:pets - - read:pets -components: - schemas: - Pet: - type: object - required: - - id - - name - properties: - id: - type: integer - format: int64 - name: - type: string - tag: - type: string - Pets: - type: array - items: - $ref: '#/components/schemas/Pet' - Error: - type: object - required: - - code - - message - properties: - code: - type: integer - format: int32 - message: - type: string - OneOfTest: - type: object - oneOf: - - type: string - - type: integer - format: int32 -`[1:]) - - loader := NewLoader() - doc, err := loader.LoadFromData(spec) - require.NoError(t, err) - require.NotNil(t, doc) - - err = doc.Validate(loader.Context) - require.NoError(t, err) - - var ptr jsonpointer.Pointer - var v interface{} - var kind reflect.Kind - - ptr, err = jsonpointer.New("/paths") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, &Paths{}, v) - require.Equal(t, reflect.TypeOf(&Paths{}).Kind(), kind) - - ptr, err = jsonpointer.New("/paths/~1pet") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, &PathItem{}, v) - require.Equal(t, reflect.TypeOf(&PathItem{}).Kind(), kind) - - ptr, err = jsonpointer.New("/paths/~1pet/put") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, &Operation{}, v) - require.Equal(t, reflect.TypeOf(&Operation{}).Kind(), kind) - - ptr, err = jsonpointer.New("/paths/~1pet/put/responses") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, &Responses{}, v) - require.Equal(t, reflect.TypeOf(&Responses{}).Kind(), kind) - - ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, &Response{}, v) - require.Equal(t, reflect.TypeOf(&Response{}).Kind(), kind) - - ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200/content") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, Content{}, v) - require.Equal(t, reflect.TypeOf(Content{}).Kind(), kind) - - ptr, err = jsonpointer.New("/paths/~1pet/put/responses/200/content/application~1json/schema") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, &Ref{}, v) - require.Equal(t, reflect.Ptr, kind) - require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) - - ptr, err = jsonpointer.New("/components/schemas/Pets/items") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, &Ref{}, v) - require.Equal(t, reflect.Ptr, kind) - require.Equal(t, "#/components/schemas/Pet", v.(*Ref).Ref) - - ptr, err = jsonpointer.New("/components/schemas/Error/properties/code") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, &Schema{}, v) - require.Equal(t, reflect.Ptr, kind) - require.Equal(t, &Types{"integer"}, v.(*Schema).Type) - - ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/0") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, &Schema{}, v) - require.Equal(t, reflect.Ptr, kind) - require.Equal(t, &Types{"string"}, v.(*Schema).Type) - - ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/1") - require.NoError(t, err) - v, kind, err = ptr.Get(doc) - require.NoError(t, err) - require.NotNil(t, v) - require.IsType(t, &Schema{}, v) - require.Equal(t, reflect.Ptr, kind) - require.Equal(t, &Types{"integer"}, v.(*Schema).Type) - - ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/5") - require.NoError(t, err) - _, _, err = ptr.Get(doc) - require.Error(t, err) - - ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/-1") - require.NoError(t, err) - _, _, err = ptr.Get(doc) - require.Error(t, err) +func TestResponseRef_Extensions(t *testing.T) { + data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + + ref := ResponseRef{} + err := json.Unmarshal(data, &ref) + assert.NoError(t, err) + + // captures extension + assert.Equal(t, "#/components/schemas/Pet", ref.Ref) + assert.Equal(t, float64(1), ref.Extensions["x-order"]) + + // does not capture non-extensions + assert.Nil(t, ref.Extensions["something"]) + + // validation + err = ref.Validate(context.Background()) + require.EqualError(t, err, "extra sibling fields: [something]") + + err = ref.Validate(context.Background(), ProhibitExtensionsWithRef()) + require.EqualError(t, err, "extra sibling fields: [something x-order]") + + err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) + assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + + // non-extension not json lookable + _, err = ref.JSONLookup("something") + assert.Error(t, err) + + t.Run("extentions in value", func(t *testing.T) { + ref.Value = &Response{Extensions: map[string]any{}} + ref.Value.Extensions["x-order"] = 2.0 + + // prefers the value next to the \$ref over the one in the \$ref. + v, err := ref.JSONLookup("x-order") + assert.NoError(t, err) + assert.Equal(t, float64(1), v) + }) +} + +func TestSchemaRef_Extensions(t *testing.T) { + data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + + ref := SchemaRef{} + err := json.Unmarshal(data, &ref) + assert.NoError(t, err) + + // captures extension + assert.Equal(t, "#/components/schemas/Pet", ref.Ref) + assert.Equal(t, float64(1), ref.Extensions["x-order"]) + + // does not capture non-extensions + assert.Nil(t, ref.Extensions["something"]) + + // validation + err = ref.Validate(context.Background()) + require.EqualError(t, err, "extra sibling fields: [something]") + + err = ref.Validate(context.Background(), ProhibitExtensionsWithRef()) + require.EqualError(t, err, "extra sibling fields: [something x-order]") + + err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) + assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + + // non-extension not json lookable + _, err = ref.JSONLookup("something") + assert.Error(t, err) + + t.Run("extentions in value", func(t *testing.T) { + ref.Value = &Schema{Extensions: map[string]any{}} + ref.Value.Extensions["x-order"] = 2.0 + + // prefers the value next to the \$ref over the one in the \$ref. + v, err := ref.JSONLookup("x-order") + assert.NoError(t, err) + assert.Equal(t, float64(1), v) + }) +} + +func TestSecuritySchemeRef_Extensions(t *testing.T) { + data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + + ref := SecuritySchemeRef{} + err := json.Unmarshal(data, &ref) + assert.NoError(t, err) + + // captures extension + assert.Equal(t, "#/components/schemas/Pet", ref.Ref) + assert.Equal(t, float64(1), ref.Extensions["x-order"]) + + // does not capture non-extensions + assert.Nil(t, ref.Extensions["something"]) + + // validation + err = ref.Validate(context.Background()) + require.EqualError(t, err, "extra sibling fields: [something]") + + err = ref.Validate(context.Background(), ProhibitExtensionsWithRef()) + require.EqualError(t, err, "extra sibling fields: [something x-order]") + + err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) + assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + + // non-extension not json lookable + _, err = ref.JSONLookup("something") + assert.Error(t, err) + + t.Run("extentions in value", func(t *testing.T) { + ref.Value = &SecurityScheme{Extensions: map[string]any{}} + ref.Value.Extensions["x-order"] = 2.0 + + // prefers the value next to the \$ref over the one in the \$ref. + v, err := ref.JSONLookup("x-order") + assert.NoError(t, err) + assert.Equal(t, float64(1), v) + }) } diff --git a/openapi3/refs_test.tmpl b/openapi3/refs_test.tmpl new file mode 100644 index 000000000..634fccf6f --- /dev/null +++ b/openapi3/refs_test.tmpl @@ -0,0 +1,54 @@ +// Code generated by go generate; DO NOT EDIT. +package {{ .Package }} + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) +{{ range $type := .Types }} +func Test{{ $type.Name }}Ref_Extensions(t *testing.T) { + data := []byte(`{"$ref":"#/components/schemas/Pet","something":"integer","x-order":1}`) + + ref := {{ $type.Name }}Ref{} + err := json.Unmarshal(data, &ref) + assert.NoError(t, err) + + // captures extension + assert.Equal(t, "#/components/schemas/Pet", ref.Ref) + assert.Equal(t, float64(1), ref.Extensions["x-order"]) + + // does not capture non-extensions + assert.Nil(t, ref.Extensions["something"]) + + // validation + err = ref.Validate(context.Background()) + require.EqualError(t, err, "extra sibling fields: [something]") + + err = ref.Validate(context.Background(), ProhibitExtensionsWithRef()) + require.EqualError(t, err, "extra sibling fields: [something x-order]") + + err = ref.Validate(context.Background(), AllowExtraSiblingFields("something")) + assert.ErrorContains(t, err, "found unresolved ref") // expected since value not defined + + // non-extension not json lookable + _, err = ref.JSONLookup("something") + assert.Error(t, err) +{{ if ne $type.Name "Header" }} + t.Run("extentions in value", func(t *testing.T) { + ref.Value = &{{ $type.Name }}{Extensions: map[string]any{}} + ref.Value.Extensions["x-order"] = 2.0 + + // prefers the value next to the \$ref over the one in the \$ref. + v, err := ref.JSONLookup("x-order") + assert.NoError(t, err) + assert.Equal(t, float64(1), v) + }) +{{ else }} + // Header does not have its own extensions. +{{ end -}} +} +{{ end -}} diff --git a/openapi3/refsgenerator.go b/openapi3/refsgenerator.go index 5bddfe258..65c3c88a6 100644 --- a/openapi3/refsgenerator.go +++ b/openapi3/refsgenerator.go @@ -13,8 +13,16 @@ import ( //go:embed refs.tmpl var tmplData string +//go:embed refs_test.tmpl +var tmplTestData string + func main() { - file, err := os.Create("refs.go") + generateTemplate("refs", tmplData) + generateTemplate("refs_test", tmplTestData) +} + +func generateTemplate(filename string, tmpl string) { + file, err := os.Create(filename + ".go") if err != nil { panic(err) } @@ -25,23 +33,28 @@ func main() { } }() - packageTemplate := template.Must(template.New("openapi3-refs").Parse(tmplData)) + packageTemplate := template.Must(template.New("openapi3-" + filename).Parse(tmpl)) + + type componentType struct { + Name string + CollectionName string + } if err := packageTemplate.Execute(file, struct { Package string - Types []string + Types []componentType }{ Package: os.Getenv("GOPACKAGE"), // set by the go:generate directive - Types: []string{ - "Callback", - "Example", - "Header", - "Link", - "Parameter", - "RequestBody", - "Response", - "Schema", - "SecurityScheme", + Types: []componentType{ + {Name: "Callback", CollectionName: "callbacks"}, + {Name: "Example", CollectionName: "examples"}, + {Name: "Header", CollectionName: "headers"}, + {Name: "Link", CollectionName: "links"}, + {Name: "Parameter", CollectionName: "parameters"}, + {Name: "RequestBody", CollectionName: "requestBodies"}, + {Name: "Response", CollectionName: "responses"}, + {Name: "Schema", CollectionName: "schemas"}, + {Name: "SecurityScheme", CollectionName: "securitySchemes"}, }, }); err != nil { panic(err) diff --git a/openapi3/request_body.go b/openapi3/request_body.go index 7b8d35399..6d4b8185e 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -9,7 +9,7 @@ import ( // RequestBody is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#request-body-object type RequestBody struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Description string `json:"description,omitempty" yaml:"description,omitempty"` Required bool `json:"required,omitempty" yaml:"required,omitempty"` @@ -83,8 +83,8 @@ func (requestBody RequestBody) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of RequestBody. -func (requestBody RequestBody) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 3+len(requestBody.Extensions)) +func (requestBody RequestBody) MarshalYAML() (any, error) { + m := make(map[string]any, 3+len(requestBody.Extensions)) for k, v := range requestBody.Extensions { m[k] = v } diff --git a/openapi3/response.go b/openapi3/response.go index 6209b5810..af8fda6f7 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -11,7 +11,7 @@ import ( // Responses is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responses-object type Responses struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` m map[string]*ResponseRef } @@ -101,7 +101,7 @@ func (responses *Responses) Validate(ctx context.Context, opts ...ValidationOpti // Response is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#response-object type Response struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Description *string `json:"description,omitempty" yaml:"description,omitempty"` Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"` @@ -143,8 +143,8 @@ func (response Response) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Response. -func (response Response) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 4+len(response.Extensions)) +func (response Response) MarshalYAML() (any, error) { + m := make(map[string]any, 4+len(response.Extensions)) for k, v := range response.Extensions { m[k] = v } diff --git a/openapi3/schema.go b/openapi3/schema.go index 8bacf729d..7be6bd38e 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -28,12 +28,6 @@ const ( TypeObject = "object" TypeString = "string" TypeNull = "null" - - // constants for integer formats - formatMinInt32 = float64(math.MinInt32) - formatMaxInt32 = float64(math.MaxInt32) - formatMinInt64 = float64(math.MinInt64) - formatMaxInt64 = float64(math.MaxInt64) ) var ( @@ -66,7 +60,7 @@ type SchemaRefs []*SchemaRef var _ jsonpointer.JSONPointable = (*SchemaRefs)(nil) // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { +func (s SchemaRefs) JSONLookup(token string) (any, error) { i, err := strconv.ParseUint(token, 10, 64) if err != nil { return nil, err @@ -87,7 +81,7 @@ func (s SchemaRefs) JSONLookup(token string) (interface{}, error) { // Schema is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object type Schema struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` OneOf SchemaRefs `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` @@ -97,9 +91,9 @@ type Schema struct { Title string `json:"title,omitempty" yaml:"title,omitempty"` Format string `json:"format,omitempty" yaml:"format,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` - Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` - Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` - Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` + Enum []any `json:"enum,omitempty" yaml:"enum,omitempty"` + Default any `json:"default,omitempty" yaml:"default,omitempty"` + Example any `json:"example,omitempty" yaml:"example,omitempty"` ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` // Array-related, here for struct compactness @@ -180,7 +174,7 @@ func (pTypes *Types) MarshalJSON() ([]byte, error) { return json.Marshal(x) } -func (pTypes *Types) MarshalYAML() (interface{}, error) { +func (pTypes *Types) MarshalYAML() (any, error) { if pTypes == nil { return nil, nil } @@ -214,7 +208,7 @@ type AdditionalProperties struct { } // MarshalYAML returns the YAML encoding of AdditionalProperties. -func (addProps AdditionalProperties) MarshalYAML() (interface{}, error) { +func (addProps AdditionalProperties) MarshalYAML() (any, error) { if x := addProps.Has; x != nil { if *x { return true, nil @@ -238,7 +232,7 @@ func (addProps AdditionalProperties) MarshalJSON() ([]byte, error) { // UnmarshalJSON sets AdditionalProperties to a copy of data. func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error { - var x interface{} + var x any if err := json.Unmarshal(data, &x); err != nil { return unmarshalError(err) } @@ -246,7 +240,7 @@ func (addProps *AdditionalProperties) UnmarshalJSON(data []byte) error { case nil: case bool: addProps.Has = &y - case map[string]interface{}: + case map[string]any: if len(y) == 0 { addProps.Schema = &SchemaRef{Value: &Schema{}} } else { @@ -279,8 +273,8 @@ func (schema Schema) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Schema. -func (schema Schema) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 36+len(schema.Extensions)) +func (schema Schema) MarshalYAML() (any, error) { + m := make(map[string]any, 36+len(schema.Extensions)) for k, v := range schema.Extensions { m[k] = v } @@ -483,7 +477,7 @@ func (schema *Schema) UnmarshalJSON(data []byte) error { } // JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (schema Schema) JSONLookup(token string) (interface{}, error) { +func (schema Schema) JSONLookup(token string) (any, error) { switch token { case "additionalProperties": if addProps := schema.AdditionalProperties.Has; addProps != nil { @@ -714,12 +708,12 @@ func (schema *Schema) WithExclusiveMax(value bool) *Schema { return schema } -func (schema *Schema) WithEnum(values ...interface{}) *Schema { +func (schema *Schema) WithEnum(values ...any) *Schema { schema.Enum = values return schema } -func (schema *Schema) WithDefault(defaultValue interface{}) *Schema { +func (schema *Schema) WithDefault(defaultValue any) *Schema { schema.Default = defaultValue return schema } @@ -988,7 +982,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, switch format { case "float", "double": default: - if validationOpts.schemaFormatValidationEnabled { + if _, ok := SchemaNumberFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { return stack, unsupportedFormat(format) } } @@ -998,7 +992,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, switch format { case "int32", "int64": default: - if validationOpts.schemaFormatValidationEnabled { + if _, ok := SchemaIntegerFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { return stack, unsupportedFormat(format) } } @@ -1019,7 +1013,6 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, // Defined in some other specification case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": default: - // Try to check for custom defined formats if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { return stack, unsupportedFormat(format) } @@ -1106,7 +1099,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, return stack, validateExtensions(ctx, schema.Extensions) } -func (schema *Schema) IsMatching(value interface{}) bool { +func (schema *Schema) IsMatching(value any) bool { settings := newSchemaValidationSettings(FailFast()) return schema.visitJSON(settings, value) == nil } @@ -1126,22 +1119,22 @@ func (schema *Schema) IsMatchingJSONString(value string) bool { return schema.visitJSON(settings, value) == nil } -func (schema *Schema) IsMatchingJSONArray(value []interface{}) bool { +func (schema *Schema) IsMatchingJSONArray(value []any) bool { settings := newSchemaValidationSettings(FailFast()) return schema.visitJSON(settings, value) == nil } -func (schema *Schema) IsMatchingJSONObject(value map[string]interface{}) bool { +func (schema *Schema) IsMatchingJSONObject(value map[string]any) bool { settings := newSchemaValidationSettings(FailFast()) return schema.visitJSON(settings, value) == nil } -func (schema *Schema) VisitJSON(value interface{}, opts ...SchemaValidationOption) error { +func (schema *Schema) VisitJSON(value any, opts ...SchemaValidationOption) error { settings := newSchemaValidationSettings(opts...) return schema.visitJSON(settings, value) } -func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interface{}) (err error) { +func (schema *Schema) visitJSON(settings *schemaValidationSettings, value any) (err error) { switch value := value.(type) { case nil: // Don't use VisitJSONNull, as we still want to reach 'visitXOFOperations', since @@ -1206,12 +1199,12 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf return schema.visitJSONNumber(settings, value) case string: return schema.visitJSONString(settings, value) - case []interface{}: + case []any: return schema.visitJSONArray(settings, value) - case map[string]interface{}: + case map[string]any: return schema.visitJSONObject(settings, value) - case map[interface{}]interface{}: // for YAML cf. issue #444 - values := make(map[string]interface{}, len(value)) + case map[any]any: // for YAML cf. issue https://github.com/getkin/kin-openapi/issues/444 + values := make(map[string]any, len(value)) for key, v := range value { if k, ok := key.(string); ok { values[k] = v @@ -1225,7 +1218,7 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf // Catch slice of non-empty interface type if reflect.TypeOf(value).Kind() == reflect.Slice { valueR := reflect.ValueOf(value) - newValue := make([]interface{}, 0, valueR.Len()) + newValue := make([]any, 0, valueR.Len()) for i := 0; i < valueR.Len(); i++ { newValue = append(newValue, valueR.Index(i).Interface()) } @@ -1241,7 +1234,7 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf } } -func (schema *Schema) visitEnumOperation(settings *schemaValidationSettings, value interface{}) (err error) { +func (schema *Schema) visitEnumOperation(settings *schemaValidationSettings, value any) (err error) { if enum := schema.Enum; len(enum) != 0 { for _, v := range enum { switch c := value.(type) { @@ -1278,7 +1271,7 @@ func (schema *Schema) visitEnumOperation(settings *schemaValidationSettings, val return } -func (schema *Schema) visitNotOperation(settings *schemaValidationSettings, value interface{}) (err error) { +func (schema *Schema) visitNotOperation(settings *schemaValidationSettings, value any) (err error) { if ref := schema.Not; ref != nil { v := ref.Value if v == nil { @@ -1301,13 +1294,13 @@ func (schema *Schema) visitNotOperation(settings *schemaValidationSettings, valu // If the XOF operations pass successfully, abort further run of validation, as they will already be satisfied (unless the schema // itself is badly specified -func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, value interface{}) (err error, run bool) { +func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, value any) (err error, run bool) { var visitedOneOf, visitedAnyOf, visitedAllOf bool if v := schema.OneOf; len(v) > 0 { var discriminatorRef string if schema.Discriminator != nil { pn := schema.Discriminator.PropertyName - if valuemap, okcheck := value.(map[string]interface{}); okcheck { + if valuemap, okcheck := value.(map[string]any); okcheck { discriminatorVal, okcheck := valuemap[pn] if !okcheck { return &SchemaError{ @@ -1523,39 +1516,56 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value } // formats - if requireInteger && schema.Format != "" { - formatMin := float64(0) - formatMax := float64(0) - switch schema.Format { - case "int32": - formatMin = formatMinInt32 - formatMax = formatMaxInt32 - case "int64": - formatMin = formatMinInt64 - formatMax = formatMaxInt64 - default: - if settings.formatValidationEnabled { - return unsupportedFormat(schema.Format) - } - } - if formatMin != 0 && formatMax != 0 && !(formatMin <= value && value <= formatMax) { - if settings.failfast { - return errSchema - } - err := &SchemaError{ - Value: value, - Schema: schema, - SchemaField: "format", - Reason: fmt.Sprintf("number must be an %s", schema.Format), - customizeMessageError: settings.customizeMessageError, + var formatStrErr string + var formatErr error + format := schema.Format + if format != "" { + if requireInteger { + if f, ok := SchemaIntegerFormats[format]; ok { + if err := f.Validate(int64(value)); err != nil { + var reason string + schemaErr := &SchemaError{} + if errors.As(err, &schemaErr) { + reason = schemaErr.Reason + } else { + reason = err.Error() + } + formatStrErr = fmt.Sprintf(`integer doesn't match the format %q (%v)`, format, reason) + formatErr = fmt.Errorf("integer doesn't match the format %q: %w", format, err) + } } - if !settings.multiError { - return err + } else { + if f, ok := SchemaNumberFormats[format]; ok { + if err := f.Validate(value); err != nil { + var reason string + schemaErr := &SchemaError{} + if errors.As(err, &schemaErr) { + reason = schemaErr.Reason + } else { + reason = err.Error() + } + formatStrErr = fmt.Sprintf(`number doesn't match the format %q (%v)`, format, reason) + formatErr = fmt.Errorf("number doesn't match the format %q: %w", format, err) + } } - me = append(me, err) } } + if formatStrErr != "" || formatErr != nil { + err := &SchemaError{ + Value: value, + Schema: schema, + SchemaField: "format", + Reason: formatStrErr, + Origin: formatErr, + customizeMessageError: settings.customizeMessageError, + } + if !settings.multiError { + return err + } + me = append(me, err) + } + // "exclusiveMinimum" if v := schema.ExclusiveMin; v && !(*schema.Min < value) { if settings.failfast { @@ -1749,23 +1759,16 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value var formatErr error if format := schema.Format; format != "" { if f, ok := SchemaStringFormats[format]; ok { - switch { - case f.regexp != nil && f.callback == nil: - if cp := f.regexp; !cp.MatchString(value) { - formatStrErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String()) - } - case f.regexp == nil && f.callback != nil: - if err := f.callback(value); err != nil { - schemaErr := &SchemaError{} - if errors.As(err, &schemaErr) { - formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%s)`, format, schemaErr.Reason) - } else { - formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%v)`, format, err) - } - formatErr = err + if err := f.Validate(value); err != nil { + var reason string + schemaErr := &SchemaError{} + if errors.As(err, &schemaErr) { + reason = schemaErr.Reason + } else { + reason = err.Error() } - default: - formatStrErr = fmt.Sprintf("corrupted entry %q in SchemaStringFormats", format) + formatStrErr = fmt.Sprintf(`string doesn't match the format %q (%v)`, format, reason) + formatErr = fmt.Errorf("string doesn't match the format %q: %w", format, err) } } } @@ -1792,12 +1795,12 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value return nil } -func (schema *Schema) VisitJSONArray(value []interface{}) error { +func (schema *Schema) VisitJSONArray(value []any) error { settings := newSchemaValidationSettings() return schema.visitJSONArray(settings, value) } -func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error { +func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []any) error { if !schema.Type.Permits(TypeArray) { return schema.expectedType(settings, value) } @@ -1891,12 +1894,12 @@ func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value [ return nil } -func (schema *Schema) VisitJSONObject(value map[string]interface{}) error { +func (schema *Schema) VisitJSONObject(value map[string]any) error { settings := newSchemaValidationSettings() return schema.visitJSONObject(settings, value) } -func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error { +func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]any) error { if !schema.Type.Permits(TypeObject) { return schema.expectedType(settings, value) } @@ -2075,7 +2078,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value return nil } -func (schema *Schema) expectedType(settings *schemaValidationSettings, value interface{}) error { +func (schema *Schema) expectedType(settings *schemaValidationSettings, value any) error { if settings.failfast { return errSchema } @@ -2105,7 +2108,7 @@ func (schema *Schema) expectedType(settings *schemaValidationSettings, value int // SchemaError is an error that occurs during schema validation. type SchemaError struct { // Value is the value that failed validation. - Value interface{} + Value any // reversePath is the path to the value that failed validation. reversePath []string // Schema is the schema that failed validation. @@ -2212,7 +2215,7 @@ func (err SchemaError) Unwrap() error { return err.Origin } -func isSliceOfUniqueItems(xs []interface{}) bool { +func isSliceOfUniqueItems(xs []any) bool { s := len(xs) m := make(map[string]struct{}, s) for _, x := range xs { @@ -2226,7 +2229,7 @@ func isSliceOfUniqueItems(xs []interface{}) bool { // SliceUniqueItemsChecker is an function used to check if an given slice // have unique items. -type SliceUniqueItemsChecker func(items []interface{}) bool +type SliceUniqueItemsChecker func(items []any) bool // By default using predefined func isSliceOfUniqueItems which make use of // json.Marshal to generate a key for map used to check if a given slice diff --git a/openapi3/schema_formats.go b/openapi3/schema_formats.go index ea38400c2..023c2669e 100644 --- a/openapi3/schema_formats.go +++ b/openapi3/schema_formats.go @@ -2,9 +2,31 @@ package openapi3 import ( "fmt" - "net" + "math" + "net/netip" "regexp" - "strings" +) + +type ( + // FormatValidator is an interface for custom format validators. + FormatValidator[T any] interface { + Validate(value T) error + } + // StringFormatValidator is a type alias for FormatValidator[string] + StringFormatValidator = FormatValidator[string] + // NumberFormatValidator is a type alias for FormatValidator[float64] + NumberFormatValidator = FormatValidator[float64] + // IntegerFormatValidator is a type alias for FormatValidator[int64] + IntegerFormatValidator = FormatValidator[int64] +) + +var ( + // SchemaStringFormats is a map of custom string format validators. + SchemaStringFormats = make(map[string]StringFormatValidator) + // SchemaNumberFormats is a map of custom number format validators. + SchemaNumberFormats = make(map[string]NumberFormatValidator) + // SchemaIntegerFormats is a map of custom integer format validators. + SchemaIntegerFormats = make(map[string]IntegerFormatValidator) ) const ( @@ -14,93 +36,134 @@ const ( // FormatOfStringForEmail pattern catches only some suspiciously wrong-looking email addresses. // Use DefineStringFormat(...) if you need something stricter. FormatOfStringForEmail = `^[^@]+@[^@<>",\s]+$` -) -// FormatCallback performs custom checks on exotic formats -type FormatCallback func(value string) error + // FormatOfStringByte is a regexp for base64-encoded characters, for example, "U3dhZ2dlciByb2Nrcw==" + 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)$` -// Format represents a format validator registered by either DefineStringFormat or DefineStringFormatCallback -type Format struct { - regexp *regexp.Regexp - callback FormatCallback + // 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})?$` +) + +func init() { + DefineStringFormatValidator("byte", NewRegexpFormatValidator(FormatOfStringByte)) + DefineStringFormatValidator("date", NewRegexpFormatValidator(FormatOfStringDate)) + DefineStringFormatValidator("date-time", NewRegexpFormatValidator(FormatOfStringDateTime)) + DefineIntegerFormatValidator("int32", NewRangeFormatValidator(int64(math.MinInt32), int64(math.MaxInt32))) + DefineIntegerFormatValidator("int64", NewRangeFormatValidator(int64(math.MinInt64), int64(math.MaxInt64))) } -// SchemaStringFormats allows for validating string formats -var SchemaStringFormats = make(map[string]Format, 4) +// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec +func DefineIPv4Format() { + DefineStringFormatValidator("ipv4", NewIPValidator(true)) +} -// DefineStringFormat defines a new regexp pattern for a given format -func DefineStringFormat(name string, pattern string) { - re, err := regexp.Compile(pattern) - if err != nil { - err := fmt.Errorf("format %q has invalid pattern %q: %w", name, pattern, err) - panic(err) - } - SchemaStringFormats[name] = Format{regexp: re} +// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec +func DefineIPv6Format() { + DefineStringFormatValidator("ipv6", NewIPValidator(false)) } -// DefineStringFormatCallback adds a validation function for a specific schema format entry -func DefineStringFormatCallback(name string, callback FormatCallback) { - SchemaStringFormats[name] = Format{callback: callback} +type stringRegexpFormatValidator struct { + re *regexp.Regexp } -func validateIP(ip string) error { - parsed := net.ParseIP(ip) - if parsed == nil { - return &SchemaError{ - Value: ip, - Reason: "Not an IP address", - } +func (s stringRegexpFormatValidator) Validate(value string) error { + if !s.re.MatchString(value) { + return fmt.Errorf(`string doesn't match pattern "%s"`, s.re.String()) } return nil } -func validateIPv4(ip string) error { - if err := validateIP(ip); err != nil { - return err - } +type callbackValidator[T any] struct { + fn func(T) error +} - if !(strings.Count(ip, ":") < 2) { - return &SchemaError{ - Value: ip, - Reason: "Not an IPv4 address (it's IPv6)", - } +func (c callbackValidator[T]) Validate(value T) error { + return c.fn(value) +} + +type rangeFormat[T int64 | float64] struct { + min, max T +} + +func (r rangeFormat[T]) Validate(value T) error { + if value < r.min || value > r.max { + return fmt.Errorf("value should be between %v and %v", r.min, r.max) } return nil } -func validateIPv6(ip string) error { - if err := validateIP(ip); err != nil { - return err - } +// NewRangeFormatValidator creates a new FormatValidator that validates the value is within a given range. +func NewRangeFormatValidator[T int64 | float64](min, max T) FormatValidator[T] { + return rangeFormat[T]{min: min, max: max} +} - if !(strings.Count(ip, ":") >= 2) { - return &SchemaError{ - Value: ip, - Reason: "Not an IPv6 address (it's IPv4)", - } +// NewRegexpFormatValidator creates a new FormatValidator that uses a regular expression to validate the value. +func NewRegexpFormatValidator(pattern string) StringFormatValidator { + re, err := regexp.Compile(pattern) + if err != nil { + err := fmt.Errorf("string regexp format has invalid pattern %q: %w", pattern, err) + panic(err) } - return nil + return stringRegexpFormatValidator{re: re} } -func init() { - // Base64 - // The pattern supports base64 and b./ase64url. Padding ('=') is supported. - DefineStringFormat("byte", `(^$|^[a-zA-Z0-9+/\-_]*=*$)`) +// NewCallbackValidator creates a new FormatValidator that uses a callback function to validate the value. +func NewCallbackValidator[T any](fn func(T) error) FormatValidator[T] { + return callbackValidator[T]{fn: fn} +} - // date - DefineStringFormat("date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`) +// DefineStringFormatValidator defines a custom format validator for a given string format. +func DefineStringFormatValidator(name string, validator StringFormatValidator) { + SchemaStringFormats[name] = validator +} - // date-time - DefineStringFormat("date-time", `^[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})?$`) +// DefineNumberFormatValidator defines a custom format validator for a given number format. +func DefineNumberFormatValidator(name string, validator NumberFormatValidator) { + SchemaNumberFormats[name] = validator +} +// DefineIntegerFormatValidator defines a custom format validator for a given integer format. +func DefineIntegerFormatValidator(name string, validator IntegerFormatValidator) { + SchemaIntegerFormats[name] = validator } -// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec -func DefineIPv4Format() { - DefineStringFormatCallback("ipv4", validateIPv4) +// 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)) } -// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec -func DefineIPv6Format() { - DefineStringFormatCallback("ipv6", validateIPv6) +// 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)) +} + +// NewIPValidator creates a new FormatValidator that validates the value is an IP address. +func NewIPValidator(isIPv4 bool) FormatValidator[string] { + return callbackValidator[string]{fn: func(ip string) error { + addr, err := netip.ParseAddr(ip) + if err != nil { + return &SchemaError{ + Value: ip, + Reason: "Not an IP address", + } + } + if isIPv4 && !addr.Is4() { + return &SchemaError{ + Value: ip, + Reason: "Not an IPv4 address (it's IPv6)", + } + } + if !isIPv4 && !addr.Is6() { + return &SchemaError{ + Value: ip, + Reason: "Not an IPv6 address (it's IPv4)", + } + } + return nil + }} } diff --git a/openapi3/schema_formats_test.go b/openapi3/schema_formats_test.go index 70092d6de..f7c6a08cb 100644 --- a/openapi3/schema_formats_test.go +++ b/openapi3/schema_formats_test.go @@ -39,6 +39,7 @@ func TestIssue430(t *testing.T) { "::FFFF:192.168.0.1": false, // "[::FFFF:C0A8:1]:80" doesn't parse per net.ParseIP() // "[::FFFF:C0A8:1%1]:80" doesn't parse per net.ParseIP() + "2001:db8::": false, } for datum := range data { @@ -49,15 +50,18 @@ func TestIssue430(t *testing.T) { DefineIPv4Format() DefineIPv6Format() + ipv4Validator := NewIPValidator(true) + ipv6Validator := NewIPValidator(false) + for datum, isV4 := range data { err = schema.VisitJSON(datum) require.NoError(t, err) if isV4 { - require.Nil(t, validateIPv4(datum), "%q should be IPv4", datum) - require.NotNil(t, validateIPv6(datum), "%q should not be IPv6", datum) + assert.Nil(t, ipv4Validator.Validate(datum), "%q should be IPv4", datum) + assert.NotNil(t, ipv6Validator.Validate(datum), "%q should not be IPv6", datum) } else { - require.NotNil(t, validateIPv4(datum), "%q should not be IPv4", datum) - require.Nil(t, validateIPv6(datum), "%q should be IPv6", datum) + assert.NotNil(t, ipv4Validator.Validate(datum), "%q should not be IPv4", datum) + assert.Nil(t, ipv6Validator.Validate(datum), "%q should be IPv6", datum) } } } @@ -65,9 +69,9 @@ func TestIssue430(t *testing.T) { func TestFormatCallback_WrapError(t *testing.T) { var errSomething = errors.New("something error") - DefineStringFormatCallback("foobar", func(value string) error { + DefineStringFormatValidator("foobar", NewCallbackValidator(func(value string) error { return errSomething - }) + })) s := &Schema{Format: "foobar"} err := s.VisitJSONString("blablabla") @@ -97,11 +101,11 @@ components: doc, err := l.LoadFromData([]byte(spc)) require.NoError(t, err) - err = doc.Components.Schemas["Something"].Value.VisitJSON(map[string]interface{}{ + err = doc.Components.Schemas["Something"].Value.VisitJSON(map[string]any{ `ip`: `123.0.0.11111`, }) - require.EqualError(t, err, `Error at "/ip": Not an IP address`) + require.EqualError(t, err, `Error at "/ip": string doesn't match the format "ipv4": Not an IP address`) delete(SchemaStringFormats, "ipv4") SchemaErrorDetailsDisabled = false @@ -115,7 +119,7 @@ func TestUuidFormat(t *testing.T) { wantErr bool } - DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) + DefineStringFormatValidator("uuid", NewRegexpFormatValidator(FormatOfStringForUUIDOfRFC4122)) testCases := []testCase{ { name: "invalid", @@ -154,3 +158,81 @@ func TestUuidFormat(t *testing.T) { }) } } + +func TestNumberFormats(t *testing.T) { + type testCase struct { + name string + typ string + format string + value any + wantErr bool + } + DefineNumberFormatValidator("lessThan10", NewCallbackValidator(func(value float64) error { + if value >= 10 { + return errors.New("not less than 10") + } + return nil + })) + DefineIntegerFormatValidator("odd", NewCallbackValidator(func(value int64) error { + if value%2 == 0 { + return errors.New("not odd") + } + return nil + })) + testCases := []testCase{ + { + name: "invalid number", + value: "test", + typ: "number", + format: "", + wantErr: true, + }, + { + name: "zero float64", + value: 0.0, + typ: "number", + format: "lessThan10", + wantErr: false, + }, + { + name: "11", + value: 11.0, + typ: "number", + format: "lessThan10", + wantErr: true, + }, + { + name: "odd 11", + value: 11.0, + typ: "integer", + format: "odd", + wantErr: false, + }, + { + name: "even 12", + value: 12.0, + typ: "integer", + format: "odd", + wantErr: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + schema := &Schema{ + Type: &Types{tc.typ}, + Format: tc.format, + } + err := schema.VisitJSON(tc.value) + var schemaError = &SchemaError{} + if tc.wantErr { + require.Error(t, err) + require.ErrorAs(t, err, &schemaError) + + require.NotZero(t, schemaError.Reason) + require.NotContains(t, schemaError.Reason, fmt.Sprint(tc.value)) + } else { + require.Nil(t, err) + } + }) + } +} diff --git a/openapi3/schema_issue289_test.go b/openapi3/schema_issue289_test.go index 95d50dc21..033371900 100644 --- a/openapi3/schema_issue289_test.go +++ b/openapi3/schema_issue289_test.go @@ -41,7 +41,7 @@ paths: {} err = doc.Validate(loader.Context) require.NoError(t, err) - err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{ + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ "name": "kin-openapi", "address": "127.0.0.1", }) diff --git a/openapi3/schema_issue492_test.go b/openapi3/schema_issue492_test.go index 4ad72abc9..2fdfcbcec 100644 --- a/openapi3/schema_issue492_test.go +++ b/openapi3/schema_issue492_test.go @@ -35,16 +35,16 @@ info: require.NoError(t, err) // verify that the expected format works - err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{ + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ "name": "kin-openapi", "time": "2001-02-03T04:05:06.789Z", }) require.NoError(t, err) // verify that the issue is fixed - err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]interface{}{ + err = doc.Components.Schemas["Server"].Value.VisitJSON(map[string]any{ "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" (regular expression "^[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.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})?$"`) } diff --git a/openapi3/schema_issue940_test.go b/openapi3/schema_issue940_test.go index 95d3d7869..792199489 100644 --- a/openapi3/schema_issue940_test.go +++ b/openapi3/schema_issue940_test.go @@ -46,7 +46,7 @@ func TestOneOfErrorPreserved(t *testing.T) { err = s.Validate(context.Background()) require.NoError(t, err) - obj := make(map[string]interface{}) + obj := make(map[string]any) err = json.Unmarshal([]byte(raw), &obj) require.NoError(t, err) diff --git a/openapi3/schema_oneOf_test.go b/openapi3/schema_oneOf_test.go index f6bd50861..336c2843c 100644 --- a/openapi3/schema_oneOf_test.go +++ b/openapi3/schema_oneOf_test.go @@ -119,7 +119,7 @@ components: func TestVisitJSON_OneOf_MissingDescriptorProperty(t *testing.T) { doc := oneofSpec(t) - err := doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + err := doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]any{ "name": "snoopy", }) require.ErrorContains(t, err, `input does not contain the discriminator property "$type"`) @@ -127,7 +127,7 @@ func TestVisitJSON_OneOf_MissingDescriptorProperty(t *testing.T) { func TestVisitJSON_OneOf_MissingDescriptorValue(t *testing.T) { doc := oneofSpec(t) - err := doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + err := doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]any{ "name": "snoopy", "$type": "snake", }) @@ -136,7 +136,7 @@ func TestVisitJSON_OneOf_MissingDescriptorValue(t *testing.T) { func TestVisitJSON_OneOf_MissingField(t *testing.T) { doc := oneofSpec(t) - err := doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + err := doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]any{ "name": "snoopy", "$type": "dog", }) @@ -145,7 +145,7 @@ func TestVisitJSON_OneOf_MissingField(t *testing.T) { func TestVisitJSON_OneOf_NoDescriptor_MissingField(t *testing.T) { doc := oneofNoDiscriminatorSpec(t) - err := doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + err := doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]any{ "name": "snoopy", }) require.ErrorContains(t, err, `doesn't match schema due to: Error at "/scratches": property "scratches" is missing`) @@ -153,14 +153,14 @@ func TestVisitJSON_OneOf_NoDescriptor_MissingField(t *testing.T) { func TestVisitJSON_OneOf_BadDiscriminatorType(t *testing.T) { doc := oneofSpec(t) - err := doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + err := doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]any{ "name": "snoopy", "scratches": true, "$type": 1, }) require.ErrorContains(t, err, `value of discriminator property "$type" is not a string`) - err = doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]interface{}{ + err = doc.Components.Schemas["Animal"].Value.VisitJSON(map[string]any{ "name": "snoopy", "barks": true, "$type": nil, @@ -205,9 +205,9 @@ components: err = doc.Validate(loader.Context) require.NoError(t, err) - err = doc.Components.Schemas["Something"].Value.VisitJSON(map[string]interface{}{ - "first": map[string]interface{}{ - "second": map[string]interface{}{ + err = doc.Components.Schemas["Something"].Value.VisitJSON(map[string]any{ + "first": map[string]any{ + "second": map[string]any{ "third": "123456789", }, }, diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index b9f2dbf55..d678361bd 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -16,19 +16,19 @@ import ( type schemaExample struct { Title string Schema *Schema - Serialization interface{} - AllValid []interface{} - AllInvalid []interface{} + Serialization any + AllValid []any + AllInvalid []any } func TestSchemas(t *testing.T) { - DefineStringFormat("uuid", FormatOfStringForUUIDOfRFC4122) + DefineStringFormatValidator("uuid", NewRegexpFormatValidator(FormatOfStringForUUIDOfRFC4122)) for _, example := range schemaExamples { - t.Run(example.Title, testSchema(t, example)) + t.Run(example.Title, testSchema(example)) } } -func testSchema(t *testing.T, example schemaExample) func(*testing.T) { +func testSchema(example schemaExample) func(*testing.T) { return func(t *testing.T) { schema := example.Schema if serialized := example.Serialization; serialized != nil { @@ -56,32 +56,32 @@ func testSchema(t *testing.T, example schemaExample) func(*testing.T) { } } // NaN and Inf aren't valid JSON but are handled - for index, value := range []interface{}{math.NaN(), math.Inf(-1), math.Inf(+1)} { + for index, value := range []any{math.NaN(), math.Inf(-1), math.Inf(+1)} { err := schema.VisitJSON(value) require.Errorf(t, err, "NaNAndInf #%d: %#v", index, value) } } } -func validateSchemaJSON(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error { +func validateSchemaJSON(t *testing.T, schema *Schema, value any, opts ...SchemaValidationOption) error { data, err := json.Marshal(value) require.NoError(t, err) - var val interface{} + var val any err = json.Unmarshal(data, &val) require.NoError(t, err) return schema.VisitJSON(val, opts...) } -func validateSchemaYAML(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error { +func validateSchemaYAML(t *testing.T, schema *Schema, value any, opts ...SchemaValidationOption) error { data, err := yaml.Marshal(value) require.NoError(t, err) - var val interface{} + var val any err = yaml.Unmarshal(data, &val) require.NoError(t, err) return schema.VisitJSON(val, opts...) } -type validateSchemaFunc func(t *testing.T, schema *Schema, value interface{}, opts ...SchemaValidationOption) error +type validateSchemaFunc func(t *testing.T, schema *Schema, value any, opts ...SchemaValidationOption) error var validateSchemaFuncs = []validateSchemaFunc{ validateSchemaJSON, @@ -92,19 +92,16 @@ var schemaExamples = []schemaExample{ { Title: "EMPTY SCHEMA", Schema: &Schema{}, - Serialization: map[string]interface{}{ - // This OA3 schema is exactly this draft-04 schema: - // {"not": {"type": "null"}} - }, - AllValid: []interface{}{ + Serialization: map[string]any{}, + AllValid: []any{ false, true, 3.14, "", - []interface{}{}, - map[string]interface{}{}, + []any{}, + map[string]any{}, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, }, }, @@ -112,13 +109,13 @@ var schemaExamples = []schemaExample{ { Title: "JUST NULLABLE", Schema: NewSchema().WithNullable(), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ // This OA3 schema is exactly both this draft-04 schema: {} and: // {anyOf: [type:string, type:number, type:integer, type:boolean // ,{type:array, items:{}}, type:object]} "nullable": true, }, - AllValid: []interface{}{ + AllValid: []any{ nil, false, true, @@ -126,30 +123,30 @@ var schemaExamples = []schemaExample{ 0.0, 3.14, "", - []interface{}{}, - map[string]interface{}{}, + []any{}, + map[string]any{}, }, }, { Title: "NULLABLE BOOLEAN", Schema: NewBoolSchema().WithNullable(), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "nullable": true, "type": "boolean", }, - AllValid: []interface{}{ + AllValid: []any{ nil, false, true, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ 0, 0.0, 3.14, "", - []interface{}{}, - map[string]interface{}{}, + []any{}, + map[string]any{}, }, }, @@ -158,13 +155,13 @@ var schemaExamples = []schemaExample{ Schema: &Schema{ Type: &Types{TypeString, TypeBoolean}, }, - AllValid: []interface{}{ + AllValid: []any{ "", "xyz", true, false, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ 1, nil, }, @@ -175,15 +172,15 @@ var schemaExamples = []schemaExample{ Schema: &Schema{ Type: &Types{TypeNumber, TypeNull}, }, - AllValid: []interface{}{ + AllValid: []any{ 0, 1, 2.3, nil, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ "x", - []interface{}{}, + []any{}, }, }, @@ -193,23 +190,23 @@ var schemaExamples = []schemaExample{ NewIntegerSchema(), NewFloat64Schema(), ).WithNullable(), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "nullable": true, - "anyOf": []interface{}{ - map[string]interface{}{"type": "integer"}, - map[string]interface{}{"type": "number"}, + "anyOf": []any{ + map[string]any{"type": "integer"}, + map[string]any{"type": "number"}, }, }, - AllValid: []interface{}{ + AllValid: []any{ nil, 42, 4.2, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ true, - []interface{}{42}, + []any{42}, "bla", - map[string]interface{}{}, + map[string]any{}, }, }, @@ -219,22 +216,22 @@ var schemaExamples = []schemaExample{ NewIntegerSchema().WithNullable(), NewFloat64Schema(), ), - Serialization: map[string]interface{}{ - "anyOf": []interface{}{ - map[string]interface{}{"type": "integer", "nullable": true}, - map[string]interface{}{"type": "number"}, + Serialization: map[string]any{ + "anyOf": []any{ + map[string]any{"type": "integer", "nullable": true}, + map[string]any{"type": "number"}, }, }, - AllValid: []interface{}{ + AllValid: []any{ nil, 42, 4.2, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ true, - []interface{}{42}, + []any{42}, "bla", - map[string]interface{}{}, + map[string]any{}, }, }, @@ -244,23 +241,23 @@ var schemaExamples = []schemaExample{ NewBoolSchema().WithNullable(), NewBoolSchema().WithNullable(), ), - Serialization: map[string]interface{}{ - "allOf": []interface{}{ - map[string]interface{}{"type": "boolean", "nullable": true}, - map[string]interface{}{"type": "boolean", "nullable": true}, + Serialization: map[string]any{ + "allOf": []any{ + map[string]any{"type": "boolean", "nullable": true}, + map[string]any{"type": "boolean", "nullable": true}, }, }, - AllValid: []interface{}{ + AllValid: []any{ nil, true, false, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ 2, 4.2, - []interface{}{42}, + []any{42}, "bla", - map[string]interface{}{}, + map[string]any{}, }, }, @@ -273,46 +270,46 @@ var schemaExamples = []schemaExample{ "stringProp": NewStringSchema().WithMaxLength(18), "boolProp": NewBoolSchema(), }), - Serialization: map[string]interface{}{ - "properties": map[string]interface{}{ - "stringProp": map[string]interface{}{"type": "string", "maxLength": 18}, - "boolProp": map[string]interface{}{"type": "boolean"}, + Serialization: map[string]any{ + "properties": map[string]any{ + "stringProp": map[string]any{"type": "string", "maxLength": 18}, + "boolProp": map[string]any{"type": "boolean"}, }, - "anyOf": []interface{}{ - map[string]interface{}{"type": "object", "required": []string{"stringProp"}}, - map[string]interface{}{"type": "object", "required": []string{"boolProp"}}, + "anyOf": []any{ + map[string]any{"type": "object", "required": []string{"stringProp"}}, + map[string]any{"type": "object", "required": []string{"boolProp"}}, }, }, - AllValid: []interface{}{ - map[string]interface{}{"stringProp": "valid string value"}, - map[string]interface{}{"boolProp": true}, - map[string]interface{}{"stringProp": "valid string value", "boolProp": true}, + AllValid: []any{ + map[string]any{"stringProp": "valid string value"}, + map[string]any{"boolProp": true}, + map[string]any{"stringProp": "valid string value", "boolProp": true}, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ 1, - map[string]interface{}{}, - map[string]interface{}{"invalidProp": false}, - map[string]interface{}{"stringProp": "invalid string value"}, - map[string]interface{}{"stringProp": "invalid string value", "boolProp": true}, + map[string]any{}, + map[string]any{"invalidProp": false}, + map[string]any{"stringProp": "invalid string value"}, + map[string]any{"stringProp": "invalid string value", "boolProp": true}, }, }, { Title: "BOOLEAN", Schema: NewBoolSchema(), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "boolean", }, - AllValid: []interface{}{ + AllValid: []any{ false, true, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, 3.14, "", - []interface{}{}, - map[string]interface{}{}, + []any{}, + map[string]any{}, }, }, @@ -321,25 +318,25 @@ var schemaExamples = []schemaExample{ Schema: NewFloat64Schema(). WithMin(2.5). WithMax(3.5), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "number", "minimum": 2.5, "maximum": 3.5, }, - AllValid: []interface{}{ + AllValid: []any{ 2.5, 3.14, 3.5, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, false, true, 2.4, 3.6, "", - []interface{}{}, - map[string]interface{}{}, + []any{}, + map[string]any{}, }, }, @@ -348,17 +345,17 @@ var schemaExamples = []schemaExample{ Schema: NewInt64Schema(). WithMin(2). WithMax(5), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "integer", "format": "int64", "minimum": 2, "maximum": 5, }, - AllValid: []interface{}{ + AllValid: []any{ 2, 5, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, false, true, @@ -366,49 +363,49 @@ var schemaExamples = []schemaExample{ 6, 3.5, "", - []interface{}{}, - map[string]interface{}{}, + []any{}, + map[string]any{}, }, }, { Title: "INTEGER OPTIONAL INT64 FORMAT", Schema: NewInt64Schema(), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "integer", "format": "int64", }, - AllValid: []interface{}{ + AllValid: []any{ 1, 256, 65536, int64(math.MaxInt32) + 10, int64(math.MinInt32) - 10, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, false, 3.5, true, "", - []interface{}{}, - map[string]interface{}{}, + []any{}, + map[string]any{}, }, }, { Title: "INTEGER OPTIONAL INT32 FORMAT", Schema: NewInt32Schema(), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "integer", "format": "int32", }, - AllValid: []interface{}{ + AllValid: []any{ 1, 256, 65536, int64(math.MaxInt32), int64(math.MaxInt32), }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, false, 3.5, @@ -416,8 +413,8 @@ var schemaExamples = []schemaExample{ int64(math.MinInt32) - 10, true, "", - []interface{}{}, - map[string]interface{}{}, + []any{}, + map[string]any{}, }, }, { @@ -426,17 +423,17 @@ var schemaExamples = []schemaExample{ WithMinLength(2). WithMaxLength(3). WithPattern("^[abc]+$"), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "string", "minLength": 2, "maxLength": 3, "pattern": "^[abc]+$", }, - AllValid: []interface{}{ + AllValid: []any{ "ab", "abc", }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, false, true, @@ -444,19 +441,19 @@ var schemaExamples = []schemaExample{ "a", "xy", "aaaa", - []interface{}{}, - map[string]interface{}{}, + []any{}, + map[string]any{}, }, }, { Title: "STRING: optional format 'uuid'", Schema: NewUUIDSchema(), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "string", "format": "uuid", }, - AllValid: []interface{}{ + AllValid: []any{ "dd7d8481-81a3-407f-95f0-a2f1cb382a4b", "dcba3901-2fba-48c1-9db2-00422055804e", "ace8e3be-c254-4c10-8859-1401d9a9d52a", @@ -467,7 +464,7 @@ var schemaExamples = []schemaExample{ "DCBA3901-2FBA-48C1-9db2-00422055804e", "ACE8E3BE-c254-4C10-8859-1401D9A9D52A", }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, "g39840b1-d0ef-446d-e555-48fcca50a90a", "4cf3i040-ea14-4daa-b0b5-ea9329473519", @@ -486,11 +483,11 @@ var schemaExamples = []schemaExample{ { Title: "STRING: format 'date-time'", Schema: NewDateTimeSchema(), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "string", "format": "date-time", }, - AllValid: []interface{}{ + AllValid: []any{ "2017-12-31T11:59:59", "2017-12-31T11:59:59Z", "2017-12-31T11:59:59-11:30", @@ -498,7 +495,7 @@ var schemaExamples = []schemaExample{ "2017-12-31T11:59:59.999+11:30", "2017-12-31T11:59:59.999Z", }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, 3.14, "2017-12-31", @@ -511,11 +508,11 @@ var schemaExamples = []schemaExample{ { Title: "STRING: format 'date-time'", Schema: NewBytesSchema(), - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "string", "format": "byte", }, - AllValid: []interface{}{ + AllValid: []any{ "", base64.StdEncoding.EncodeToString(func() []byte { data := make([]byte, 0, 1024) @@ -532,7 +529,7 @@ var schemaExamples = []schemaExample{ return data }()), }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, " ", "\n\n", // a \n is ok for JSON but not for YAML decoder/encoder @@ -549,33 +546,33 @@ var schemaExamples = []schemaExample{ UniqueItems: true, Items: NewFloat64Schema().NewRef(), }, - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "array", "minItems": 2, "maxItems": 3, "uniqueItems": true, - "items": map[string]interface{}{ + "items": map[string]any{ "type": "number", }, }, - AllValid: []interface{}{ - []interface{}{ + AllValid: []any{ + []any{ 1, 2, }, - []interface{}{ + []any{ 1, 2, 3, }, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, 3.14, - []interface{}{ + []any{ 1, }, - []interface{}{ + []any{ 42, 42, }, - []interface{}{ + []any{ 1, 2, 3, 4, }, }, @@ -592,45 +589,45 @@ var schemaExamples = []schemaExample{ }, }).NewRef(), }, - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "array", "uniqueItems": true, - "items": map[string]interface{}{ - "properties": map[string]interface{}{ - "key1": map[string]interface{}{ + "items": map[string]any{ + "properties": map[string]any{ + "key1": map[string]any{ "type": "number", }, }, "type": "object", }, }, - AllValid: []interface{}{ - []interface{}{ - map[string]interface{}{ + AllValid: []any{ + []any{ + map[string]any{ "key1": 1, "key2": 1, // Additional properties will make object different // By default additionalProperties is true }, - map[string]interface{}{ + map[string]any{ "key1": 1, }, }, - []interface{}{ - map[string]interface{}{ + []any{ + map[string]any{ "key1": 1, }, - map[string]interface{}{ + map[string]any{ "key1": 2, }, }, }, - AllInvalid: []interface{}{ - []interface{}{ - map[string]interface{}{ + AllInvalid: []any{ + []any{ + map[string]any{ "key1": 1, }, - map[string]interface{}{ + map[string]any{ "key1": 1, }, }, @@ -653,15 +650,15 @@ var schemaExamples = []schemaExample{ }, }).NewRef(), }, - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "array", "uniqueItems": true, - "items": map[string]interface{}{ - "properties": map[string]interface{}{ - "key1": map[string]interface{}{ + "items": map[string]any{ + "properties": map[string]any{ + "key1": map[string]any{ "type": "array", "uniqueItems": true, - "items": map[string]interface{}{ + "items": map[string]any{ "type": "number", }, }, @@ -669,53 +666,53 @@ var schemaExamples = []schemaExample{ "type": "object", }, }, - AllValid: []interface{}{ - []interface{}{ - map[string]interface{}{ - "key1": []interface{}{ + AllValid: []any{ + []any{ + map[string]any{ + "key1": []any{ 1, 2, }, }, - map[string]interface{}{ - "key1": []interface{}{ + map[string]any{ + "key1": []any{ 3, 4, }, }, }, - []interface{}{ // Slice have items with the same value but with different index will treated as different slices - map[string]interface{}{ - "key1": []interface{}{ + []any{ + map[string]any{ + "key1": []any{ 10, 9, }, }, - map[string]interface{}{ - "key1": []interface{}{ + map[string]any{ + "key1": []any{ 9, 10, }, }, }, }, - AllInvalid: []interface{}{ - []interface{}{ // Violate outer array uniqueItems: true - map[string]interface{}{ - "key1": []interface{}{ + AllInvalid: []any{ + []any{ + map[string]any{ + "key1": []any{ 9, 9, }, }, - map[string]interface{}{ - "key1": []interface{}{ + map[string]any{ + "key1": []any{ 9, 9, }, }, }, - []interface{}{ // Violate inner(array in object) array uniqueItems: true - map[string]interface{}{ - "key1": []interface{}{ + []any{ + map[string]any{ + "key1": []any{ 9, 9, }, }, - map[string]interface{}{ - "key1": []interface{}{ + map[string]any{ + "key1": []any{ 8, 8, }, }, @@ -734,35 +731,35 @@ var schemaExamples = []schemaExample{ Items: NewFloat64Schema().NewRef(), }).NewRef(), }, - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "array", "uniqueItems": true, - "items": map[string]interface{}{ - "items": map[string]interface{}{ + "items": map[string]any{ + "items": map[string]any{ "type": "number", }, "uniqueItems": true, "type": "array", }, }, - AllValid: []interface{}{ - []interface{}{ - []interface{}{1, 2}, - []interface{}{3, 4}, + AllValid: []any{ + []any{ + []any{1, 2}, + []any{3, 4}, }, - []interface{}{ // Slice have items with the same value but with different index will treated as different slices - []interface{}{1, 2}, - []interface{}{2, 1}, + []any{ + []any{1, 2}, + []any{2, 1}, }, }, - AllInvalid: []interface{}{ - []interface{}{ // Violate outer array uniqueItems: true - []interface{}{8, 9}, - []interface{}{8, 9}, + AllInvalid: []any{ + []any{ + []any{8, 9}, + []any{8, 9}, }, - []interface{}{ // Violate inner array uniqueItems: true - []interface{}{9, 9}, - []interface{}{8, 8}, + []any{ + []any{9, 9}, + []any{8, 8}, }, }, }, @@ -783,14 +780,14 @@ var schemaExamples = []schemaExample{ }).NewRef(), }).NewRef(), }, - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "array", "uniqueItems": true, - "items": map[string]interface{}{ - "items": map[string]interface{}{ + "items": map[string]any{ + "items": map[string]any{ "type": "object", - "properties": map[string]interface{}{ - "key1": map[string]interface{}{ + "properties": map[string]any{ + "key1": map[string]any{ "type": "number", }, }, @@ -799,71 +796,71 @@ var schemaExamples = []schemaExample{ "type": "array", }, }, - AllValid: []interface{}{ - []interface{}{ - []interface{}{ - map[string]interface{}{ + AllValid: []any{ + []any{ + []any{ + map[string]any{ "key1": 1, }, }, - []interface{}{ - map[string]interface{}{ + []any{ + map[string]any{ "key1": 2, }, }, }, - []interface{}{ // Slice have items with the same value but with different index will treated as different slices - []interface{}{ - map[string]interface{}{ + []any{ + []any{ + map[string]any{ "key1": 1, }, - map[string]interface{}{ + map[string]any{ "key1": 2, }, }, - []interface{}{ - map[string]interface{}{ + []any{ + map[string]any{ "key1": 2, }, - map[string]interface{}{ + map[string]any{ "key1": 1, }, }, }, }, - AllInvalid: []interface{}{ - []interface{}{ // Violate outer array uniqueItems: true - []interface{}{ - map[string]interface{}{ + AllInvalid: []any{ + []any{ + []any{ + map[string]any{ "key1": 1, }, - map[string]interface{}{ + map[string]any{ "key1": 2, }, }, - []interface{}{ - map[string]interface{}{ + []any{ + map[string]any{ "key1": 1, }, - map[string]interface{}{ + map[string]any{ "key1": 2, }, }, }, - []interface{}{ // Violate inner array uniqueItems: true - []interface{}{ - map[string]interface{}{ + []any{ + []any{ + map[string]any{ "key1": 1, }, - map[string]interface{}{ + map[string]any{ "key1": 1, }, }, - []interface{}{ - map[string]interface{}{ + []any{ + map[string]any{ "key1": 2, }, - map[string]interface{}{ + map[string]any{ "key1": 2, }, }, @@ -880,36 +877,36 @@ var schemaExamples = []schemaExample{ "numberProperty": NewFloat64Schema().NewRef(), }, }, - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "object", "maxProperties": 2, - "properties": map[string]interface{}{ - "numberProperty": map[string]interface{}{ + "properties": map[string]any{ + "numberProperty": map[string]any{ "type": "number", }, }, }, - AllValid: []interface{}{ - map[string]interface{}{}, - map[string]interface{}{ + AllValid: []any{ + map[string]any{}, + map[string]any{ "numberProperty": 3.14, }, - map[string]interface{}{ + map[string]any{ "numberProperty": 3.14, "some prop": nil, }, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, false, true, 3.14, "", - []interface{}{}, - map[string]interface{}{ + []any{}, + map[string]any{ "numberProperty": "abc", }, - map[string]interface{}{ + map[string]any{ "numberProperty": 3.14, "some prop": 42, "third": "prop", @@ -925,21 +922,21 @@ var schemaExamples = []schemaExample{ }, }}, }, - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "object", - "additionalProperties": map[string]interface{}{ + "additionalProperties": map[string]any{ "type": "number", }, }, - AllValid: []interface{}{ - map[string]interface{}{}, - map[string]interface{}{ + AllValid: []any{ + map[string]any{}, + map[string]any{ "x": 3.14, "y": 3.14, }, }, - AllInvalid: []interface{}{ - map[string]interface{}{ + AllInvalid: []any{ + map[string]any{ "x": "abc", }, }, @@ -949,13 +946,13 @@ var schemaExamples = []schemaExample{ Type: &Types{"object"}, AdditionalProperties: AdditionalProperties{Has: BoolPtr(true)}, }, - Serialization: map[string]interface{}{ + Serialization: map[string]any{ "type": "object", "additionalProperties": true, }, - AllValid: []interface{}{ - map[string]interface{}{}, - map[string]interface{}{ + AllValid: []any{ + map[string]any{}, + map[string]any{ "x": false, "y": 3.14, }, @@ -967,7 +964,7 @@ var schemaExamples = []schemaExample{ Schema: &Schema{ Not: &SchemaRef{ Value: &Schema{ - Enum: []interface{}{ + Enum: []any{ nil, true, 3.14, @@ -976,9 +973,9 @@ var schemaExamples = []schemaExample{ }, }, }, - Serialization: map[string]interface{}{ - "not": map[string]interface{}{ - "enum": []interface{}{ + Serialization: map[string]any{ + "not": map[string]any{ + "enum": []any{ nil, true, 3.14, @@ -986,12 +983,12 @@ var schemaExamples = []schemaExample{ }, }, }, - AllValid: []interface{}{ + AllValid: []any{ false, 2, "abc", }, - AllInvalid: []interface{}{ + AllInvalid: []any{ nil, true, 3.14, @@ -1015,26 +1012,26 @@ var schemaExamples = []schemaExample{ }, }, }, - Serialization: map[string]interface{}{ - "anyOf": []interface{}{ - map[string]interface{}{ + Serialization: map[string]any{ + "anyOf": []any{ + map[string]any{ "type": "number", "minimum": 1, "maximum": 2, }, - map[string]interface{}{ + map[string]any{ "type": "number", "minimum": 2, "maximum": 3, }, }, }, - AllValid: []interface{}{ + AllValid: []any{ 1, 2, 3, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ 0, 4, }, @@ -1056,24 +1053,24 @@ var schemaExamples = []schemaExample{ }, }, }, - Serialization: map[string]interface{}{ - "allOf": []interface{}{ - map[string]interface{}{ + Serialization: map[string]any{ + "allOf": []any{ + map[string]any{ "type": "number", "minimum": 1, "maximum": 2, }, - map[string]interface{}{ + map[string]any{ "type": "number", "minimum": 2, "maximum": 3, }, }, }, - AllValid: []interface{}{ + AllValid: []any{ 2, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ 0, 1, 3, @@ -1097,25 +1094,25 @@ var schemaExamples = []schemaExample{ }, }, }, - Serialization: map[string]interface{}{ - "oneOf": []interface{}{ - map[string]interface{}{ + Serialization: map[string]any{ + "oneOf": []any{ + map[string]any{ "type": "number", "minimum": 1, "maximum": 2, }, - map[string]interface{}{ + map[string]any{ "type": "number", "minimum": 2, "maximum": 3, }, }, }, - AllValid: []interface{}{ + AllValid: []any{ 1, 3, }, - AllInvalid: []interface{}{ + AllInvalid: []any{ 0, 2, 4, @@ -1247,7 +1244,7 @@ var schemaErrorExamples = []schemaErrorExample{ type schemaMultiErrorExample struct { Title string Schema *Schema - Values []interface{} + Values []any ExpectedErrors []MultiError } @@ -1296,7 +1293,7 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ WithMinLength(2). WithMaxLength(3). WithPattern("^[abc]+$"), - Values: []interface{}{ + Values: []any{ "f", "foobar", }, @@ -1310,7 +1307,7 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ Schema: NewIntegerSchema(). WithMin(1). WithMax(10), - Values: []interface{}{ + Values: []any{ 0.5, 10.1, }, @@ -1326,9 +1323,9 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ WithMaxItems(2). WithItems(NewStringSchema(). WithPattern("^[abc]+$")), - Values: []interface{}{ - []interface{}{"foo"}, - []interface{}{"foo", "bar", "fizz"}, + Values: []any{ + []any{"foo"}, + []any{"foo", "bar", "fizz"}, }, ExpectedErrors: []MultiError{ { @@ -1352,9 +1349,9 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ "key2": NewIntegerSchema(), }), ), - Values: []interface{}{ - []interface{}{ - map[string]interface{}{ + Values: []any{ + []any{ + map[string]any{ "key1": 100, // not a string "key2": "not an integer", }, @@ -1377,11 +1374,11 @@ var schemaMultiErrorExamples = []schemaMultiErrorExample{ WithItems(NewStringSchema(). WithPattern("^[abc]+$")), }), - Values: []interface{}{ - map[string]interface{}{ + Values: []any{ + map[string]any{ "key1": 100, // not a string "key2": "not an integer", - "key3": []interface{}{"abc", "def"}, + "key3": []any{"abc", "def"}, }, }, ExpectedErrors: []MultiError{ @@ -1413,7 +1410,7 @@ components: type: object `[1:]) - data := map[string]interface{}{ + data := map[string]any{ "name": "kin-openapi", "ownerName": true, } @@ -1463,22 +1460,22 @@ enum: err = schema.VisitJSON(1337) require.Error(t, err) - err = schema.VisitJSON([]interface{}{}) + err = schema.VisitJSON([]any{}) require.NoError(t, err) - err = schema.VisitJSON([]interface{}{"a"}) + err = schema.VisitJSON([]any{"a"}) require.NoError(t, err) - err = schema.VisitJSON([]interface{}{"b"}) + err = schema.VisitJSON([]any{"b"}) require.Error(t, err) - err = schema.VisitJSON(map[string]interface{}{}) + err = schema.VisitJSON(map[string]any{}) require.NoError(t, err) - err = schema.VisitJSON(map[string]interface{}{"b": "c"}) + err = schema.VisitJSON(map[string]any{"b": "c"}) require.NoError(t, err) - err = schema.VisitJSON(map[string]interface{}{"d": "e"}) + err = schema.VisitJSON(map[string]any{"d": "e"}) require.Error(t, err) } diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index 7b6662c4d..b5c94b618 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -11,7 +11,7 @@ import ( // SecurityScheme is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object type SecurityScheme struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Type string `json:"type,omitempty" yaml:"type,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -60,8 +60,8 @@ func (ss SecurityScheme) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of SecurityScheme. -func (ss SecurityScheme) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 8+len(ss.Extensions)) +func (ss SecurityScheme) MarshalYAML() (any, error) { + m := make(map[string]any, 8+len(ss.Extensions)) for k, v := range ss.Extensions { m[k] = v } @@ -215,7 +215,7 @@ func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption // OAuthFlows is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object type OAuthFlows struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"` Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"` @@ -242,8 +242,8 @@ func (flows OAuthFlows) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of OAuthFlows. -func (flows OAuthFlows) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 4+len(flows.Extensions)) +func (flows OAuthFlows) MarshalYAML() (any, error) { + m := make(map[string]any, 4+len(flows.Extensions)) for k, v := range flows.Extensions { m[k] = v } @@ -315,7 +315,7 @@ func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) // OAuthFlow is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object type OAuthFlow struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` @@ -333,8 +333,8 @@ func (flow OAuthFlow) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of OAuthFlow. -func (flow OAuthFlow) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 4+len(flow.Extensions)) +func (flow OAuthFlow) MarshalYAML() (any, error) { + m := make(map[string]any, 4+len(flow.Extensions)) for k, v := range flow.Extensions { m[k] = v } diff --git a/openapi3/server.go b/openapi3/server.go index 85b716fc9..7a2007f20 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -51,7 +51,7 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) // Server is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-object type Server struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` URL string `json:"url" yaml:"url"` // Required Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -92,8 +92,8 @@ func (server Server) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Server. -func (server Server) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 3+len(server.Extensions)) +func (server Server) MarshalYAML() (any, error) { + m := make(map[string]any, 3+len(server.Extensions)) for k, v := range server.Extensions { m[k] = v } @@ -234,7 +234,7 @@ func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (e // ServerVariable is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object type ServerVariable struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` Default string `json:"default,omitempty" yaml:"default,omitempty"` @@ -251,8 +251,8 @@ func (serverVariable ServerVariable) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of ServerVariable. -func (serverVariable ServerVariable) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 4+len(serverVariable.Extensions)) +func (serverVariable ServerVariable) MarshalYAML() (any, error) { + m := make(map[string]any, 4+len(serverVariable.Extensions)) for k, v := range serverVariable.Extensions { m[k] = v } diff --git a/openapi3/tag.go b/openapi3/tag.go index 8dc996724..182d0502a 100644 --- a/openapi3/tag.go +++ b/openapi3/tag.go @@ -33,7 +33,7 @@ func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error { // Tag is specified by OpenAPI/Swagger 3.0 standard. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tag-object type Tag struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -50,8 +50,8 @@ func (t Tag) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of Tag. -func (t Tag) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 3+len(t.Extensions)) +func (t Tag) MarshalYAML() (any, error) { + m := make(map[string]any, 3+len(t.Extensions)) for k, v := range t.Extensions { m[k] = v } diff --git a/openapi3/testdata/circularRef/base.yml b/openapi3/testdata/circularRef/base.yml index ff8240eb0..897a45f37 100644 --- a/openapi3/testdata/circularRef/base.yml +++ b/openapi3/testdata/circularRef/base.yml @@ -14,3 +14,5 @@ components: properties: foo: $ref: "#/components/schemas/Foo" + Baz: + $ref: "./baz.yml#/BazNested" diff --git a/openapi3/testdata/circularRef/baz.yml b/openapi3/testdata/circularRef/baz.yml new file mode 100644 index 000000000..fb8c85420 --- /dev/null +++ b/openapi3/testdata/circularRef/baz.yml @@ -0,0 +1,9 @@ +BazNested: + type: object + properties: + baz: + $ref: "#/BazNested" + bazArray: + type: array + items: + $ref: "#/BazNested" diff --git a/openapi3/testdata/circularRef2/AwsEnvironmentSettings.yaml b/openapi3/testdata/circularRef2/AwsEnvironmentSettings.yaml new file mode 100644 index 000000000..33ba38f3e --- /dev/null +++ b/openapi3/testdata/circularRef2/AwsEnvironmentSettings.yaml @@ -0,0 +1,7 @@ +type: object +properties: + children: + type: array + items: + $ref: './AwsEnvironmentSettings.yaml' +description: test \ No newline at end of file diff --git a/openapi3/testdata/circularRef2/circular2.yaml b/openapi3/testdata/circularRef2/circular2.yaml new file mode 100644 index 000000000..f10bf590a --- /dev/null +++ b/openapi3/testdata/circularRef2/circular2.yaml @@ -0,0 +1,16 @@ +openapi: 3.0.0 +info: + title: Circular Reference Example + version: 1.0.0 +paths: + /sample: + put: + requestBody: + required: true + content: + application/json: + schema: + $ref: './AwsEnvironmentSettings.yaml' + responses: + '200': + description: Ok \ No newline at end of file diff --git a/openapi3/testdata/interalizationNameCollision/api.yml b/openapi3/testdata/interalizationNameCollision/api.yml new file mode 100644 index 000000000..fe39f2986 --- /dev/null +++ b/openapi3/testdata/interalizationNameCollision/api.yml @@ -0,0 +1,25 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Internalise ref name collision. +paths: + /book/record: + get: + operationId: getBookRecord + responses: + 200: + description: A Book record. + content: + application/json: + schema: + $ref: './schemas/book/record.yml' + /cd/record: + get: + operationId: getCDRecord + responses: + 200: + description: A CD record. + content: + application/json: + schema: + $ref: './schemas/cd/record.yml' diff --git a/openapi3/testdata/interalizationNameCollision/api.yml.internalized.yml b/openapi3/testdata/interalizationNameCollision/api.yml.internalized.yml new file mode 100644 index 000000000..75e52f0fc --- /dev/null +++ b/openapi3/testdata/interalizationNameCollision/api.yml.internalized.yml @@ -0,0 +1,75 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Internalise ref name collision." + }, + "paths": { + "/book/record": { + "get": { + "operationId": "getBookRecord", + "responses": { + "200": { + "description": "A Book record.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/schemas_book_record" + } + } + } + } + } + } + }, + "/cd/record": { + "get": { + "operationId": "getCDRecord", + "responses": { + "200": { + "description": "A CD record.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/schemas_cd_record" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "schemas_book_record": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "number" + }, + "pages": { + "type": "number" + } + } + }, + "schemas_cd_record": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "number" + }, + "tracks": { + "type": "number" + } + } + } + } + } +} diff --git a/openapi3/testdata/interalizationNameCollision/schemas/book/record.yml b/openapi3/testdata/interalizationNameCollision/schemas/book/record.yml new file mode 100644 index 000000000..ef72cc994 --- /dev/null +++ b/openapi3/testdata/interalizationNameCollision/schemas/book/record.yml @@ -0,0 +1,8 @@ +type: object +required: + - id +properties: + id: + type: number + pages: + type: number diff --git a/openapi3/testdata/interalizationNameCollision/schemas/cd/record.yml b/openapi3/testdata/interalizationNameCollision/schemas/cd/record.yml new file mode 100644 index 000000000..0e5ba7acf --- /dev/null +++ b/openapi3/testdata/interalizationNameCollision/schemas/cd/record.yml @@ -0,0 +1,8 @@ +type: object +required: + - id +properties: + id: + type: number + tracks: + type: number diff --git a/openapi3/testdata/issue959/openapi.yml.internalized.yml b/openapi3/testdata/issue959/openapi.yml.internalized.yml index 3cbe674d6..db83681dc 100644 --- a/openapi3/testdata/issue959/openapi.yml.internalized.yml +++ b/openapi3/testdata/issue959/openapi.yml.internalized.yml @@ -1,7 +1,7 @@ { "components": { "schemas": { - "External1": { + "components_External1": { "type": "string" } } @@ -26,10 +26,10 @@ "name": "external1", "required": true, "schema": { - "$ref": "#/components/schemas/External1" + "$ref": "#/components/schemas/components_External1" } } ] } } -} \ No newline at end of file +} diff --git a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml index 0d508527a..33387fdf0 100644 --- a/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml +++ b/openapi3/testdata/recursiveRef/openapi.yml.internalized.yml @@ -1,7 +1,7 @@ { "components": { "parameters": { - "number": { + "parameters_number": { "in": "query", "name": "someNumber", "schema": { @@ -22,6 +22,34 @@ } }, "schemas": { + "components_Bar": { + "example": "bar", + "type": "string" + }, + "components_Cat": { + "properties": { + "cat": { + "$ref": "#/components/schemas/components_Cat" + } + }, + "type": "object" + }, + "components_Foo": { + "properties": { + "bar": { + "$ref": "#/components/schemas/components_Bar" + } + }, + "type": "object" + }, + "components_Foo_Foo2": { + "properties": { + "foo": { + "$ref": "#/components/schemas/components_Foo" + } + }, + "type": "object" + }, "Bar": { "example": "bar", "type": "string" @@ -33,7 +61,7 @@ "Foo": { "properties": { "bar": { - "$ref": "#/components/schemas/Bar" + "$ref": "#/components/schemas/components_Bar" } }, "type": "object" @@ -41,19 +69,15 @@ "Foo2": { "properties": { "foo": { - "$ref": "#/components/schemas/Foo" + "$ref": "#/components/schemas/components_Foo" } }, "type": "object" }, - "error":{ - "title":"ErrorDetails", - "type":"object" - }, "Cat": { "properties": { "cat": { - "$ref": "#/components/schemas/Cat" + "$ref": "#/components/schemas/components_Cat" } }, "type": "object" @@ -86,7 +110,7 @@ "schema": { "properties": { "foo2": { - "$ref": "#/components/schemas/Foo2" + "$ref": "#/components/schemas/components_Foo_Foo2" } }, "type": "object" @@ -102,7 +126,7 @@ }, "parameters": [ { - "$ref": "#/components/parameters/number" + "$ref": "#/components/parameters/parameters_number" } ] } diff --git a/openapi3/testdata/refsToRoot/openapi.yml b/openapi3/testdata/refsToRoot/openapi.yml new file mode 100644 index 000000000..ad9fc1e9b --- /dev/null +++ b/openapi3/testdata/refsToRoot/openapi.yml @@ -0,0 +1,60 @@ +openapi: 3.0.3 +info: + version: 1.0.0 + title: Mode ref resolution Example +paths: + /records: + get: + operationId: getBookRecords + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/BookRecords' + 500: + $ref: './other/response.yml' + /record: + get: + operationId: getBookRecord + parameters: + - $ref: 'other/parameter.yml' + responses: + 200: + content: + application/json: + schema: + $ref: '#/components/schemas/BookRecord' + examples: + first-example: + $ref: './other/example.yml' + headers: + X-Custom-Header: + $ref: 'schema/book/../../other/header.yml' + X-Custom-Header2: + schema: + type: string + 500: + $ref: './other/response.yml' +components: + schemas: + BookRecord: + $ref: './schemas/book/record.yml' + BookRecords: + $ref: './schemas/book/records.yml' + CdRecord: + $ref: './schemas/cd/record.yml' + CdRecords: + $ref: './schemas/cd/records.yml' + responses: + ErrorResponse: + $ref: './other/response.yml' + parameters: + BookIDParameter: + $ref: './other/parameter.yml' + headers: + CustomHeader: + $ref: './other/header.yml' + examples: + RecordResponseExample: + $ref: './other/example.yml' diff --git a/openapi3/testdata/refsToRoot/other/example.yml b/openapi3/testdata/refsToRoot/other/example.yml new file mode 100644 index 000000000..2137d106e --- /dev/null +++ b/openapi3/testdata/refsToRoot/other/example.yml @@ -0,0 +1,2 @@ +description: Example example +id: 42 diff --git a/openapi3/testdata/refsToRoot/other/header.yml b/openapi3/testdata/refsToRoot/other/header.yml new file mode 100644 index 000000000..5fe26bfb2 --- /dev/null +++ b/openapi3/testdata/refsToRoot/other/header.yml @@ -0,0 +1,3 @@ +description: Example +schema: + type: string diff --git a/openapi3/testdata/refsToRoot/other/parameter.yml b/openapi3/testdata/refsToRoot/other/parameter.yml new file mode 100644 index 000000000..b30e0a33f --- /dev/null +++ b/openapi3/testdata/refsToRoot/other/parameter.yml @@ -0,0 +1,2 @@ +name: id +in: query diff --git a/openapi3/testdata/refsToRoot/other/response.yml b/openapi3/testdata/refsToRoot/other/response.yml new file mode 100644 index 000000000..e1766afba --- /dev/null +++ b/openapi3/testdata/refsToRoot/other/response.yml @@ -0,0 +1,4 @@ +content: + application/json: + schema: + $ref: '../schemas/error.yml' diff --git a/openapi3/testdata/refsToRoot/schemas/book/record.yml b/openapi3/testdata/refsToRoot/schemas/book/record.yml new file mode 100644 index 000000000..0e383fcdd --- /dev/null +++ b/openapi3/testdata/refsToRoot/schemas/book/record.yml @@ -0,0 +1,6 @@ +type: object +required: + - id +properties: + id: + type: number diff --git a/openapi3/testdata/refsToRoot/schemas/book/records.yml b/openapi3/testdata/refsToRoot/schemas/book/records.yml new file mode 100644 index 000000000..5097d391d --- /dev/null +++ b/openapi3/testdata/refsToRoot/schemas/book/records.yml @@ -0,0 +1,3 @@ +type: array +items: + $ref: './record.yml' diff --git a/openapi3/testdata/refsToRoot/schemas/cd/record.yml b/openapi3/testdata/refsToRoot/schemas/cd/record.yml new file mode 100644 index 000000000..0e383fcdd --- /dev/null +++ b/openapi3/testdata/refsToRoot/schemas/cd/record.yml @@ -0,0 +1,6 @@ +type: object +required: + - id +properties: + id: + type: number diff --git a/openapi3/testdata/refsToRoot/schemas/cd/records.yml b/openapi3/testdata/refsToRoot/schemas/cd/records.yml new file mode 100644 index 000000000..22ea0332a --- /dev/null +++ b/openapi3/testdata/refsToRoot/schemas/cd/records.yml @@ -0,0 +1,3 @@ +type: array +items: + $ref: '../../openapi.yml#/components/schemas/CdRecord' diff --git a/openapi3/testdata/refsToRoot/schemas/error.yml b/openapi3/testdata/refsToRoot/schemas/error.yml new file mode 100644 index 000000000..f2ed54321 --- /dev/null +++ b/openapi3/testdata/refsToRoot/schemas/error.yml @@ -0,0 +1,6 @@ +type: object +required: + - msg +properties: + id: + type: string diff --git a/openapi3/testdata/spec.yaml.internalized.yml b/openapi3/testdata/spec.yaml.internalized.yml index feca4a00c..bcfd3fe9f 100644 --- a/openapi3/testdata/spec.yaml.internalized.yml +++ b/openapi3/testdata/spec.yaml.internalized.yml @@ -4,19 +4,19 @@ "Test": { "properties": { "test": { - "$ref": "#/components/schemas/b" + "$ref": "#/components/schemas/ext_definitions_b" } }, "type": "object" }, - "a": { + "ext_definitions_a": { "type": "string" }, - "b": { + "ext_definitions_b": { "description": "I use a local reference.", "properties": { "name": { - "$ref": "#/components/schemas/a" + "$ref": "#/components/schemas/ext_definitions_a" } }, "type": "object" diff --git a/openapi3/testdata/testref.openapi.yml.internalized.yml b/openapi3/testdata/testref.openapi.yml.internalized.yml index e35a50041..f270941d8 100644 --- a/openapi3/testdata/testref.openapi.yml.internalized.yml +++ b/openapi3/testdata/testref.openapi.yml.internalized.yml @@ -4,7 +4,7 @@ "AnotherTestSchema": { "type": "string" }, - "CustomTestSchema": { + "components_Name": { "type": "string" } } diff --git a/openapi3/unique_items_checker_test.go b/openapi3/unique_items_checker_test.go index 3dd72306a..1c81180b4 100644 --- a/openapi3/unique_items_checker_test.go +++ b/openapi3/unique_items_checker_test.go @@ -15,7 +15,7 @@ func TestRegisterArrayUniqueItemsChecker(t *testing.T) { UniqueItems: true, Items: openapi3.NewStringSchema().NewRef(), } - val = []interface{}{"1", "2", "3"} + val = []any{"1", "2", "3"} ) // Fist checked by predefined function @@ -25,7 +25,7 @@ func TestRegisterArrayUniqueItemsChecker(t *testing.T) { // Register a function will always return false when check if a // slice has unique items, then use a slice indeed has unique // items to verify that check unique items will failed. - openapi3.RegisterArrayUniqueItemsChecker(func(items []interface{}) bool { + openapi3.RegisterArrayUniqueItemsChecker(func(items []any) bool { return false }) defer openapi3.RegisterArrayUniqueItemsChecker(nil) // Reset for other tests diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go index 8982594b5..45563256a 100644 --- a/openapi3/validation_options.go +++ b/openapi3/validation_options.go @@ -12,6 +12,7 @@ type ValidationOptions struct { schemaDefaultsValidationDisabled bool schemaFormatValidationEnabled bool schemaPatternValidationDisabled bool + schemaExtensionsInRefProhibited bool extraSiblingFieldsAllowed map[string]struct{} } @@ -92,6 +93,26 @@ func DisableExamplesValidation() ValidationOption { } } +// AllowExtensionsWithRef allows extensions (fields starting with 'x-') +// as siblings for $ref fields. This is the default. +// Non-extension fields are prohibited unless allowed explicitly with the +// AllowExtraSiblingFields option. +func AllowExtensionsWithRef() ValidationOption { + return func(options *ValidationOptions) { + options.schemaExtensionsInRefProhibited = false + } +} + +// ProhibitExtensionsWithRef causes the validation to return an +// error if extensions (fields starting with 'x-') are found as +// siblings for $ref fields. Non-extension fields are prohibited +// unless allowed explicitly with the AllowExtraSiblingFields option. +func ProhibitExtensionsWithRef() ValidationOption { + return func(options *ValidationOptions) { + options.schemaExtensionsInRefProhibited = true + } +} + // WithValidationOptions allows adding validation options to a context object that can be used when validating any OpenAPI type. func WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context { if len(opts) == 0 { diff --git a/openapi3/xml.go b/openapi3/xml.go index e69c1fa6d..69d1b3483 100644 --- a/openapi3/xml.go +++ b/openapi3/xml.go @@ -8,7 +8,7 @@ import ( // XML is specified by OpenAPI/Swagger standard version 3. // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xml-object type XML struct { - Extensions map[string]interface{} `json:"-" yaml:"-"` + Extensions map[string]any `json:"-" yaml:"-"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` @@ -27,8 +27,8 @@ func (xml XML) MarshalJSON() ([]byte, error) { } // MarshalYAML returns the YAML encoding of XML. -func (xml XML) MarshalYAML() (interface{}, error) { - m := make(map[string]interface{}, 5+len(xml.Extensions)) +func (xml XML) MarshalYAML() (any, error) { + m := make(map[string]any, 5+len(xml.Extensions)) for k, v := range xml.Extensions { m[k] = v } diff --git a/openapi3filter/internal.go b/openapi3filter/internal.go index 5c6a8a6c6..f807e06f1 100644 --- a/openapi3filter/internal.go +++ b/openapi3filter/internal.go @@ -13,7 +13,7 @@ func parseMediaType(contentType string) string { return contentType[:i] } -func isNilValue(value interface{}) bool { +func isNilValue(value any) bool { if value == nil { return true } diff --git a/openapi3filter/issue639_test.go b/openapi3filter/issue639_test.go index 5656d3eee..136967953 100644 --- a/openapi3filter/issue639_test.go +++ b/openapi3filter/issue639_test.go @@ -52,7 +52,7 @@ func TestIssue639(t *testing.T) { tests := []struct { name string options *Options - expectedDefaultVal interface{} + expectedDefaultVal any }{ { name: "no defaults are added to requests", @@ -90,7 +90,7 @@ func TestIssue639(t *testing.T) { bodyAfterValidation, err := io.ReadAll(httpReq.Body) require.NoError(t, err) - raw := map[string]interface{}{} + raw := map[string]any{} err = json.Unmarshal(bodyAfterValidation, &raw) require.NoError(t, err) require.Equal(t, testcase.expectedDefaultVal, diff --git a/openapi3filter/issue733_test.go b/openapi3filter/issue733_test.go index 0d2214b58..f43a826e8 100644 --- a/openapi3filter/issue733_test.go +++ b/openapi3filter/issue733_test.go @@ -76,7 +76,7 @@ paths: dec := json.NewDecoder(req.Body) dec.UseNumber() - var jsonAfter map[string]interface{} + var jsonAfter map[string]any err = dec.Decode(&jsonAfter) require.NoError(t, err) diff --git a/openapi3filter/middleware_test.go b/openapi3filter/middleware_test.go index 137228cd1..d492bec0c 100644 --- a/openapi3filter/middleware_test.go +++ b/openapi3filter/middleware_test.go @@ -451,7 +451,7 @@ paths: panic(err) } w.Header().Set("Content-Type", "application/json") - result := map[string]interface{}{"result": x * x} + result := map[string]any{"result": x * x} if x == 42 { // An easter egg. Unfortunately, the spec does not allow additional properties... result["comment"] = "the answer to the ultimate question of life, the universe, and everything" @@ -494,7 +494,7 @@ paths: // Customize validation error responses to use JSON w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) - json.NewEncoder(w).Encode(map[string]interface{}{ + json.NewEncoder(w).Encode(map[string]any{ "status": status, "message": http.StatusText(status), }) diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 882d2d34d..c509def3e 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -39,11 +39,11 @@ const ( // ParseError describes errors which happens while parse operation's parameters, requestBody, or response. type ParseError struct { Kind ParseErrorKind - Value interface{} + Value any Reason string Cause error - path []interface{} + path []any } var _ interface{ Unwrap() error } = ParseError{} @@ -92,8 +92,8 @@ func (e ParseError) Unwrap() error { } // Path returns a path to the root cause. -func (e *ParseError) Path() []interface{} { - var path []interface{} +func (e *ParseError) Path() []any { + var path []any if v, ok := e.Cause.(*ParseError); ok { p := v.Path() if len(p) > 0 { @@ -113,7 +113,7 @@ func invalidSerializationMethodErr(sm *openapi3.SerializationMethod) error { // Decodes a parameter defined via the content property as an object. It uses // the user specified decoder, or our build-in decoder for application/json func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationInput) ( - value interface{}, + value any, schema *openapi3.Schema, found bool, err error, @@ -164,7 +164,7 @@ func decodeContentParameter(param *openapi3.Parameter, input *RequestValidationI } func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) ( - outValue interface{}, + outValue any, outSchema *openapi3.Schema, err error, ) { @@ -192,7 +192,7 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) } outSchema = mt.Schema.Value - unmarshal := func(encoded string, paramSchema *openapi3.SchemaRef) (decoded interface{}, err error) { + unmarshal := func(encoded string, paramSchema *openapi3.SchemaRef) (decoded any, err error) { if err = json.Unmarshal([]byte(encoded), &decoded); err != nil { if paramSchema != nil && !paramSchema.Value.Type.Is("object") { decoded, err = encoded, nil @@ -207,9 +207,9 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) return } } else { - outArray := make([]interface{}, 0, len(values)) + outArray := make([]any, 0, len(values)) for _, v := range values { - var item interface{} + var item any if item, err = unmarshal(v, outSchema.Items); err != nil { err = fmt.Errorf("error unmarshaling parameter %q", param.Name) return @@ -222,15 +222,15 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) } type valueDecoder interface { - DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) - DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) - DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) + DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) + DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]any, bool, error) + DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]any, bool, error) } // decodeStyledParameter returns a value of an operation's parameter from HTTP request for // parameters defined using the style format, and whether the parameter is supplied in the input. // The function returns ParseError when HTTP request contains an invalid value of a parameter. -func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationInput) (interface{}, bool, error) { +func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationInput) (any, bool, error) { sm, err := param.SerializationMethod() if err != nil { return nil, false, err @@ -259,11 +259,11 @@ func decodeStyledParameter(param *openapi3.Parameter, input *RequestValidationIn return decodeValue(dec, param.Name, sm, param.Schema, param.Required) } -func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef, required bool) (interface{}, bool, error) { +func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef, required bool) (any, bool, error) { var found bool if len(schema.Value.AllOf) > 0 { - var value interface{} + var value any var err error for _, sr := range schema.Value.AllOf { var f bool @@ -292,7 +292,7 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho if len(schema.Value.OneOf) > 0 { isMatched := 0 - var value interface{} + var value any for _, sr := range schema.Value.OneOf { v, f, _ := decodeValue(dec, param, sm, sr, required) found = found || f @@ -316,14 +316,14 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho } if schema.Value.Type != nil { - var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) + var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) switch { case schema.Value.Type.Is("array"): - decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { + decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { return dec.DecodeArray(param, sm, schema) } case schema.Value.Type.Is("object"): - decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { + decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { return dec.DecodeObject(param, sm, schema) } default: @@ -355,7 +355,7 @@ type pathParamDecoder struct { pathParams map[string]string } -func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { +func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { var prefix string switch sm.Style { case "simple": @@ -385,7 +385,7 @@ func (d *pathParamDecoder) DecodePrimitive(param string, sm *openapi3.Serializat return val, ok, err } -func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { +func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]any, bool, error) { var prefix, delim string switch { case sm.Style == "simple": @@ -423,7 +423,7 @@ func (d *pathParamDecoder) DecodeArray(param string, sm *openapi3.SerializationM return val, ok, err } -func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { +func (d *pathParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]any, bool, error) { var prefix, propsDelim, valueDelim string switch { case sm.Style == "simple" && !sm.Explode: @@ -495,7 +495,7 @@ type urlValuesDecoder struct { values url.Values } -func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { +func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { if sm.Style != "form" { return nil, false, invalidSerializationMethodErr(sm) } @@ -513,7 +513,7 @@ func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.Serializat return val, ok, err } -func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { +func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]any, bool, error) { if sm.Style == "deepObject" { return nil, false, invalidSerializationMethodErr(sm) } @@ -542,14 +542,14 @@ func (d *urlValuesDecoder) DecodeArray(param string, sm *openapi3.SerializationM // parseArray returns an array that contains items from a raw array. // Every item is parsed as a primitive value. // The function returns an error when an error happened while parse array's items. -func (d *urlValuesDecoder) parseArray(raw []string, sm *openapi3.SerializationMethod, schemaRef *openapi3.SchemaRef) ([]interface{}, error) { - var value []interface{} +func (d *urlValuesDecoder) parseArray(raw []string, sm *openapi3.SerializationMethod, schemaRef *openapi3.SchemaRef) ([]any, error) { + var value []any for i, v := range raw { item, err := d.parseValue(v, schemaRef.Value.Items) if err != nil { if v, ok := err.(*ParseError); ok { - return nil, &ParseError{path: []interface{}{i}, Cause: v} + return nil, &ParseError{path: []any{i}, Cause: v} } return nil, fmt.Errorf("item %d: %w", i, err) } @@ -564,9 +564,9 @@ func (d *urlValuesDecoder) parseArray(raw []string, sm *openapi3.SerializationMe return value, nil } -func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (interface{}, error) { +func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (any, error) { if len(schema.Value.AllOf) > 0 { - var value interface{} + var value any var err error for _, sr := range schema.Value.AllOf { value, err = d.parseValue(v, sr) @@ -578,7 +578,7 @@ func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (int } if len(schema.Value.AnyOf) > 0 { - var value interface{} + var value any var err error for _, sr := range schema.Value.AnyOf { if value, err = d.parseValue(v, sr); err == nil { @@ -591,7 +591,7 @@ func (d *urlValuesDecoder) parseValue(v string, schema *openapi3.SchemaRef) (int if len(schema.Value.OneOf) > 0 { isMatched := 0 - var value interface{} + var value any var err error for _, sr := range schema.Value.OneOf { result, err := d.parseValue(v, sr) @@ -623,7 +623,7 @@ const ( urlDecoderDelimiter = "\x1F" // should not conflict with URL characters ) -func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { +func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]any, bool, error) { var propsFn func(url.Values) (map[string]string, error) switch sm.Style { case "form": @@ -714,7 +714,7 @@ type headerParamDecoder struct { header http.Header } -func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { +func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { if sm.Style != "simple" { return nil, false, invalidSerializationMethodErr(sm) } @@ -729,7 +729,7 @@ func (d *headerParamDecoder) DecodePrimitive(param string, sm *openapi3.Serializ return val, ok, err } -func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { +func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]any, bool, error) { if sm.Style != "simple" { return nil, false, invalidSerializationMethodErr(sm) } @@ -744,7 +744,7 @@ func (d *headerParamDecoder) DecodeArray(param string, sm *openapi3.Serializatio return val, ok, err } -func (d *headerParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { +func (d *headerParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]any, bool, error) { if sm.Style != "simple" { return nil, false, invalidSerializationMethodErr(sm) } @@ -771,7 +771,7 @@ type cookieParamDecoder struct { req *http.Request } -func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { +func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (any, bool, error) { if sm.Style != "form" { return nil, false, invalidSerializationMethodErr(sm) } @@ -790,7 +790,7 @@ func (d *cookieParamDecoder) DecodePrimitive(param string, sm *openapi3.Serializ return val, found, err } -func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]interface{}, bool, error) { +func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) ([]any, bool, error) { if sm.Style != "form" || sm.Explode { return nil, false, invalidSerializationMethodErr(sm) } @@ -808,7 +808,7 @@ func (d *cookieParamDecoder) DecodeArray(param string, sm *openapi3.Serializatio return val, found, err } -func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]interface{}, bool, error) { +func (d *cookieParamDecoder) DecodeObject(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (map[string]any, bool, error) { if sm.Style != "form" || sm.Explode { return nil, false, invalidSerializationMethodErr(sm) } @@ -871,26 +871,26 @@ func propsFromString(src, propDelim, valueDelim string) (map[string]string, erro return props, nil } -func deepGet(m map[string]interface{}, keys ...string) (interface{}, bool) { +func deepGet(m map[string]any, keys ...string) (any, bool) { for _, key := range keys { val, ok := m[key] if !ok { return nil, false } - if m, ok = val.(map[string]interface{}); !ok { + if m, ok = val.(map[string]any); !ok { return val, true } } return m, true } -func deepSet(m map[string]interface{}, keys []string, value interface{}) { +func deepSet(m map[string]any, keys []string, value any) { for i := 0; i < len(keys)-1; i++ { key := keys[i] if _, ok := m[key]; !ok { - m[key] = make(map[string]interface{}) + m[key] = make(map[string]any) } - m = m[key].(map[string]interface{}) + m = m[key].(map[string]any) } m[keys[len(keys)-1]] = value } @@ -916,8 +916,8 @@ func findNestedSchema(parentSchema *openapi3.SchemaRef, keys []string) (*openapi } // makeObject returns an object that contains properties from props. -func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string]interface{}, error) { - mobj := make(map[string]interface{}) +func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string]any, error) { + mobj := make(map[string]any) for kk, value := range props { keys := strings.Split(kk, urlDecoderDelimiter) @@ -932,7 +932,7 @@ func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string if err != nil { return nil, err } - result, ok := r.(map[string]interface{}) + result, ok := r.(map[string]any) if !ok { return nil, &ParseError{Kind: KindOther, Reason: "invalid param object", Value: result} } @@ -941,8 +941,8 @@ func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string } // example: map[0:map[key:true] 1:map[key:false]] -> [map[key:true] map[key:false]] -func sliceMapToSlice(m map[string]interface{}) ([]interface{}, error) { - var result []interface{} +func sliceMapToSlice(m map[string]any) ([]any, error) { + var result []any keys := make([]int, 0, len(m)) for k := range m { @@ -970,7 +970,7 @@ func sliceMapToSlice(m map[string]interface{}) ([]interface{}, error) { } // buildResObj constructs an object based on a given schema and param values -func buildResObj(params map[string]interface{}, parentKeys []string, key string, schema *openapi3.SchemaRef) (interface{}, error) { +func buildResObj(params map[string]any, parentKeys []string, key string, schema *openapi3.SchemaRef) (any, error) { mapKeys := parentKeys if key != "" { mapKeys = append(mapKeys, key) @@ -982,7 +982,7 @@ func buildResObj(params map[string]interface{}, parentKeys []string, key string, if !ok { return nil, nil } - t, isMap := paramArr.(map[string]interface{}) + t, isMap := paramArr.(map[string]any) if !isMap { return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Reason: "array items must be set with indexes"} } @@ -991,7 +991,7 @@ func buildResObj(params map[string]interface{}, parentKeys []string, key string, if err != nil { return nil, &ParseError{path: pathFromKeys(mapKeys), Kind: KindInvalidFormat, Reason: fmt.Sprintf("could not convert value map to array: %v", err)} } - resultArr := make([]interface{} /*not 0,*/, len(arr)) + resultArr := make([]any /*not 0,*/, len(arr)) for i := range arr { r, err := buildResObj(params, mapKeys, strconv.Itoa(i), schema.Value.Items) if err != nil { @@ -1003,10 +1003,10 @@ func buildResObj(params map[string]interface{}, parentKeys []string, key string, } return resultArr, nil case schema.Value.Type.Is("object"): - resultMap := make(map[string]interface{}) + resultMap := make(map[string]any) additPropsSchema := schema.Value.AdditionalProperties.Schema pp, _ := deepGet(params, mapKeys...) - objectParams, ok := pp.(map[string]interface{}) + objectParams, ok := pp.(map[string]any) if !ok { // not the expected type, but return it either way and leave validation up to ValidateParameter return pp, nil @@ -1060,20 +1060,20 @@ func buildResObj(params map[string]interface{}, parentKeys []string, key string, } // buildFromSchemas decodes params with anyOf, oneOf, allOf schemas. -func buildFromSchemas(schemas openapi3.SchemaRefs, params map[string]interface{}, mapKeys []string, key string) (interface{}, error) { - resultMap := make(map[string]interface{}) +func buildFromSchemas(schemas openapi3.SchemaRefs, params map[string]any, mapKeys []string, key string) (any, error) { + resultMap := make(map[string]any) for _, s := range schemas { val, err := buildResObj(params, mapKeys, key, s) if err == nil && val != nil { - if m, ok := val.(map[string]interface{}); ok { + if m, ok := val.(map[string]any); ok { for k, v := range m { resultMap[k] = v } continue } - if a, ok := val.([]interface{}); ok { + if a, ok := val.([]any); ok { if len(a) > 0 { return a, nil } @@ -1099,8 +1099,8 @@ func handlePropParseError(path []string, err error) error { return fmt.Errorf("property %q: %w", strings.Join(path, "."), err) } -func pathFromKeys(kk []string) []interface{} { - path := make([]interface{}, 0, len(kk)) +func pathFromKeys(kk []string) []any { + path := make([]any, 0, len(kk)) for _, v := range kk { path = append(path, v) } @@ -1110,13 +1110,13 @@ func pathFromKeys(kk []string) []interface{} { // parseArray returns an array that contains items from a raw array. // Every item is parsed as a primitive value. // The function returns an error when an error happened while parse array's items. -func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, error) { - var value []interface{} +func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]any, error) { + var value []any for i, v := range raw { item, err := parsePrimitive(v, schemaRef.Value.Items) if err != nil { if v, ok := err.(*ParseError); ok { - return nil, &ParseError{path: []interface{}{i}, Cause: v} + return nil, &ParseError{path: []any{i}, Cause: v} } return nil, fmt.Errorf("item %d: %w", i, err) } @@ -1134,7 +1134,7 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err // parsePrimitive returns a value that is created by parsing a source string to a primitive type // that is specified by a schema. The function returns nil when the source string is empty. // The function panics when a schema has a non-primitive type. -func parsePrimitive(raw string, schema *openapi3.SchemaRef) (v interface{}, err error) { +func parsePrimitive(raw string, schema *openapi3.SchemaRef) (v any, err error) { if raw == "" { return nil, nil } @@ -1146,7 +1146,7 @@ func parsePrimitive(raw string, schema *openapi3.SchemaRef) (v interface{}, err return } -func parsePrimitiveCase(raw string, schema *openapi3.SchemaRef, typ string) (interface{}, error) { +func parsePrimitiveCase(raw string, schema *openapi3.SchemaRef, typ string) (any, error) { switch typ { case "integer": if schema.Value.Format == "int32" { @@ -1184,8 +1184,8 @@ func parsePrimitiveCase(raw string, schema *openapi3.SchemaRef, typ string) (int type EncodingFn func(partName string) *openapi3.Encoding // BodyDecoder is an interface to decode a body of a request or response. -// An implementation must return a value that is a primitive, []interface{}, or map[string]interface{}. -type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (interface{}, error) +// An implementation must return a value that is a primitive, []any, or map[string]any. +type BodyDecoder func(io.Reader, http.Header, *openapi3.SchemaRef, EncodingFn) (any, error) // bodyDecoders contains decoders for supported content types of a body. // By default, there is content type "application/json" is supported only. @@ -1233,7 +1233,7 @@ const prefixUnsupportedCT = "unsupported content type" // The function returns ParseError when a body is invalid. func decodeBody(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) ( string, - interface{}, + any, error, ) { contentType := header.Get(headerCT) @@ -1271,7 +1271,7 @@ func init() { RegisterBodyDecoder("text/plain", plainBodyDecoder) } -func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { +func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (any, error) { data, err := io.ReadAll(body) if err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} @@ -1281,8 +1281,8 @@ func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schem // JSONBodyDecoder decodes a JSON formatted body. It is public so that is easy // to register additional JSON based formats. -func JSONBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { - var value interface{} +func JSONBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (any, error) { + var value any dec := json.NewDecoder(body) dec.UseNumber() if err := dec.Decode(&value); err != nil { @@ -1291,15 +1291,15 @@ func JSONBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schema return value, nil } -func yamlBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { - var value interface{} +func yamlBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (any, error) { + var value any if err := yaml.NewDecoder(body).Decode(&value); err != nil { return nil, &ParseError{Kind: KindInvalidFormat, Cause: err} } return value, nil } -func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { +func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (any, error) { // Validate schema of request body. // By the OpenAPI 3 specification request body's schema must have type "object". // Properties of the schema describes individual parts of request body. @@ -1330,7 +1330,7 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. } // Make an object value from form values. - obj := make(map[string]interface{}) + obj := make(map[string]any) dec := &urlValuesDecoder{values: values} // Decode schema constructs (allOf, anyOf, oneOf) @@ -1354,7 +1354,7 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. // decodeSchemaConstructs tries to decode properties based on provided schemas. // This function is for decoding purposes only and not for validation. -func decodeSchemaConstructs(dec *urlValuesDecoder, schemas []*openapi3.SchemaRef, obj map[string]interface{}, encFn EncodingFn) error { +func decodeSchemaConstructs(dec *urlValuesDecoder, schemas []*openapi3.SchemaRef, obj map[string]any, encFn EncodingFn) error { for _, schemaRef := range schemas { for name, prop := range schemaRef.Value.Properties { value, _, err := decodeProperty(dec, name, prop, encFn) @@ -1371,11 +1371,11 @@ func decodeSchemaConstructs(dec *urlValuesDecoder, schemas []*openapi3.SchemaRef return nil } -func isEqual(value1, value2 interface{}) bool { +func isEqual(value1, value2 any) bool { return reflect.DeepEqual(value1, value2) } -func decodeProperty(dec valueDecoder, name string, prop *openapi3.SchemaRef, encFn EncodingFn) (interface{}, bool, error) { +func decodeProperty(dec valueDecoder, name string, prop *openapi3.SchemaRef, encFn EncodingFn) (any, bool, error) { var enc *openapi3.Encoding if encFn != nil { enc = encFn(name) @@ -1384,13 +1384,13 @@ func decodeProperty(dec valueDecoder, name string, prop *openapi3.SchemaRef, enc return decodeValue(dec, name, sm, prop, false) } -func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { +func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (any, error) { if !schema.Value.Type.Is("object") { return nil, errors.New("unsupported schema of request body") } // Parse form. - values := make(map[string][]interface{}) + values := make(map[string][]any) contentType := header.Get(headerCT) _, params, err := mime.ParseMediaType(contentType) if err != nil { @@ -1453,10 +1453,10 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S } } - var value interface{} + var value any if _, value, err = decodeBody(part, http.Header(part.Header), valueSchema, subEncFn); err != nil { if v, ok := err.(*ParseError); ok { - return nil, &ParseError{path: []interface{}{name}, Cause: v} + return nil, &ParseError{path: []any{name}, Cause: v} } return nil, fmt.Errorf("part %s: %w", name, err) } @@ -1487,7 +1487,7 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S } // Make an object value from form values. - obj := make(map[string]interface{}) + obj := make(map[string]any) for name, prop := range allTheProperties { vv := values[name] if len(vv) == 0 { @@ -1504,7 +1504,7 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S } // FileBodyDecoder is a body decoder that decodes a file body to a string. -func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { +func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (any, error) { data, err := io.ReadAll(body) if err != nil { return nil, err @@ -1513,7 +1513,7 @@ func FileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schema } // zipFileBodyDecoder is a body decoder that decodes a zip file body to a string. -func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { +func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (any, error) { buff := bytes.NewBuffer([]byte{}) size, err := io.Copy(buff, body) if err != nil { @@ -1563,7 +1563,7 @@ func zipFileBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Sch } // csvBodyDecoder is a body decoder that decodes a csv body to a string. -func csvBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { +func csvBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (any, error) { r := csv.NewReader(body) var sb strings.Builder diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 662636b46..6e456d2aa 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -27,7 +27,7 @@ var ( arrayOf = func(items *openapi3.SchemaRef) *openapi3.SchemaRef { return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"array"}, Items: items}} } - objectOf = func(args ...interface{}) *openapi3.SchemaRef { + objectOf = func(args ...any) *openapi3.SchemaRef { s := &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, Properties: make(map[string]*openapi3.SchemaRef)}} if len(args)%2 != 0 { panic("invalid arguments. must be an even number of arguments") @@ -118,25 +118,25 @@ var ( ) func TestDeepGet(t *testing.T) { - iarray := map[string]interface{}{ - "0": map[string]interface{}{ + iarray := map[string]any{ + "0": map[string]any{ "foo": 111, }, - "1": map[string]interface{}{ + "1": map[string]any{ "bar": 222, }, } tests := []struct { name string - m map[string]interface{} + m map[string]any keys []string - expected interface{} + expected any shouldFind bool }{ { name: "Simple map - key exists", - m: map[string]interface{}{ + m: map[string]any{ "foo": "bar", }, keys: []string{"foo"}, @@ -145,8 +145,8 @@ func TestDeepGet(t *testing.T) { }, { name: "Nested map - key exists", - m: map[string]interface{}{ - "foo": map[string]interface{}{ + m: map[string]any{ + "foo": map[string]any{ "bar": "baz", }, }, @@ -156,8 +156,8 @@ func TestDeepGet(t *testing.T) { }, { name: "Nested map - key does not exist", - m: map[string]interface{}{ - "foo": map[string]interface{}{ + m: map[string]any{ + "foo": map[string]any{ "bar": "baz", }, }, @@ -167,8 +167,8 @@ func TestDeepGet(t *testing.T) { }, { name: "Array - element exists", - m: map[string]interface{}{ - "array": map[string]interface{}{"0": "a", "1": "b", "2": "c"}, + m: map[string]any{ + "array": map[string]any{"0": "a", "1": "b", "2": "c"}, }, keys: []string{"array", "1"}, expected: "b", @@ -176,8 +176,8 @@ func TestDeepGet(t *testing.T) { }, { name: "Array - element does not exist - invalid index", - m: map[string]interface{}{ - "array": map[string]interface{}{"0": "a", "1": "b", "2": "c"}, + m: map[string]any{ + "array": map[string]any{"0": "a", "1": "b", "2": "c"}, }, keys: []string{"array", "3"}, expected: nil, @@ -185,8 +185,8 @@ func TestDeepGet(t *testing.T) { }, { name: "Array - element does not exist - invalid keys", - m: map[string]interface{}{ - "array": map[string]interface{}{"0": "a", "1": "b", "2": "c"}, + m: map[string]any{ + "array": map[string]any{"0": "a", "1": "b", "2": "c"}, }, keys: []string{"array", "a", "999"}, expected: nil, @@ -194,7 +194,7 @@ func TestDeepGet(t *testing.T) { }, { name: "Array of objects - element exists 1", - m: map[string]interface{}{ + m: map[string]any{ "array": iarray, }, keys: []string{"array", "1", "bar"}, @@ -203,18 +203,18 @@ func TestDeepGet(t *testing.T) { }, { name: "Array of objects - element exists 2", - m: map[string]interface{}{ + m: map[string]any{ "array": iarray, }, keys: []string{"array", "0"}, - expected: map[string]interface{}{ + expected: map[string]any{ "foo": 111, }, shouldFind: true, }, { name: "Array of objects - element exists 3", - m: map[string]interface{}{ + m: map[string]any{ "array": iarray, }, keys: []string{"array"}, @@ -237,26 +237,26 @@ func TestDeepGet(t *testing.T) { func TestDeepSet(t *testing.T) { tests := []struct { name string - inputMap map[string]interface{} + inputMap map[string]any keys []string - value interface{} - expected map[string]interface{} + value any + expected map[string]any }{ { name: "simple set", - inputMap: map[string]interface{}{}, + inputMap: map[string]any{}, keys: []string{"key"}, value: "value", - expected: map[string]interface{}{"key": "value"}, + expected: map[string]any{"key": "value"}, }, { name: "intermediate array of objects", - inputMap: map[string]interface{}{}, + inputMap: map[string]any{}, keys: []string{"nested", "0", "key"}, value: true, - expected: map[string]interface{}{ - "nested": map[string]interface{}{ - "0": map[string]interface{}{ + expected: map[string]any{ + "nested": map[string]any{ + "0": map[string]any{ "key": true, }, }, @@ -264,12 +264,12 @@ func TestDeepSet(t *testing.T) { }, { name: "existing nested array of objects", - inputMap: map[string]interface{}{"nested": map[string]interface{}{"0": map[string]interface{}{"existingKey": "existingValue"}}}, + inputMap: map[string]any{"nested": map[string]any{"0": map[string]any{"existingKey": "existingValue"}}}, keys: []string{"nested", "0", "newKey"}, value: "newValue", - expected: map[string]interface{}{ - "nested": map[string]interface{}{ - "0": map[string]interface{}{ + expected: map[string]any{ + "nested": map[string]any{ + "0": map[string]any{ "existingKey": "existingValue", "newKey": "newValue", }, @@ -294,7 +294,7 @@ func TestDecodeParameter(t *testing.T) { query string header string cookie string - want interface{} + want any found bool err error } @@ -441,21 +441,21 @@ func TestDecodeParameter(t *testing.T) { name: "simple", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: stringArraySchema}, path: "/foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: stringArraySchema}, path: "/foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "label", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: stringArraySchema}, path: "/.foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -469,7 +469,7 @@ func TestDecodeParameter(t *testing.T) { name: "label explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: stringArraySchema}, path: "/.foo.bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -483,7 +483,7 @@ func TestDecodeParameter(t *testing.T) { name: "matrix", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: stringArraySchema}, path: "/;param=foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -497,7 +497,7 @@ func TestDecodeParameter(t *testing.T) { name: "matrix explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: stringArraySchema}, path: "/;param=foo;param=bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -511,7 +511,7 @@ func TestDecodeParameter(t *testing.T) { name: "default", param: &openapi3.Parameter{Name: "param", In: "path", Schema: stringArraySchema}, path: "/foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -519,21 +519,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(integerSchema)}, path: "/1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(numberSchema)}, path: "/1.1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "param", In: "path", Schema: arrayOf(booleanSchema)}, path: "/true,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, @@ -544,21 +544,21 @@ func TestDecodeParameter(t *testing.T) { name: "simple", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: noExplode, Schema: objectSchema}, path: "/id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "simple", Explode: explode, Schema: objectSchema}, path: "/id=foo,name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "label", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: noExplode, Schema: objectSchema}, path: "/.id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -572,7 +572,7 @@ func TestDecodeParameter(t *testing.T) { name: "label explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "label", Explode: explode, Schema: objectSchema}, path: "/.id=foo.name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -586,7 +586,7 @@ func TestDecodeParameter(t *testing.T) { name: "matrix", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: noExplode, Schema: objectSchema}, path: "/;param=id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -600,7 +600,7 @@ func TestDecodeParameter(t *testing.T) { name: "matrix explode", param: &openapi3.Parameter{Name: "param", In: "path", Style: "matrix", Explode: explode, Schema: objectSchema}, path: "/;id=foo;name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -614,7 +614,7 @@ func TestDecodeParameter(t *testing.T) { name: "default", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectSchema}, path: "/id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -622,21 +622,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", integerSchema)}, path: "/foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", numberSchema)}, path: "/foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "param", In: "path", Schema: objectOf("foo", booleanSchema)}, path: "/foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, @@ -786,49 +786,49 @@ func TestDecodeParameter(t *testing.T) { name: "form", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: stringArraySchema}, query: "param=foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "form explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: stringArraySchema}, query: "param=foo¶m=bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "spaceDelimited", param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: noExplode, Schema: stringArraySchema}, query: "param=foo bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "spaceDelimited explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "spaceDelimited", Explode: explode, Schema: stringArraySchema}, query: "param=foo¶m=bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "pipeDelimited", param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: noExplode, Schema: stringArraySchema}, query: "param=foo|bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "pipeDelimited explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "pipeDelimited", Explode: explode, Schema: stringArraySchema}, query: "param=foo¶m=bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "default", param: &openapi3.Parameter{Name: "param", In: "query", Schema: stringArraySchema}, query: "param=foo¶m=bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -836,21 +836,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(integerSchema)}, query: "param=1¶m=foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(numberSchema)}, query: "param=1.1¶m=foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "param", In: "query", Schema: arrayOf(booleanSchema)}, query: "param=true¶m=foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, @@ -861,21 +861,21 @@ func TestDecodeParameter(t *testing.T) { name: "form", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: noExplode, Schema: objectSchema}, query: "param=id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "form explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "form", Explode: explode, Schema: objectSchema}, query: "id=foo&name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "deepObject explode", param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectSchema}, query: "param[id]=foo¶m[name]=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -888,20 +888,20 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][prop2][item2]=def", - err: &ParseError{path: []interface{}{"obj", "prop2", "item2"}, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}, + err: &ParseError{path: []any{"obj", "prop2", "item2"}, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}, }, { name: "deepObject explode array - missing indexes", param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectOf("items", stringArraySchema)}, query: "param[items]=f%26oo¶m[items]=bar", found: true, - err: &ParseError{path: []interface{}{"items"}, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}, + err: &ParseError{path: []any{"items"}, Kind: KindInvalidFormat, Reason: "array items must be set with indexes"}, }, { name: "deepObject explode array", param: &openapi3.Parameter{Name: "param", In: "query", Style: "deepObject", Explode: explode, Schema: objectOf("items", integerArraySchema)}, query: "param[items][1]=456¶m[items][0]=123", - want: map[string]interface{}{"items": []interface{}{int64(123), int64(456)}}, + want: map[string]any{"items": []any{int64(123), int64(456)}}, found: true, }, { @@ -915,8 +915,8 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][prop1]=bar¶m[obj][prop2]=foo¶m[objTwo]=string", - want: map[string]interface{}{ - "obj": map[string]interface{}{"prop1": "bar", "prop2": "foo"}, + want: map[string]any{ + "obj": map[string]any{"prop1": "bar", "prop2": "foo"}, "objTwo": "string", }, found: true, @@ -931,8 +931,8 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][prop1][item1]=1¶m[obj][prop1][item2]=abc", - want: map[string]interface{}{ - "obj": map[string]interface{}{"prop1": map[string]interface{}{ + want: map[string]any{ + "obj": map[string]any{"prop1": map[string]any{ "item1": int64(1), "item2": "abc", }}, @@ -950,7 +950,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][prop1]=notbool¶m[objTwo]=string", - err: &ParseError{path: []interface{}{"obj", "prop1"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "notbool"}}, + err: &ParseError{path: []any{"obj", "prop1"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "notbool"}}, }, { name: "deepObject explode nested object additionalProperties - bad index inside additionalProperties", @@ -964,10 +964,10 @@ func TestDecodeParameter(t *testing.T) { }, query: "param[obj][prop1]=bar¶m[obj][prop2][badindex]=bad¶m[objTwo]=string", err: &ParseError{ - path: []interface{}{"obj", "prop2"}, + path: []any{"obj", "prop2"}, Reason: `path is not convertible to primitive`, Kind: KindInvalidFormat, - Value: map[string]interface{}{"badindex": "bad"}, + Value: map[string]any{"badindex": "bad"}, }, }, { @@ -981,8 +981,8 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne]=bar¶m[obj][nestedObjTwo]=foo¶m[objTwo]=string", - want: map[string]interface{}{ - "obj": map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, + want: map[string]any{ + "obj": map[string]any{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, "objTwo": "string", }, found: true, @@ -996,7 +996,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "anotherparam=bar", - want: map[string]interface{}(nil), + want: map[string]any(nil), }, { name: "deepObject explode nested object - extraneous deep object param ignored", @@ -1007,7 +1007,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "anotherparam[obj][nestedObjOne]=one&anotherparam[obj][nestedObjTwo]=two", - want: map[string]interface{}(nil), + want: map[string]any(nil), }, { name: "deepObject explode nested object - bad array item type", @@ -1018,7 +1018,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[objTwo][0]=badint", - err: &ParseError{path: []interface{}{"objTwo", "0"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}}, + err: &ParseError{path: []any{"objTwo", "0"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}}, }, { name: "deepObject explode deeply nested object - bad array item type", @@ -1029,7 +1029,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne][0]=badint", - err: &ParseError{path: []interface{}{"obj", "nestedObjOne", "0"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}}, + err: &ParseError{path: []any{"obj", "nestedObjOne", "0"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "badint"}}, }, { name: "deepObject explode deeply nested object - array index not an int", @@ -1040,7 +1040,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne][badindex]=badint", - err: &ParseError{path: []interface{}{"obj", "nestedObjOne"}, Kind: KindInvalidFormat, Reason: "could not convert value map to array: array indexes must be integers: strconv.Atoi: parsing \"badindex\": invalid syntax"}, + err: &ParseError{path: []any{"obj", "nestedObjOne"}, Kind: KindInvalidFormat, Reason: "could not convert value map to array: array indexes must be integers: strconv.Atoi: parsing \"badindex\": invalid syntax"}, }, { name: "deepObject explode nested object with array", @@ -1053,9 +1053,9 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne]=bar¶m[obj][nestedObjTwo]=foo¶m[objTwo][0]=f%26oo¶m[objTwo][1]=bar", - want: map[string]interface{}{ - "obj": map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, - "objTwo": []interface{}{"f%26oo", "bar"}, + want: map[string]any{ + "obj": map[string]any{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, + "objTwo": []any{"f%26oo", "bar"}, }, found: true, }, @@ -1070,7 +1070,7 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne]=bar¶m[obj][nestedObjTwo]=bad¶m[objTwo][0]=f%26oo¶m[objTwo][1]=bar", - err: &ParseError{path: []interface{}{"obj", "nestedObjTwo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bad"}}, + err: &ParseError{path: []any{"obj", "nestedObjTwo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bad"}}, }, { name: "deepObject explode nested object with nested array", @@ -1083,9 +1083,9 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne]=bar¶m[obj][nestedObjTwo]=foo¶m[objTwo][items][0]=f%26oo¶m[objTwo][items][1]=bar", - want: map[string]interface{}{ - "obj": map[string]interface{}{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, - "objTwo": map[string]interface{}{"items": []interface{}{"f%26oo", "bar"}}, + want: map[string]any{ + "obj": map[string]any{"nestedObjOne": "bar", "nestedObjTwo": "foo"}, + "objTwo": map[string]any{"items": []any{"f%26oo", "bar"}}, }, found: true, }, @@ -1099,9 +1099,9 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[obj][nestedObjOne][items][0]=baz¶m[objTwo][items][0]=foo¶m[objTwo][items][1]=bar", - want: map[string]interface{}{ - "obj": map[string]interface{}{"nestedObjOne": map[string]interface{}{"items": []interface{}{"baz"}}}, - "objTwo": map[string]interface{}{"items": []interface{}{"foo", "bar"}}, + want: map[string]any{ + "obj": map[string]any{"nestedObjOne": map[string]any{"items": []any{"baz"}}}, + "objTwo": map[string]any{"items": []any{"foo", "bar"}}, }, found: true, }, @@ -1114,10 +1114,10 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[arr][1][1]=123¶m[arr][1][2]=456", - want: map[string]interface{}{ - "arr": []interface{}{ + want: map[string]any{ + "arr": []any{ nil, - []interface{}{nil, int64(123), int64(456)}, + []any{nil, int64(123), int64(456)}, }, }, found: true, @@ -1131,12 +1131,12 @@ func TestDecodeParameter(t *testing.T) { ), }, query: "param[arr][3][key]=true¶m[arr][0][key]=false", - want: map[string]interface{}{ - "arr": []interface{}{ - map[string]interface{}{"key": false}, + want: map[string]any{ + "arr": []any{ + map[string]any{"key": false}, nil, nil, - map[string]interface{}{"key": true}, + map[string]any{"key": true}, }, }, found: true, @@ -1151,10 +1151,10 @@ func TestDecodeParameter(t *testing.T) { }, query: "param[arr][0][key]=true¶m[arr][1][key]=false", found: true, - want: map[string]interface{}{ - "arr": []interface{}{ - map[string]interface{}{"key": true}, - map[string]interface{}{"key": false}, + want: map[string]any{ + "arr": []any{ + map[string]any{"key": true}, + map[string]any{"key": false}, }, }, }, @@ -1162,7 +1162,7 @@ func TestDecodeParameter(t *testing.T) { name: "default", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectSchema}, query: "id=foo&name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -1170,21 +1170,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", integerSchema)}, query: "foo=bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", numberSchema)}, query: "foo=bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "param", In: "query", Schema: objectOf("foo", booleanSchema)}, query: "foo=bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, @@ -1270,21 +1270,21 @@ func TestDecodeParameter(t *testing.T) { name: "simple", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: stringArraySchema}, header: "X-Param:foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: stringArraySchema}, header: "X-Param:foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: stringArraySchema}, header: "X-Param:foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -1292,21 +1292,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(integerSchema)}, header: "X-Param:1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(numberSchema)}, header: "X-Param:1.1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: arrayOf(booleanSchema)}, header: "X-Param:true,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, @@ -1317,21 +1317,21 @@ func TestDecodeParameter(t *testing.T) { name: "simple", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: noExplode, Schema: objectSchema}, header: "X-Param:id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "simple explode", param: &openapi3.Parameter{Name: "X-Param", In: "header", Style: "simple", Explode: explode, Schema: objectSchema}, header: "X-Param:id=foo,name=bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { name: "default", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectSchema}, header: "X-Param:id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -1346,21 +1346,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", integerSchema)}, header: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", numberSchema)}, header: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "X-Param", In: "header", Schema: objectOf("foo", booleanSchema)}, header: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, @@ -1446,7 +1446,7 @@ func TestDecodeParameter(t *testing.T) { name: "form", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: stringArraySchema}, cookie: "X-Param:foo,bar", - want: []interface{}{"foo", "bar"}, + want: []any{"foo", "bar"}, found: true, }, { @@ -1454,21 +1454,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(integerSchema)}, cookie: "X-Param:1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid number items", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(numberSchema)}, cookie: "X-Param:1.1,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, { name: "invalid boolean items", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: arrayOf(booleanSchema)}, cookie: "X-Param:true,foo", found: true, - err: &ParseError{path: []interface{}{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, + err: &ParseError{path: []any{1}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "foo"}}, }, }, }, @@ -1479,7 +1479,7 @@ func TestDecodeParameter(t *testing.T) { name: "form", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectSchema}, cookie: "X-Param:id,foo,name,bar", - want: map[string]interface{}{"id": "foo", "name": "bar"}, + want: map[string]any{"id": "foo", "name": "bar"}, found: true, }, { @@ -1487,21 +1487,21 @@ func TestDecodeParameter(t *testing.T) { param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", integerSchema)}, cookie: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid number prop", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", numberSchema)}, cookie: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, { name: "invalid boolean prop", param: &openapi3.Parameter{Name: "X-Param", In: "cookie", Style: "form", Explode: noExplode, Schema: objectOf("foo", booleanSchema)}, cookie: "X-Param:foo,bar", found: true, - err: &ParseError{path: []interface{}{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, + err: &ParseError{path: []any{"foo"}, Cause: &ParseError{Kind: KindInvalidFormat, Value: "bar"}}, }, }, }, @@ -1595,7 +1595,7 @@ func TestDecodeBody(t *testing.T) { urlencodedPipeDelim.Set("b", "10") urlencodedPipeDelim.Add("c", "c1|c2") - d, err := json.Marshal(map[string]interface{}{"d1": "d1"}) + d, err := json.Marshal(map[string]any{"d1": "d1"}) require.NoError(t, err) multipartForm, multipartFormMime, err := newTestMultipartForm([]*testFormPart{ {name: "a", contentType: "text/plain", data: strings.NewReader("a1")}, @@ -1639,7 +1639,7 @@ func TestDecodeBody(t *testing.T) { body io.Reader schema *openapi3.Schema encoding map[string]*openapi3.Encoding - want interface{} + want any wantErr error }{ { @@ -1685,7 +1685,7 @@ func TestDecodeBody(t *testing.T) { WithProperty("a", openapi3.NewStringSchema()). WithProperty("b", openapi3.NewIntegerSchema()). WithProperty("c", openapi3.NewArraySchema().WithItems(openapi3.NewStringSchema())), - want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}}, + want: map[string]any{"a": "a1", "b": int64(10), "c": []any{"c1", "c2"}}, }, { name: "urlencoded space delimited", @@ -1698,7 +1698,7 @@ func TestDecodeBody(t *testing.T) { encoding: map[string]*openapi3.Encoding{ "c": {Style: openapi3.SerializationSpaceDelimited, Explode: openapi3.BoolPtr(false)}, }, - want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}}, + want: map[string]any{"a": "a1", "b": int64(10), "c": []any{"c1", "c2"}}, }, { name: "urlencoded pipe delimited", @@ -1711,7 +1711,7 @@ func TestDecodeBody(t *testing.T) { encoding: map[string]*openapi3.Encoding{ "c": {Style: openapi3.SerializationPipeDelimited, Explode: openapi3.BoolPtr(false)}, }, - want: map[string]interface{}{"a": "a1", "b": int64(10), "c": []interface{}{"c1", "c2"}}, + want: map[string]any{"a": "a1", "b": int64(10), "c": []any{"c1", "c2"}}, }, { name: "multipart", @@ -1724,7 +1724,7 @@ func TestDecodeBody(t *testing.T) { WithProperty("d", openapi3.NewObjectSchema().WithProperty("d1", openapi3.NewStringSchema())). WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")). WithProperty("g", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1", "b": json.Number("10"), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo", "g": "g1"}, + want: map[string]any{"a": "a1", "b": json.Number("10"), "c": []any{"c1", "c2"}, "d": map[string]any{"d1": "d1"}, "f": "foo", "g": "g1"}, }, { name: "multipartExtraPart", @@ -1732,7 +1732,7 @@ func TestDecodeBody(t *testing.T) { body: multipartFormExtraPart, schema: openapi3.NewObjectSchema(). WithProperty("a", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1"}, + want: map[string]any{"a": "a1"}, wantErr: &ParseError{Kind: KindOther}, }, { @@ -1742,7 +1742,7 @@ func TestDecodeBody(t *testing.T) { schema: openapi3.NewObjectSchema(). WithAnyAdditionalProperties(). WithProperty("a", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1"}, + want: map[string]any{"a": "a1"}, }, { name: "multipartWithAdditionalProperties", @@ -1752,7 +1752,7 @@ func TestDecodeBody(t *testing.T) { WithAdditionalProperties(openapi3.NewObjectSchema(). WithProperty("x", openapi3.NewStringSchema())). WithProperty("a", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1", "x": "x1"}, + want: map[string]any{"a": "a1", "x": "x1"}, }, { name: "multipartWithAdditionalPropertiesError", @@ -1762,7 +1762,7 @@ func TestDecodeBody(t *testing.T) { WithAdditionalProperties(openapi3.NewObjectSchema(). WithProperty("x", openapi3.NewStringSchema())). WithProperty("a", openapi3.NewStringSchema()), - want: map[string]interface{}{"a": "a1", "x": "x1"}, + want: map[string]any{"a": "a1", "x": "x1"}, wantErr: &ParseError{Kind: KindOther}, }, { @@ -1836,7 +1836,7 @@ func newTestMultipartForm(parts []*testFormPart) (io.Reader, string, error) { func TestRegisterAndUnregisterBodyDecoder(t *testing.T) { var decoder BodyDecoder - decoder = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (decoded interface{}, err error) { + decoder = func(body io.Reader, h http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (decoded any, err error) { var data []byte if data, err = io.ReadAll(body); err != nil { return diff --git a/openapi3filter/req_resp_encoder.go b/openapi3filter/req_resp_encoder.go index 2ec946afb..5c328c756 100644 --- a/openapi3filter/req_resp_encoder.go +++ b/openapi3filter/req_resp_encoder.go @@ -6,7 +6,7 @@ import ( "sync" ) -func encodeBody(body interface{}, mediaType string) ([]byte, error) { +func encodeBody(body any, mediaType string) ([]byte, error) { if encoder := RegisteredBodyEncoder(mediaType); encoder != nil { return encoder(body) } @@ -17,7 +17,7 @@ func encodeBody(body interface{}, mediaType string) ([]byte, error) { } // BodyEncoder really is an (encoding/json).Marshaler -type BodyEncoder func(body interface{}) ([]byte, error) +type BodyEncoder func(body any) ([]byte, error) var bodyEncodersM sync.RWMutex var bodyEncoders = map[string]BodyEncoder{ diff --git a/openapi3filter/req_resp_encoder_test.go b/openapi3filter/req_resp_encoder_test.go index 11fe2afa9..3b7e6a754 100644 --- a/openapi3filter/req_resp_encoder_test.go +++ b/openapi3filter/req_resp_encoder_test.go @@ -11,7 +11,7 @@ import ( func TestRegisterAndUnregisterBodyEncoder(t *testing.T) { var encoder BodyEncoder - encoder = func(body interface{}) (data []byte, err error) { + encoder = func(body any) (data []byte, err error) { return []byte(strings.Join(body.([]string), ",")), nil } contentType := "text/csv" diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go index 7a8b5ca11..f21060a99 100644 --- a/openapi3filter/validate_request.go +++ b/openapi3filter/validate_request.go @@ -121,7 +121,7 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param options = &Options{} } - var value interface{} + var value any var err error var found bool var schema *openapi3.Schema diff --git a/openapi3filter/validate_request_input.go b/openapi3filter/validate_request_input.go index 91dd102b6..c7565ebb7 100644 --- a/openapi3filter/validate_request_input.go +++ b/openapi3filter/validate_request_input.go @@ -17,7 +17,7 @@ import ( // If a query parameter appears multiple times, values[] will have more // than one value, but for all other parameter types it should have just // one. -type ContentParameterDecoder func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error) +type ContentParameterDecoder func(param *openapi3.Parameter, values []string) (any, *openapi3.Schema, error) type RequestValidationInput struct { Request *http.Request diff --git a/openapi3filter/validate_request_test.go b/openapi3filter/validate_request_test.go index 14f568d8a..984af4c21 100644 --- a/openapi3filter/validate_request_test.go +++ b/openapi3filter/validate_request_test.go @@ -228,7 +228,7 @@ func TestValidateQueryParams(t *testing.T) { name string param *openapi3.Parameter query string - want map[string]interface{} + want map[string]any err *openapi3.SchemaError // test ParseError in decoder tests } @@ -267,9 +267,9 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj][prop1][inexistent]=1", - want: map[string]interface{}{ - "obj": map[string]interface{}{ - "prop1": map[string]interface{}{}, + want: map[string]any{ + "obj": map[string]any{ + "prop1": map[string]any{}, }, }, }, @@ -286,9 +286,9 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj][prop1][item1]=1.123", - want: map[string]interface{}{ - "obj": map[string]interface{}{ - "prop1": map[string]interface{}{ + want: map[string]any{ + "obj": map[string]any{ + "prop1": map[string]any{ "item1": float64(1.123), }, }, @@ -316,7 +316,7 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "anotherparam=bar", - want: map[string]interface{}(nil), + want: map[string]any(nil), }, { name: "deepObject explode additionalProperties with object properties - multiple properties", @@ -328,15 +328,15 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj][prop1][item1]=1¶m[obj][prop1][item2][0]=abc¶m[obj][prop2][item1]=2¶m[obj][prop2][item2][0]=def", - want: map[string]interface{}{ - "obj": map[string]interface{}{ - "prop1": map[string]interface{}{ + want: map[string]any{ + "obj": map[string]any{ + "prop1": map[string]any{ "item1": int64(1), - "item2": []interface{}{"abc"}, + "item2": []any{"abc"}, }, - "prop2": map[string]interface{}{ + "prop2": map[string]any{ "item1": int64(2), - "item2": []interface{}{"def"}, + "item2": []any{"def"}, }, }, }, @@ -353,7 +353,7 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj]=1", - want: map[string]interface{}{ + want: map[string]any{ "obj": int64(1), }, }, @@ -366,7 +366,7 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj]=1", - want: map[string]interface{}{ + want: map[string]any{ "obj": int64(1), }, }, @@ -379,7 +379,7 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj]=true", - want: map[string]interface{}{ + want: map[string]any{ "obj": true, }, }, @@ -392,8 +392,8 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj][id2]=1¶m[obj][name2]=abc", - want: map[string]interface{}{ - "obj": map[string]interface{}{ + want: map[string]any{ + "obj": map[string]any{ "id2": "1", "name2": "abc", }, @@ -410,7 +410,7 @@ func TestValidateQueryParams(t *testing.T) { query: "param[obj][id]=1¶m[obj][id2]=2", err: &openapi3.SchemaError{ SchemaField: "oneOf", - Value: map[string]interface{}{"id": "1", "id2": "2"}, + Value: map[string]any{"id": "1", "id2": "2"}, Reason: "value matches more than one schema from \"oneOf\" (matches schemas at indices [0 1])", Schema: oneofSchemaObject.Value, }, @@ -424,8 +424,8 @@ func TestValidateQueryParams(t *testing.T) { ), }, query: "param[obj][0]=a¶m[obj][1]=b", - want: map[string]interface{}{ - "obj": []interface{}{ + want: map[string]any{ + "obj": []any{ "a", "b", }, diff --git a/openapi3filter/validate_response.go b/openapi3filter/validate_response.go index 87a8a1119..77f127e73 100644 --- a/openapi3filter/validate_response.go +++ b/openapi3filter/validate_response.go @@ -162,7 +162,7 @@ func ValidateResponse(ctx context.Context, input *ResponseValidationInput) error func validateResponseHeader(headerName string, headerRef *openapi3.HeaderRef, input *ResponseValidationInput, opts []openapi3.SchemaValidationOption) error { var err error - var decodedValue interface{} + var decodedValue any var found bool var sm *openapi3.SerializationMethod dec := &headerParamDecoder{header: input.Header} diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go index f8fd6db8b..6e2bbd294 100644 --- a/openapi3filter/validation_error_test.go +++ b/openapi3filter/validation_error_test.go @@ -43,14 +43,14 @@ type validationTest struct { wantErrReason string wantErrSchemaReason string wantErrSchemaPath string - wantErrSchemaValue interface{} + wantErrSchemaValue any wantErrSchemaOriginReason string wantErrSchemaOriginPath string - wantErrSchemaOriginValue interface{} + wantErrSchemaOriginValue any wantErrParam string wantErrParamIn string wantErrParseKind ParseErrorKind - wantErrParseValue interface{} + wantErrParseValue any wantErrParseReason string wantErrResponse *ValidationError } diff --git a/openapi3filter/validation_test.go b/openapi3filter/validation_test.go index 85ff0dc7a..7f78b117b 100644 --- a/openapi3filter/validation_test.go +++ b/openapi3filter/validation_test.go @@ -23,13 +23,13 @@ type ExampleRequest struct { Method string URL string ContentType string - Body interface{} + Body any } type ExampleResponse struct { Status int ContentType string - Body interface{} + Body any } type ExampleSecurityScheme struct { @@ -333,8 +333,8 @@ func TestFilter(t *testing.T) { require.IsType(t, &RequestError{}, err) // Now, repeat the above two test cases using a custom parameter decoder. - customDecoder := func(param *openapi3.Parameter, values []string) (interface{}, *openapi3.Schema, error) { - var value interface{} + customDecoder := func(param *openapi3.Parameter, values []string) (any, *openapi3.Schema, error) { + var value any err := json.Unmarshal([]byte(values[0]), &value) schema := param.Content.Get("application/something_funny").Schema.Value return value, schema, err @@ -356,7 +356,7 @@ func TestFilter(t *testing.T) { require.IsType(t, &RequestError{}, err) } -func marshalReader(value interface{}) io.ReadCloser { +func marshalReader(value any) io.ReadCloser { if value == nil { return nil } @@ -464,7 +464,7 @@ func matchReqBodyError(want, got error) bool { return false } -func toJSON(v interface{}) io.Reader { +func toJSON(v any) io.Reader { data, err := json.Marshal(v) if err != nil { panic(err) diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index 6afed0033..9eae7ccbb 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -89,7 +89,7 @@ func CreateComponentSchemas(exso ExportComponentSchemasOptions) Option { } // NewSchemaRefForValue is a shortcut for NewGenerator(...).NewSchemaRefForValue(...) -func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) { +func NewSchemaRefForValue(value any, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) { g := NewGenerator(opts...) return g.NewSchemaRefForValue(value, schemas) } @@ -128,7 +128,7 @@ func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, erro } // NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef, and updates a supplied map with any dependent component schemas if they lead to cycles -func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas) (*openapi3.SchemaRef, error) { +func (g *Generator) NewSchemaRefForValue(value any, schemas openapi3.Schemas) (*openapi3.SchemaRef, error) { ref, err := g.GenerateSchemaRef(reflect.TypeOf(value)) if err != nil { return nil, err diff --git a/openapi3gen/openapi3gen_newschemarefforvalue_test.go b/openapi3gen/openapi3gen_newschemarefforvalue_test.go index 1c46f2320..ada3d8860 100644 --- a/openapi3gen/openapi3gen_newschemarefforvalue_test.go +++ b/openapi3gen/openapi3gen_newschemarefforvalue_test.go @@ -366,10 +366,10 @@ func ExampleNewSchemaRefForValue_withExportingSchemasWithMap() { Age string `json:"age"` } type MyType struct { - Field1 string `json:"field1"` - Field2 string `json:"field2"` - Map1 map[string]interface{} `json:"anymap"` - Map2 map[string]Child `json:"anymapChild"` + Field1 string `json:"field1"` + Field2 string `json:"field2"` + Map1 map[string]any `json:"anymap"` + Map2 map[string]Child `json:"anymapChild"` } schemas := make(openapi3.Schemas) diff --git a/routers/legacy/pathpattern/node.go b/routers/legacy/pathpattern/node.go index 3f197df95..75932a26d 100644 --- a/routers/legacy/pathpattern/node.go +++ b/routers/legacy/pathpattern/node.go @@ -55,7 +55,7 @@ func PathFromHost(host string, specialDashes bool) string { type Node struct { VariableNames []string - Value interface{} + Value any Suffixes SuffixList } @@ -153,7 +153,7 @@ func (list SuffixList) Swap(i, j int) { list[i], list[j] = b, a } -func (currentNode *Node) MustAdd(path string, value interface{}, options *Options) { +func (currentNode *Node) MustAdd(path string, value any, options *Options) { node, err := currentNode.CreateNode(path, options) if err != nil { panic(err) @@ -161,7 +161,7 @@ func (currentNode *Node) MustAdd(path string, value interface{}, options *Option node.Value = value } -func (currentNode *Node) Add(path string, value interface{}, options *Options) error { +func (currentNode *Node) Add(path string, value any, options *Options) error { node, err := currentNode.CreateNode(path, options) if err != nil { return err diff --git a/routers/legacy/validate_request_test.go b/routers/legacy/validate_request_test.go index 9c15ed44a..77d8bc1de 100644 --- a/routers/legacy/validate_request_test.go +++ b/routers/legacy/validate_request_test.go @@ -78,7 +78,7 @@ func Example() { panic(err) } - p, err := json.Marshal(map[string]interface{}{ + p, err := json.Marshal(map[string]any{ "pet_type": "Cat", "breed": "Dingo", "bark": true, pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy