go.mod: bump github.com/getkin/kin-openapi to v0.131.0

As deepmap/oapi-codegen didn't work with this newer version, upgrade to
oapi-codegen/oapi-codegen v2.

Mitigating CVE-2025-30153
This commit is contained in:
Sanne Raymaekers 2025-03-21 11:50:30 +01:00 committed by Ondřej Budai
parent c5cb0d0618
commit b2700903ae
403 changed files with 44758 additions and 16347 deletions

View file

@ -2,36 +2,60 @@ package openapi3
import (
"context"
"fmt"
"github.com/go-openapi/jsonpointer"
"sort"
)
type Callbacks map[string]*CallbackRef
// 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]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
var _ jsonpointer.JSONPointable = (*Callbacks)(nil)
func (c Callbacks) JSONLookup(token string) (interface{}, error) {
ref, ok := c[token]
if ref == nil || !ok {
return nil, fmt.Errorf("object has no field %q", token)
}
if ref.Ref != "" {
return &Ref{Ref: ref.Ref}, nil
}
return ref.Value, nil
m map[string]*PathItem
}
// Callback is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#callbackObject
type Callback map[string]*PathItem
// NewCallback builds a Callback object with path items in insertion order.
func NewCallback(opts ...NewCallbackOption) *Callback {
Callback := NewCallbackWithCapacity(len(opts))
for _, opt := range opts {
opt(Callback)
}
return Callback
}
func (value Callback) Validate(ctx context.Context) error {
for _, v := range value {
// NewCallbackOption describes options to NewCallback func
type NewCallbackOption func(*Callback)
// WithCallback adds Callback as an option to NewCallback
func WithCallback(cb string, pathItem *PathItem) NewCallbackOption {
return func(callback *Callback) {
if p := pathItem; p != nil && cb != "" {
callback.Set(cb, p)
}
}
}
// Validate returns an error if Callback does not comply with the OpenAPI spec.
func (callback *Callback) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
keys := make([]string, 0, callback.Len())
for key := range callback.Map() {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
v := callback.Value(key)
if err := v.Validate(ctx); err != nil {
return err
}
}
return nil
return validateExtensions(ctx, callback.Extensions)
}
// UnmarshalJSON sets Callbacks to a copy of data.
func (callbacks *Callbacks) UnmarshalJSON(data []byte) (err error) {
*callbacks, _, err = unmarshalStringMapP[CallbackRef](data)
return
}

View file

@ -2,22 +2,36 @@ package openapi3
import (
"context"
"encoding/json"
"fmt"
"regexp"
"sort"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
)
type (
Callbacks map[string]*CallbackRef
Examples map[string]*ExampleRef
Headers map[string]*HeaderRef
Links map[string]*LinkRef
ParametersMap map[string]*ParameterRef
RequestBodies map[string]*RequestBodyRef
ResponseBodies map[string]*ResponseRef
Schemas map[string]*SchemaRef
SecuritySchemes map[string]*SecuritySchemeRef
)
// Components is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#components-object
type Components struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"`
Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"`
Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"`
RequestBodies RequestBodies `json:"requestBodies,omitempty" yaml:"requestBodies,omitempty"`
Responses Responses `json:"responses,omitempty" yaml:"responses,omitempty"`
Responses ResponseBodies `json:"responses,omitempty" yaml:"responses,omitempty"`
SecuritySchemes SecuritySchemes `json:"securitySchemes,omitempty" yaml:"securitySchemes,omitempty"`
Examples Examples `json:"examples,omitempty" yaml:"examples,omitempty"`
Links Links `json:"links,omitempty" yaml:"links,omitempty"`
@ -28,82 +42,331 @@ func NewComponents() Components {
return Components{}
}
func (components *Components) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(components)
// MarshalJSON returns the JSON encoding of Components.
func (components Components) MarshalJSON() ([]byte, error) {
x, err := components.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of Components.
func (components Components) MarshalYAML() (any, error) {
m := make(map[string]any, 9+len(components.Extensions))
for k, v := range components.Extensions {
m[k] = v
}
if x := components.Schemas; len(x) != 0 {
m["schemas"] = x
}
if x := components.Parameters; len(x) != 0 {
m["parameters"] = x
}
if x := components.Headers; len(x) != 0 {
m["headers"] = x
}
if x := components.RequestBodies; len(x) != 0 {
m["requestBodies"] = x
}
if x := components.Responses; len(x) != 0 {
m["responses"] = x
}
if x := components.SecuritySchemes; len(x) != 0 {
m["securitySchemes"] = x
}
if x := components.Examples; len(x) != 0 {
m["examples"] = x
}
if x := components.Links; len(x) != 0 {
m["links"] = x
}
if x := components.Callbacks; len(x) != 0 {
m["callbacks"] = x
}
return m, nil
}
// UnmarshalJSON sets Components to a copy of data.
func (components *Components) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, components)
type ComponentsBis Components
var x ComponentsBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "schemas")
delete(x.Extensions, "parameters")
delete(x.Extensions, "headers")
delete(x.Extensions, "requestBodies")
delete(x.Extensions, "responses")
delete(x.Extensions, "securitySchemes")
delete(x.Extensions, "examples")
delete(x.Extensions, "links")
delete(x.Extensions, "callbacks")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*components = Components(x)
return nil
}
func (components *Components) Validate(ctx context.Context) (err error) {
for k, v := range components.Schemas {
// Validate returns an error if Components does not comply with the OpenAPI spec.
func (components *Components) Validate(ctx context.Context, opts ...ValidationOption) (err error) {
ctx = WithValidationOptions(ctx, opts...)
schemas := make([]string, 0, len(components.Schemas))
for name := range components.Schemas {
schemas = append(schemas, name)
}
sort.Strings(schemas)
for _, k := range schemas {
v := components.Schemas[k]
if err = ValidateIdentifier(k); err != nil {
return
return fmt.Errorf("schema %q: %w", k, err)
}
if err = v.Validate(ctx); err != nil {
return
return fmt.Errorf("schema %q: %w", k, err)
}
}
for k, v := range components.Parameters {
parameters := make([]string, 0, len(components.Parameters))
for name := range components.Parameters {
parameters = append(parameters, name)
}
sort.Strings(parameters)
for _, k := range parameters {
v := components.Parameters[k]
if err = ValidateIdentifier(k); err != nil {
return
return fmt.Errorf("parameter %q: %w", k, err)
}
if err = v.Validate(ctx); err != nil {
return
return fmt.Errorf("parameter %q: %w", k, err)
}
}
for k, v := range components.RequestBodies {
requestBodies := make([]string, 0, len(components.RequestBodies))
for name := range components.RequestBodies {
requestBodies = append(requestBodies, name)
}
sort.Strings(requestBodies)
for _, k := range requestBodies {
v := components.RequestBodies[k]
if err = ValidateIdentifier(k); err != nil {
return
return fmt.Errorf("request body %q: %w", k, err)
}
if err = v.Validate(ctx); err != nil {
return
return fmt.Errorf("request body %q: %w", k, err)
}
}
for k, v := range components.Responses {
responses := make([]string, 0, len(components.Responses))
for name := range components.Responses {
responses = append(responses, name)
}
sort.Strings(responses)
for _, k := range responses {
if err = ValidateIdentifier(k); err != nil {
return
return fmt.Errorf("response %q: %w", k, err)
}
v := components.Responses[k]
if err = v.Validate(ctx); err != nil {
return
return fmt.Errorf("response %q: %w", k, err)
}
}
for k, v := range components.Headers {
headers := make([]string, 0, len(components.Headers))
for name := range components.Headers {
headers = append(headers, name)
}
sort.Strings(headers)
for _, k := range headers {
v := components.Headers[k]
if err = ValidateIdentifier(k); err != nil {
return
return fmt.Errorf("header %q: %w", k, err)
}
if err = v.Validate(ctx); err != nil {
return
return fmt.Errorf("header %q: %w", k, err)
}
}
for k, v := range components.SecuritySchemes {
securitySchemes := make([]string, 0, len(components.SecuritySchemes))
for name := range components.SecuritySchemes {
securitySchemes = append(securitySchemes, name)
}
sort.Strings(securitySchemes)
for _, k := range securitySchemes {
v := components.SecuritySchemes[k]
if err = ValidateIdentifier(k); err != nil {
return
return fmt.Errorf("security scheme %q: %w", k, err)
}
if err = v.Validate(ctx); err != nil {
return
return fmt.Errorf("security scheme %q: %w", k, err)
}
}
return
examples := make([]string, 0, len(components.Examples))
for name := range components.Examples {
examples = append(examples, name)
}
sort.Strings(examples)
for _, k := range examples {
v := components.Examples[k]
if err = ValidateIdentifier(k); err != nil {
return fmt.Errorf("example %q: %w", k, err)
}
if err = v.Validate(ctx); err != nil {
return fmt.Errorf("example %q: %w", k, err)
}
}
links := make([]string, 0, len(components.Links))
for name := range components.Links {
links = append(links, name)
}
sort.Strings(links)
for _, k := range links {
v := components.Links[k]
if err = ValidateIdentifier(k); err != nil {
return fmt.Errorf("link %q: %w", k, err)
}
if err = v.Validate(ctx); err != nil {
return fmt.Errorf("link %q: %w", k, err)
}
}
callbacks := make([]string, 0, len(components.Callbacks))
for name := range components.Callbacks {
callbacks = append(callbacks, name)
}
sort.Strings(callbacks)
for _, k := range callbacks {
v := components.Callbacks[k]
if err = ValidateIdentifier(k); err != nil {
return fmt.Errorf("callback %q: %w", k, err)
}
if err = v.Validate(ctx); err != nil {
return fmt.Errorf("callback %q: %w", k, err)
}
}
return validateExtensions(ctx, components.Extensions)
}
const identifierPattern = `^[a-zA-Z0-9._-]+$`
var _ jsonpointer.JSONPointable = (*Schemas)(nil)
// IdentifierRegExp verifies whether Component object key matches 'identifierPattern' pattern, according to OapiAPI v3.x.0.
// Hovever, to be able supporting legacy OpenAPI v2.x, there is a need to customize above pattern in orde not to fail
// converted v2-v3 validation
var IdentifierRegExp = regexp.MustCompile(identifierPattern)
func ValidateIdentifier(value string) error {
if IdentifierRegExp.MatchString(value) {
return nil
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
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 != "" {
return &Ref{Ref: ref}, nil
} else {
return v.Value, nil
}
}
var _ jsonpointer.JSONPointable = (*ParametersMap)(nil)
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
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 != "" {
return &Ref{Ref: ref}, nil
} else {
return v.Value, nil
}
}
var _ jsonpointer.JSONPointable = (*Headers)(nil)
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
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 != "" {
return &Ref{Ref: ref}, nil
} else {
return v.Value, nil
}
}
var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil)
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
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 != "" {
return &Ref{Ref: ref}, nil
} else {
return v.Value, nil
}
}
var _ jsonpointer.JSONPointable = (*ResponseRef)(nil)
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
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 != "" {
return &Ref{Ref: ref}, nil
} else {
return v.Value, nil
}
}
var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil)
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
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 != "" {
return &Ref{Ref: ref}, nil
} else {
return v.Value, nil
}
}
var _ jsonpointer.JSONPointable = (*Examples)(nil)
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
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 != "" {
return &Ref{Ref: ref}, nil
} else {
return v.Value, nil
}
}
var _ jsonpointer.JSONPointable = (*Links)(nil)
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
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 != "" {
return &Ref{Ref: ref}, nil
} else {
return v.Value, nil
}
}
var _ jsonpointer.JSONPointable = (*Callbacks)(nil)
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
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 != "" {
return &Ref{Ref: ref}, nil
} else {
return v.Value, nil
}
return fmt.Errorf("identifier %q is not supported by OpenAPIv3 standard (regexp: %q)", value, identifierPattern)
}

View file

@ -0,0 +1,71 @@
package openapi3
import (
"context"
"encoding/json"
)
// 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]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
Email string `json:"email,omitempty" yaml:"email,omitempty"`
}
// MarshalJSON returns the JSON encoding of Contact.
func (contact Contact) MarshalJSON() ([]byte, error) {
x, err := contact.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of Contact.
func (contact Contact) MarshalYAML() (any, error) {
m := make(map[string]any, 3+len(contact.Extensions))
for k, v := range contact.Extensions {
m[k] = v
}
if x := contact.Name; x != "" {
m["name"] = x
}
if x := contact.URL; x != "" {
m["url"] = x
}
if x := contact.Email; x != "" {
m["email"] = x
}
return m, nil
}
// UnmarshalJSON sets Contact to a copy of data.
func (contact *Contact) UnmarshalJSON(data []byte) error {
type ContactBis Contact
var x ContactBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "name")
delete(x.Extensions, "url")
delete(x.Extensions, "email")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*contact = Contact(x)
return nil
}
// Validate returns an error if Contact does not comply with the OpenAPI spec.
func (contact *Contact) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
return validateExtensions(ctx, contact.Extensions)
}

View file

@ -2,6 +2,7 @@ package openapi3
import (
"context"
"sort"
"strings"
)
@ -9,7 +10,7 @@ import (
type Content map[string]*MediaType
func NewContent() Content {
return make(map[string]*MediaType, 4)
return make(map[string]*MediaType)
}
func NewContentWithSchema(schema *Schema, consumes []string) Content {
@ -104,12 +105,26 @@ func (content Content) Get(mime string) *MediaType {
return content["*/*"]
}
func (value Content) Validate(ctx context.Context) error {
for _, v := range value {
// Validate MediaType
// Validate returns an error if Content does not comply with the OpenAPI spec.
func (content Content) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
keys := make([]string, 0, len(content))
for key := range content {
keys = append(keys, key)
}
sort.Strings(keys)
for _, k := range keys {
v := content[k]
if err := v.Validate(ctx); err != nil {
return err
}
}
return nil
}
// UnmarshalJSON sets Content to a copy of data.
func (content *Content) UnmarshalJSON(data []byte) (err error) {
*content, _, err = unmarshalStringMapP[MediaType](data)
return
}

View file

@ -2,27 +2,63 @@ package openapi3
import (
"context"
"github.com/getkin/kin-openapi/jsoninfo"
"encoding/json"
)
// Discriminator is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminatorObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object
type Discriminator struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
PropertyName string `json:"propertyName" yaml:"propertyName"`
Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"`
PropertyName string `json:"propertyName" yaml:"propertyName"` // required
Mapping StringMap `json:"mapping,omitempty" yaml:"mapping,omitempty"`
}
func (value *Discriminator) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(value)
// MarshalJSON returns the JSON encoding of Discriminator.
func (discriminator Discriminator) MarshalJSON() ([]byte, error) {
x, err := discriminator.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
func (value *Discriminator) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, value)
// MarshalYAML returns the YAML encoding of Discriminator.
func (discriminator Discriminator) MarshalYAML() (any, error) {
m := make(map[string]any, 2+len(discriminator.Extensions))
for k, v := range discriminator.Extensions {
m[k] = v
}
m["propertyName"] = discriminator.PropertyName
if x := discriminator.Mapping; len(x) != 0 {
m["mapping"] = x
}
return m, nil
}
func (value *Discriminator) Validate(ctx context.Context) error {
// UnmarshalJSON sets Discriminator to a copy of data.
func (discriminator *Discriminator) UnmarshalJSON(data []byte) error {
type DiscriminatorBis Discriminator
var x DiscriminatorBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "propertyName")
delete(x.Extensions, "mapping")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*discriminator = Discriminator(x)
return nil
}
// Validate returns an error if Discriminator does not comply with the OpenAPI spec.
func (discriminator *Discriminator) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
return validateExtensions(ctx, discriminator.Extensions)
}

View file

@ -1,4 +1,4 @@
// Package openapi3 parses and writes OpenAPI 3 specification documents.
//
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md
// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md
package openapi3

View file

@ -2,15 +2,16 @@ package openapi3
import (
"context"
"encoding/json"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
"sort"
)
// Encoding is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encodingObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encoding-object
type Encoding struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
ContentType string `json:"contentType,omitempty" yaml:"contentType,omitempty"`
Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"`
@ -39,12 +40,59 @@ func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding {
return encoding
}
func (encoding *Encoding) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(encoding)
// MarshalJSON returns the JSON encoding of Encoding.
func (encoding Encoding) MarshalJSON() ([]byte, error) {
x, err := encoding.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of Encoding.
func (encoding Encoding) MarshalYAML() (any, error) {
m := make(map[string]any, 5+len(encoding.Extensions))
for k, v := range encoding.Extensions {
m[k] = v
}
if x := encoding.ContentType; x != "" {
m["contentType"] = x
}
if x := encoding.Headers; len(x) != 0 {
m["headers"] = x
}
if x := encoding.Style; x != "" {
m["style"] = x
}
if x := encoding.Explode; x != nil {
m["explode"] = x
}
if x := encoding.AllowReserved; x {
m["allowReserved"] = x
}
return m, nil
}
// UnmarshalJSON sets Encoding to a copy of data.
func (encoding *Encoding) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, encoding)
type EncodingBis Encoding
var x EncodingBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "contentType")
delete(x.Extensions, "headers")
delete(x.Extensions, "style")
delete(x.Extensions, "explode")
delete(x.Extensions, "allowReserved")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*encoding = Encoding(x)
return nil
}
// SerializationMethod returns a serialization method of request body.
@ -62,11 +110,21 @@ func (encoding *Encoding) SerializationMethod() *SerializationMethod {
return sm
}
func (value *Encoding) Validate(ctx context.Context) error {
if value == nil {
// Validate returns an error if Encoding does not comply with the OpenAPI spec.
func (encoding *Encoding) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if encoding == nil {
return nil
}
for k, v := range value.Headers {
headers := make([]string, 0, len(encoding.Headers))
for k := range encoding.Headers {
headers = append(headers, k)
}
sort.Strings(headers)
for _, k := range headers {
v := encoding.Headers[k]
if err := ValidateIdentifier(k); err != nil {
return nil
}
@ -76,7 +134,7 @@ func (value *Encoding) Validate(ctx context.Context) error {
}
// Validate a media types's serialization method.
sm := value.SerializationMethod()
sm := encoding.SerializationMethod()
switch {
case sm.Style == SerializationForm && sm.Explode,
sm.Style == SerializationForm && !sm.Explode,
@ -85,10 +143,9 @@ func (value *Encoding) Validate(ctx context.Context) error {
sm.Style == SerializationPipeDelimited && sm.Explode,
sm.Style == SerializationPipeDelimited && !sm.Explode,
sm.Style == SerializationDeepObject && sm.Explode:
// it is a valid
default:
return fmt.Errorf("serialization method with style=%q and explode=%v is not supported by media type", sm.Style, sm.Explode)
}
return nil
return validateExtensions(ctx, encoding.Extensions)
}

View file

@ -10,16 +10,22 @@ import (
type MultiError []error
func (me MultiError) Error() string {
return spliceErr(" | ", me)
}
func spliceErr(sep string, errs []error) string {
buff := &bytes.Buffer{}
for _, e := range me {
for i, e := range errs {
buff.WriteString(e.Error())
buff.WriteString(" | ")
if i != len(errs)-1 {
buff.WriteString(sep)
}
}
return buff.String()
}
//Is allows you to determine if a generic error is in fact a MultiError using `errors.Is()`
//It will also return true if any of the contained errors match target
// Is allows you to determine if a generic error is in fact a MultiError using `errors.Is()`
// It will also return true if any of the contained errors match target
func (me MultiError) Is(target error) bool {
if _, ok := target.(MultiError); ok {
return true
@ -32,8 +38,8 @@ func (me MultiError) Is(target error) bool {
return false
}
//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 {
// 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 any) bool {
for _, e := range me {
if errors.As(e, target) {
return true
@ -41,3 +47,13 @@ func (me MultiError) As(target interface{}) bool {
}
return false
}
type multiErrorForOneOf MultiError
func (meo multiErrorForOneOf) Error() string {
return spliceErr(" Or ", meo)
}
func (meo multiErrorForOneOf) Unwrap() error {
return MultiError(meo)
}

View file

@ -2,53 +2,92 @@ package openapi3
import (
"context"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
"encoding/json"
"errors"
)
type Examples map[string]*ExampleRef
var _ jsonpointer.JSONPointable = (*Examples)(nil)
func (e Examples) JSONLookup(token string) (interface{}, error) {
ref, ok := e[token]
if ref == nil || !ok {
return nil, fmt.Errorf("object has no field %q", token)
}
if ref.Ref != "" {
return &Ref{Ref: ref.Ref}, nil
}
return ref.Value, nil
}
// Example is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#exampleObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#example-object
type Example struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
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 {
return &Example{
Value: value,
func NewExample(value any) *Example {
return &Example{Value: value}
}
// MarshalJSON returns the JSON encoding of Example.
func (example Example) MarshalJSON() ([]byte, error) {
x, err := example.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
func (example *Example) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(example)
// MarshalYAML returns the YAML encoding of Example.
func (example Example) MarshalYAML() (any, error) {
m := make(map[string]any, 4+len(example.Extensions))
for k, v := range example.Extensions {
m[k] = v
}
if x := example.Summary; x != "" {
m["summary"] = x
}
if x := example.Description; x != "" {
m["description"] = x
}
if x := example.Value; x != nil {
m["value"] = x
}
if x := example.ExternalValue; x != "" {
m["externalValue"] = x
}
return m, nil
}
// UnmarshalJSON sets Example to a copy of data.
func (example *Example) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, example)
type ExampleBis Example
var x ExampleBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "summary")
delete(x.Extensions, "description")
delete(x.Extensions, "value")
delete(x.Extensions, "externalValue")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*example = Example(x)
return nil
}
func (value *Example) Validate(ctx context.Context) error {
return nil // TODO
// Validate returns an error if Example does not comply with the OpenAPI spec.
func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if example.Value != nil && example.ExternalValue != "" {
return errors.New("value and externalValue are mutually exclusive")
}
if example.Value == nil && example.ExternalValue == "" {
return errors.New("no value or externalValue field")
}
return validateExtensions(ctx, example.Extensions)
}
// UnmarshalJSON sets Examples to a copy of data.
func (examples *Examples) UnmarshalJSON(data []byte) (err error) {
*examples, _, err = unmarshalStringMapP[ExampleRef](data)
return
}

View file

@ -0,0 +1,16 @@
package openapi3
import "context"
func validateExampleValue(ctx context.Context, input any, schema *Schema) error {
opts := make([]SchemaValidationOption, 0, 2)
if vo := getValidationOptions(ctx); vo.examplesValidationAsReq {
opts = append(opts, VisitAsRequest())
} else if vo.examplesValidationAsRes {
opts = append(opts, VisitAsResponse())
}
opts = append(opts, MultiErrors())
return schema.VisitJSON(input, opts...)
}

View file

@ -1,38 +1,32 @@
package openapi3
import (
"github.com/getkin/kin-openapi/jsoninfo"
"context"
"fmt"
"sort"
"strings"
)
// ExtensionProps provides support for OpenAPI extensions.
// It reads/writes all properties that begin with "x-".
type ExtensionProps struct {
Extensions map[string]interface{} `json:"-" yaml:"-"`
}
func validateExtensions(ctx context.Context, extensions map[string]any) error { // FIXME: newtype + Validate(...)
allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
// Assert that the type implements the interface
var _ jsoninfo.StrictStruct = &ExtensionProps{}
// EncodeWith will be invoked by package "jsoninfo"
func (props *ExtensionProps) EncodeWith(encoder *jsoninfo.ObjectEncoder, value interface{}) error {
for k, v := range props.Extensions {
if err := encoder.EncodeExtension(k, v); err != nil {
return err
var unknowns []string
for k := range extensions {
if strings.HasPrefix(k, "x-") {
continue
}
if allowed != nil {
if _, ok := allowed[k]; ok {
continue
}
}
unknowns = append(unknowns, k)
}
return encoder.EncodeStructFieldsAndExtensions(value)
}
// DecodeWith will be invoked by package "jsoninfo"
func (props *ExtensionProps) DecodeWith(decoder *jsoninfo.ObjectDecoder, value interface{}) error {
if err := decoder.DecodeStructFieldsAndExtensions(value); err != nil {
return err
if len(unknowns) != 0 {
sort.Strings(unknowns)
return fmt.Errorf("extra sibling fields: %+v", unknowns)
}
source := decoder.DecodeExtensionMap()
result := make(map[string]interface{}, len(source))
for k, v := range source {
result[k] = v
}
props.Extensions = result
return nil
}

View file

@ -2,36 +2,74 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"github.com/getkin/kin-openapi/jsoninfo"
)
// ExternalDocs is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#external-documentation-object
type ExternalDocs struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
}
func (e *ExternalDocs) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(e)
// MarshalJSON returns the JSON encoding of ExternalDocs.
func (e ExternalDocs) MarshalJSON() ([]byte, error) {
x, err := e.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of ExternalDocs.
func (e ExternalDocs) MarshalYAML() (any, error) {
m := make(map[string]any, 2+len(e.Extensions))
for k, v := range e.Extensions {
m[k] = v
}
if x := e.Description; x != "" {
m["description"] = x
}
if x := e.URL; x != "" {
m["url"] = x
}
return m, nil
}
// UnmarshalJSON sets ExternalDocs to a copy of data.
func (e *ExternalDocs) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, e)
type ExternalDocsBis ExternalDocs
var x ExternalDocsBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "description")
delete(x.Extensions, "url")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*e = ExternalDocs(x)
return nil
}
func (e *ExternalDocs) Validate(ctx context.Context) error {
// Validate returns an error if ExternalDocs does not comply with the OpenAPI spec.
func (e *ExternalDocs) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if e.URL == "" {
return errors.New("url is required")
}
if _, err := url.Parse(e.URL); err != nil {
return fmt.Errorf("url is incorrect: %w", err)
}
return nil
return validateExtensions(ctx, e.Extensions)
}

View file

@ -5,61 +5,63 @@ import (
"errors"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
)
type Headers map[string]*HeaderRef
var _ jsonpointer.JSONPointable = (*Headers)(nil)
func (h Headers) JSONLookup(token string) (interface{}, error) {
ref, ok := h[token]
if ref == nil || !ok {
return nil, fmt.Errorf("object has no field %q", token)
}
if ref.Ref != "" {
return &Ref{Ref: ref.Ref}, nil
}
return ref.Value, nil
}
// Header is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#headerObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#header-object
type Header struct {
Parameter
}
var _ jsonpointer.JSONPointable = (*Header)(nil)
func (value *Header) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, value)
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
func (header Header) JSONLookup(token string) (any, error) {
return header.Parameter.JSONLookup(token)
}
// MarshalJSON returns the JSON encoding of Header.
func (header Header) MarshalJSON() ([]byte, error) {
return header.Parameter.MarshalJSON()
}
// UnmarshalJSON sets Header to a copy of data.
func (header *Header) UnmarshalJSON(data []byte) error {
return header.Parameter.UnmarshalJSON(data)
}
// MarshalYAML returns the JSON encoding of Header.
func (header Header) MarshalYAML() (any, error) {
return header.Parameter, nil
}
// SerializationMethod returns a header's serialization method.
func (value *Header) SerializationMethod() (*SerializationMethod, error) {
style := value.Style
func (header *Header) SerializationMethod() (*SerializationMethod, error) {
style := header.Style
if style == "" {
style = SerializationSimple
}
explode := false
if value.Explode != nil {
explode = *value.Explode
if header.Explode != nil {
explode = *header.Explode
}
return &SerializationMethod{Style: style, Explode: explode}, nil
}
func (value *Header) Validate(ctx context.Context) error {
if value.Name != "" {
// Validate returns an error if Header does not comply with the OpenAPI spec.
func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if header.Name != "" {
return errors.New("header 'name' MUST NOT be specified, it is given in the corresponding headers map")
}
if value.In != "" {
if header.In != "" {
return errors.New("header 'in' MUST NOT be specified, it is implicitly in header")
}
// Validate a parameter's serialization method.
sm, err := value.SerializationMethod()
sm, err := header.SerializationMethod()
if err != nil {
return err
}
@ -67,62 +69,34 @@ func (value *Header) Validate(ctx context.Context) error {
sm.Style == SerializationSimple && !sm.Explode ||
sm.Style == SerializationSimple && sm.Explode; !smSupported {
e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a header parameter", sm.Style, sm.Explode)
return fmt.Errorf("header schema is invalid: %v", e)
return fmt.Errorf("header schema is invalid: %w", e)
}
if (value.Schema == nil) == (value.Content == nil) {
e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", value)
return fmt.Errorf("header schema is invalid: %v", e)
if (header.Schema == nil) == (len(header.Content) == 0) {
e := fmt.Errorf("parameter must contain exactly one of content and schema: %v", header)
return fmt.Errorf("header schema is invalid: %w", e)
}
if schema := value.Schema; schema != nil {
if schema := header.Schema; schema != nil {
if err := schema.Validate(ctx); err != nil {
return fmt.Errorf("header schema is invalid: %v", err)
return fmt.Errorf("header schema is invalid: %w", err)
}
}
if content := value.Content; content != nil {
if content := header.Content; content != nil {
e := errors.New("parameter content must only contain one entry")
if len(content) > 1 {
return fmt.Errorf("header content is invalid: %w", e)
}
if err := content.Validate(ctx); err != nil {
return fmt.Errorf("header content is invalid: %v", err)
return fmt.Errorf("header content is invalid: %w", err)
}
}
return nil
}
func (value Header) JSONLookup(token string) (interface{}, error) {
switch token {
case "schema":
if value.Schema != nil {
if value.Schema.Ref != "" {
return &Ref{Ref: value.Schema.Ref}, nil
}
return value.Schema.Value, nil
}
case "name":
return value.Name, nil
case "in":
return value.In, nil
case "description":
return value.Description, nil
case "style":
return value.Style, nil
case "explode":
return value.Explode, nil
case "allowEmptyValue":
return value.AllowEmptyValue, nil
case "allowReserved":
return value.AllowReserved, nil
case "deprecated":
return value.Deprecated, nil
case "required":
return value.Required, nil
case "example":
return value.Example, nil
case "examples":
return value.Examples, nil
case "content":
return value.Content, nil
}
v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token)
return v, err
// UnmarshalJSON sets Headers to a copy of data.
func (headers *Headers) UnmarshalJSON(data []byte) (err error) {
*headers, _, err = unmarshalStringMapP[HeaderRef](data)
return
}

View file

@ -0,0 +1,261 @@
package openapi3
import (
"fmt"
"net/url"
"path"
"reflect"
"regexp"
"sort"
"strings"
"github.com/go-openapi/jsonpointer"
)
const identifierChars = `a-zA-Z0-9._-`
// 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(`^[` + identifierChars + `]+$`)
InvalidIdentifierCharRegExp = regexp.MustCompile(`[^` + identifierChars + `]`)
)
// 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 (charset: [%q])", value, identifierChars)
}
// Float64Ptr is a helper for defining OpenAPI schemas.
func Float64Ptr(value float64) *float64 {
return &value
}
// BoolPtr is a helper for defining OpenAPI schemas.
func BoolPtr(value bool) *bool {
return &value
}
// Int64Ptr is a helper for defining OpenAPI schemas.
func Int64Ptr(value int64) *int64 {
return &value
}
// Uint64Ptr is a helper for defining OpenAPI schemas.
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/<type>'. If it does, it returns the name of it in the form
// '#/components/<type>/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, "//")
}

View file

@ -2,15 +2,15 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"github.com/getkin/kin-openapi/jsoninfo"
)
// Info is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#infoObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#info-object
type Info struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Title string `json:"title" yaml:"title"` // Required
Description string `json:"description,omitempty" yaml:"description,omitempty"`
@ -20,80 +20,86 @@ type Info struct {
Version string `json:"version" yaml:"version"` // Required
}
func (value *Info) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(value)
// MarshalJSON returns the JSON encoding of Info.
func (info Info) MarshalJSON() ([]byte, error) {
x, err := info.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
func (value *Info) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, value)
// MarshalYAML returns the YAML encoding of Info.
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
}
m["title"] = info.Title
if x := info.Description; x != "" {
m["description"] = x
}
if x := info.TermsOfService; x != "" {
m["termsOfService"] = x
}
if x := info.Contact; x != nil {
m["contact"] = x
}
if x := info.License; x != nil {
m["license"] = x
}
m["version"] = info.Version
return m, nil
}
func (value *Info) Validate(ctx context.Context) error {
if contact := value.Contact; contact != nil {
// UnmarshalJSON sets Info to a copy of data.
func (info *Info) UnmarshalJSON(data []byte) error {
type InfoBis Info
var x InfoBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "title")
delete(x.Extensions, "description")
delete(x.Extensions, "termsOfService")
delete(x.Extensions, "contact")
delete(x.Extensions, "license")
delete(x.Extensions, "version")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*info = Info(x)
return nil
}
// Validate returns an error if Info does not comply with the OpenAPI spec.
func (info *Info) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if contact := info.Contact; contact != nil {
if err := contact.Validate(ctx); err != nil {
return err
}
}
if license := value.License; license != nil {
if license := info.License; license != nil {
if err := license.Validate(ctx); err != nil {
return err
}
}
if value.Version == "" {
if info.Version == "" {
return errors.New("value of version must be a non-empty string")
}
if value.Title == "" {
if info.Title == "" {
return errors.New("value of title must be a non-empty string")
}
return nil
}
// Contact is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#contactObject
type Contact struct {
ExtensionProps
Name string `json:"name,omitempty" yaml:"name,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
Email string `json:"email,omitempty" yaml:"email,omitempty"`
}
func (value *Contact) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(value)
}
func (value *Contact) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, value)
}
func (value *Contact) Validate(ctx context.Context) error {
return nil
}
// License is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#licenseObject
type License struct {
ExtensionProps
Name string `json:"name" yaml:"name"` // Required
URL string `json:"url,omitempty" yaml:"url,omitempty"`
}
func (value *License) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(value)
}
func (value *License) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, value)
}
func (value *License) Validate(ctx context.Context) error {
if value.Name == "" {
return errors.New("value of license name must be a non-empty string")
}
return nil
return validateExtensions(ctx, info.Extensions)
}

View file

@ -2,145 +2,270 @@ 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 ""
// 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/<type>" 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")
}
split := strings.SplitN(ref, "#", 2)
if len(split) == 2 {
return filepath.Base(split[1])
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
}
ref = split[0]
for ext := filepath.Ext(ref); len(ext) > 0; ext = filepath.Ext(ref) {
ref = strings.TrimSuffix(ref, ext)
filePath, componentPath := name.Path, name.Fragment
// Cut out the "#/components/<type>" 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 filepath.Base(ref)
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 schemaNames(s Schemas) []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 parametersMapNames(s ParametersMap) []string {
out := make([]string, 0, len(s))
for i := range s {
out = append(out, i)
}
return out
func isExternalRef(ref string, parentIsExternal bool) bool {
return ref != "" && (!strings.HasPrefix(ref, "#/components/") || parentIsExternal)
}
func isExternalRef(ref string) bool {
return ref != "" && !strings.HasPrefix(ref, "#/components/")
}
func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver) {
if s == nil || !isExternalRef(s.Ref) {
return
func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
if s == nil || !isExternalRef(s.Ref, parentIsExternal) {
return false
}
name := refNameResolver(s.Ref)
if _, ok := doc.Components.Schemas[name]; ok {
s.Ref = "#/components/schemas/" + name
return
name := refNameResolver(doc, s)
if doc.Components != nil {
if _, ok := doc.Components.Schemas[name]; ok {
s.Ref = "#/components/schemas/" + name
return true
}
}
if doc.Components == nil {
doc.Components = &Components{}
}
if doc.Components.Schemas == nil {
doc.Components.Schemas = make(Schemas)
}
doc.Components.Schemas[name] = s.Value.NewRef()
s.Ref = "#/components/schemas/" + name
return true
}
func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolver) {
if p == nil || !isExternalRef(p.Ref) {
return
func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
if p == nil || !isExternalRef(p.Ref, parentIsExternal) {
return false
}
name := refNameResolver(p.Ref)
if _, ok := doc.Components.Parameters[name]; ok {
p.Ref = "#/components/parameters/" + name
return
name := refNameResolver(doc, p)
if doc.Components != nil {
if _, ok := doc.Components.Parameters[name]; ok {
p.Ref = "#/components/parameters/" + name
return true
}
}
if doc.Components == nil {
doc.Components = &Components{}
}
if doc.Components.Parameters == nil {
doc.Components.Parameters = make(ParametersMap)
}
doc.Components.Parameters[name] = &ParameterRef{Value: p.Value}
p.Ref = "#/components/parameters/" + name
return true
}
func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver) {
if h == nil || !isExternalRef(h.Ref) {
return
func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
if h == nil || !isExternalRef(h.Ref, parentIsExternal) {
return false
}
name := refNameResolver(h.Ref)
if _, ok := doc.Components.Headers[name]; ok {
h.Ref = "#/components/headers/" + name
return
name := refNameResolver(doc, h)
if doc.Components != nil {
if _, ok := doc.Components.Headers[name]; ok {
h.Ref = "#/components/headers/" + name
return true
}
}
if doc.Components == nil {
doc.Components = &Components{}
}
if doc.Components.Headers == nil {
doc.Components.Headers = make(Headers)
}
doc.Components.Headers[name] = &HeaderRef{Value: h.Value}
h.Ref = "#/components/headers/" + name
return true
}
func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameResolver) {
if r == nil || !isExternalRef(r.Ref) {
return
func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
if r == nil || !isExternalRef(r.Ref, parentIsExternal) {
return false
}
name := refNameResolver(r.Ref)
if _, ok := doc.Components.RequestBodies[name]; ok {
r.Ref = "#/components/requestBodies/" + name
return
name := refNameResolver(doc, r)
if doc.Components != nil {
if _, ok := doc.Components.RequestBodies[name]; ok {
r.Ref = "#/components/requestBodies/" + name
return true
}
}
if doc.Components == nil {
doc.Components = &Components{}
}
if doc.Components.RequestBodies == nil {
doc.Components.RequestBodies = make(RequestBodies)
}
doc.Components.RequestBodies[name] = &RequestBodyRef{Value: r.Value}
r.Ref = "#/components/requestBodies/" + name
return true
}
func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver) {
if r == nil || !isExternalRef(r.Ref) {
return
func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
if r == nil || !isExternalRef(r.Ref, parentIsExternal) {
return false
}
name := refNameResolver(r.Ref)
if _, ok := doc.Components.Responses[name]; ok {
r.Ref = "#/components/responses/" + name
return
name := refNameResolver(doc, r)
if doc.Components != nil {
if _, ok := doc.Components.Responses[name]; ok {
r.Ref = "#/components/responses/" + name
return true
}
}
if doc.Components == nil {
doc.Components = &Components{}
}
if doc.Components.Responses == nil {
doc.Components.Responses = make(Responses)
doc.Components.Responses = make(ResponseBodies)
}
doc.Components.Responses[name] = &ResponseRef{Value: r.Value}
r.Ref = "#/components/responses/" + name
return true
}
func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver RefNameResolver) {
if ss == nil || !isExternalRef(ss.Ref) {
func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver RefNameResolver, parentIsExternal bool) {
if ss == nil || !isExternalRef(ss.Ref, parentIsExternal) {
return
}
name := refNameResolver(ss.Ref)
if _, ok := doc.Components.SecuritySchemes[name]; ok {
ss.Ref = "#/components/securitySchemes/" + name
return
name := refNameResolver(doc, ss)
if doc.Components != nil {
if _, ok := doc.Components.SecuritySchemes[name]; ok {
ss.Ref = "#/components/securitySchemes/" + name
return
}
}
if doc.Components == nil {
doc.Components = &Components{}
}
if doc.Components.SecuritySchemes == nil {
doc.Components.SecuritySchemes = make(SecuritySchemes)
@ -150,14 +275,20 @@ func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver Ref
}
func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver) {
if e == nil || !isExternalRef(e.Ref) {
func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver, parentIsExternal bool) {
if e == nil || !isExternalRef(e.Ref, parentIsExternal) {
return
}
name := refNameResolver(e.Ref)
if _, ok := doc.Components.Examples[name]; ok {
e.Ref = "#/components/examples/" + name
return
name := refNameResolver(doc, e)
if doc.Components != nil {
if _, ok := doc.Components.Examples[name]; ok {
e.Ref = "#/components/examples/" + name
return
}
}
if doc.Components == nil {
doc.Components = &Components{}
}
if doc.Components.Examples == nil {
doc.Components.Examples = make(Examples)
@ -167,14 +298,20 @@ func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver) {
}
func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver) {
if l == nil || !isExternalRef(l.Ref) {
func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver, parentIsExternal bool) {
if l == nil || !isExternalRef(l.Ref, parentIsExternal) {
return
}
name := refNameResolver(l.Ref)
if _, ok := doc.Components.Links[name]; ok {
l.Ref = "#/components/links/" + name
return
name := refNameResolver(doc, l)
if doc.Components != nil {
if _, ok := doc.Components.Links[name]; ok {
l.Ref = "#/components/links/" + name
return
}
}
if doc.Components == nil {
doc.Components = &Components{}
}
if doc.Components.Links == nil {
doc.Components.Links = make(Links)
@ -184,124 +321,158 @@ func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver) {
}
func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver) {
if c == nil || !isExternalRef(c.Ref) {
return
func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver, parentIsExternal bool) bool {
if c == nil || !isExternalRef(c.Ref, parentIsExternal) {
return false
}
name := refNameResolver(c.Ref)
if _, ok := doc.Components.Callbacks[name]; ok {
c.Ref = "#/components/callbacks/" + name
name := refNameResolver(doc, c)
if doc.Components == nil {
doc.Components = &Components{}
}
if doc.Components.Callbacks == nil {
doc.Components.Callbacks = make(Callbacks)
}
doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value}
c.Ref = "#/components/callbacks/" + name
doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value}
return true
}
func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver) {
if s == nil {
func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver, parentIsExternal bool) {
if s == nil || doc.isVisitedSchema(s) {
return
}
for _, list := range []SchemaRefs{s.AllOf, s.AnyOf, s.OneOf} {
for _, s2 := range list {
doc.addSchemaToSpec(s2, refNameResolver)
isExternal := doc.addSchemaToSpec(s2, refNameResolver, parentIsExternal)
if s2 != nil {
doc.derefSchema(s2.Value, refNameResolver)
doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal)
}
}
}
for _, s2 := range s.Properties {
doc.addSchemaToSpec(s2, refNameResolver)
for _, name := range componentNames(s.Properties) {
s2 := s.Properties[name]
isExternal := doc.addSchemaToSpec(s2, refNameResolver, parentIsExternal)
if s2 != nil {
doc.derefSchema(s2.Value, refNameResolver)
doc.derefSchema(s2.Value, refNameResolver, isExternal || parentIsExternal)
}
}
for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties, s.Items} {
doc.addSchemaToSpec(ref, refNameResolver)
for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties.Schema, s.Items} {
isExternal := doc.addSchemaToSpec(ref, refNameResolver, parentIsExternal)
if ref != nil {
doc.derefSchema(ref.Value, refNameResolver)
doc.derefSchema(ref.Value, refNameResolver, isExternal || parentIsExternal)
}
}
}
func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver) {
for _, h := range hs {
doc.addHeaderToSpec(h, refNameResolver)
doc.derefParameter(h.Value.Parameter, refNameResolver)
func (doc *T) derefHeaders(hs Headers, refNameResolver RefNameResolver, parentIsExternal bool) {
for _, name := range componentNames(hs) {
h := hs[name]
isExternal := doc.addHeaderToSpec(h, refNameResolver, parentIsExternal)
if doc.isVisitedHeader(h.Value) {
continue
}
doc.derefParameter(h.Value.Parameter, refNameResolver, parentIsExternal || isExternal)
}
}
func (doc *T) derefExamples(es Examples, refNameResolver RefNameResolver) {
for _, e := range es {
doc.addExampleToSpec(e, refNameResolver)
func (doc *T) derefExamples(es Examples, refNameResolver RefNameResolver, parentIsExternal bool) {
for _, name := range componentNames(es) {
e := es[name]
doc.addExampleToSpec(e, refNameResolver, parentIsExternal)
}
}
func (doc *T) derefContent(c Content, refNameResolver RefNameResolver) {
for _, mediatype := range c {
doc.addSchemaToSpec(mediatype.Schema, refNameResolver)
func (doc *T) derefContent(c Content, refNameResolver RefNameResolver, parentIsExternal bool) {
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)
doc.derefSchema(mediatype.Schema.Value, refNameResolver, isExternal || parentIsExternal)
}
doc.derefExamples(mediatype.Examples, refNameResolver)
for _, e := range mediatype.Encoding {
doc.derefHeaders(e.Headers, refNameResolver)
doc.derefExamples(mediatype.Examples, refNameResolver, parentIsExternal)
for _, name := range componentNames(mediatype.Encoding) {
e := mediatype.Encoding[name]
doc.derefHeaders(e.Headers, refNameResolver, parentIsExternal)
}
}
}
func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver) {
for _, l := range ls {
doc.addLinkToSpec(l, refNameResolver)
func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver, parentIsExternal bool) {
for _, name := range componentNames(ls) {
l := ls[name]
doc.addLinkToSpec(l, refNameResolver, parentIsExternal)
}
}
func (doc *T) derefResponses(es Responses, refNameResolver RefNameResolver) {
for _, e := range es {
doc.addResponseToSpec(e, refNameResolver)
if e.Value != nil {
doc.derefHeaders(e.Value.Headers, refNameResolver)
doc.derefContent(e.Value.Content, refNameResolver)
doc.derefLinks(e.Value.Links, refNameResolver)
}
func (doc *T) derefResponse(r *ResponseRef, refNameResolver RefNameResolver, parentIsExternal bool) {
isExternal := doc.addResponseToSpec(r, refNameResolver, parentIsExternal)
if v := r.Value; v != nil {
doc.derefHeaders(v.Headers, refNameResolver, isExternal || parentIsExternal)
doc.derefContent(v.Content, refNameResolver, isExternal || parentIsExternal)
doc.derefLinks(v.Links, refNameResolver, isExternal || parentIsExternal)
}
}
func (doc *T) derefParameter(p Parameter, refNameResolver RefNameResolver) {
doc.addSchemaToSpec(p.Schema, refNameResolver)
doc.derefContent(p.Content, refNameResolver)
func (doc *T) derefResponses(rs *Responses, refNameResolver RefNameResolver, parentIsExternal bool) {
doc.derefResponseBodies(rs.Map(), refNameResolver, parentIsExternal)
}
func (doc *T) derefResponseBodies(es ResponseBodies, refNameResolver RefNameResolver, parentIsExternal bool) {
for _, name := range componentNames(es) {
e := es[name]
doc.derefResponse(e, refNameResolver, parentIsExternal)
}
}
func (doc *T) derefParameter(p Parameter, refNameResolver RefNameResolver, parentIsExternal bool) {
isExternal := doc.addSchemaToSpec(p.Schema, refNameResolver, parentIsExternal)
doc.derefContent(p.Content, refNameResolver, parentIsExternal)
if p.Schema != nil {
doc.derefSchema(p.Schema.Value, refNameResolver)
doc.derefSchema(p.Schema.Value, refNameResolver, isExternal || parentIsExternal)
}
}
func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver) {
doc.derefContent(r.Content, refNameResolver)
func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver, parentIsExternal bool) {
doc.derefContent(r.Content, refNameResolver, parentIsExternal)
}
func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver) {
for _, ops := range paths {
func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver, parentIsExternal bool) {
for _, name := range componentNames(paths) {
ops := paths[name]
pathIsExternal := isExternalRef(ops.Ref, parentIsExternal)
// inline full operations
ops.Ref = ""
for _, op := range ops.Operations() {
doc.addRequestBodyToSpec(op.RequestBody, refNameResolver)
if op.RequestBody != nil && op.RequestBody.Value != nil {
doc.derefRequestBody(*op.RequestBody.Value, refNameResolver)
for _, param := range ops.Parameters {
isExternal := doc.addParameterToSpec(param, refNameResolver, pathIsExternal)
if param.Value != nil {
doc.derefParameter(*param.Value, refNameResolver, pathIsExternal || isExternal)
}
for _, cb := range op.Callbacks {
doc.addCallbackToSpec(cb, refNameResolver)
}
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 _, name := range componentNames(op.Callbacks) {
cb := op.Callbacks[name]
isExternal := doc.addCallbackToSpec(cb, refNameResolver, pathIsExternal)
if cb.Value != nil {
doc.derefPaths(*cb.Value, refNameResolver)
cbValue := (*cb.Value).Map()
doc.derefPaths(cbValue, refNameResolver, pathIsExternal || isExternal)
}
}
doc.derefResponses(op.Responses, refNameResolver)
doc.derefResponses(op.Responses, refNameResolver, pathIsExternal)
for _, param := range op.Parameters {
doc.addParameterToSpec(param, refNameResolver)
isExternal := doc.addParameterToSpec(param, refNameResolver, pathIsExternal)
if param.Value != nil {
doc.derefParameter(*param.Value, refNameResolver)
doc.derefParameter(*param.Value, refNameResolver, pathIsExternal || isExternal)
}
}
}
@ -314,56 +485,62 @@ func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameReso
// refNameResolver takes in references to returns a name to store the reference under locally.
// It MUST return a unique name for each reference type.
// A default implementation is provided that will suffice for most use cases. See the function
// documention for more details.
// documentation for more details.
//
// Example:
//
// doc.InternalizeRefs(context.Background(), nil)
func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string) {
// doc.InternalizeRefs(context.Background(), nil)
func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(*T, ComponentRef) string) {
doc.resetVisited()
if refNameResolver == nil {
refNameResolver = DefaultRefNameResolver
}
// Handle components section
names := schemaNames(doc.Components.Schemas)
for _, name := range names {
schema := doc.Components.Schemas[name]
doc.addSchemaToSpec(schema, refNameResolver)
if schema != nil {
schema.Ref = "" // always dereference the top level
doc.derefSchema(schema.Value, refNameResolver)
if components := doc.Components; components != nil {
for _, name := range componentNames(components.Schemas) {
schema := components.Schemas[name]
isExternal := doc.addSchemaToSpec(schema, refNameResolver, false)
if schema != nil {
schema.Ref = "" // always dereference the top level
doc.derefSchema(schema.Value, refNameResolver, isExternal)
}
}
}
names = parametersMapNames(doc.Components.Parameters)
for _, name := range names {
p := doc.Components.Parameters[name]
doc.addParameterToSpec(p, refNameResolver)
if p != nil && p.Value != nil {
p.Ref = "" // always dereference the top level
doc.derefParameter(*p.Value, refNameResolver)
for _, name := range componentNames(components.Parameters) {
p := components.Parameters[name]
isExternal := doc.addParameterToSpec(p, refNameResolver, false)
if p != nil && p.Value != nil {
p.Ref = "" // always dereference the top level
doc.derefParameter(*p.Value, refNameResolver, isExternal)
}
}
}
doc.derefHeaders(doc.Components.Headers, refNameResolver)
for _, req := range doc.Components.RequestBodies {
doc.addRequestBodyToSpec(req, refNameResolver)
if req != nil && req.Value != nil {
req.Ref = "" // always dereference the top level
doc.derefRequestBody(*req.Value, refNameResolver)
doc.derefHeaders(components.Headers, refNameResolver, false)
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
doc.derefRequestBody(*req.Value, refNameResolver, isExternal)
}
}
}
doc.derefResponses(doc.Components.Responses, refNameResolver)
for _, ss := range doc.Components.SecuritySchemes {
doc.addSecuritySchemeToSpec(ss, refNameResolver)
}
doc.derefExamples(doc.Components.Examples, refNameResolver)
doc.derefLinks(doc.Components.Links, refNameResolver)
for _, cb := range doc.Components.Callbacks {
doc.addCallbackToSpec(cb, refNameResolver)
if cb != nil && cb.Value != nil {
cb.Ref = "" // always dereference the top level
doc.derefPaths(*cb.Value, refNameResolver)
doc.derefResponseBodies(components.Responses, refNameResolver, false)
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 _, 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
cbValue := (*cb.Value).Map()
doc.derefPaths(cbValue, refNameResolver, isExternal)
}
}
}
doc.derefPaths(doc.Paths, refNameResolver)
doc.derefPaths(doc.Paths.Map(), refNameResolver, false)
}

View file

@ -0,0 +1,68 @@
package openapi3
import (
"context"
"encoding/json"
"errors"
)
// 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]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Name string `json:"name" yaml:"name"` // Required
URL string `json:"url,omitempty" yaml:"url,omitempty"`
}
// MarshalJSON returns the JSON encoding of License.
func (license License) MarshalJSON() ([]byte, error) {
x, err := license.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of License.
func (license License) MarshalYAML() (any, error) {
m := make(map[string]any, 2+len(license.Extensions))
for k, v := range license.Extensions {
m[k] = v
}
m["name"] = license.Name
if x := license.URL; x != "" {
m["url"] = x
}
return m, nil
}
// UnmarshalJSON sets License to a copy of data.
func (license *License) UnmarshalJSON(data []byte) error {
type LicenseBis License
var x LicenseBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "name")
delete(x.Extensions, "url")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*license = License(x)
return nil
}
// Validate returns an error if License does not comply with the OpenAPI spec.
func (license *License) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if license.Name == "" {
return errors.New("value of license name must be a non-empty string")
}
return validateExtensions(ctx, license.Extensions)
}

View file

@ -2,56 +2,102 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
)
type Links map[string]*LinkRef
func (l Links) JSONLookup(token string) (interface{}, error) {
ref, ok := l[token]
if ok == false {
return nil, fmt.Errorf("object has no field %q", token)
}
if ref != nil && ref.Ref != "" {
return &Ref{Ref: ref.Ref}, nil
}
return ref.Value, nil
}
var _ jsonpointer.JSONPointable = (*Links)(nil)
// Link is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#linkObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#link-object
type Link struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
OperationRef string `json:"operationRef,omitempty" yaml:"operationRef,omitempty"`
OperationID string `json:"operationId,omitempty" yaml:"operationId,omitempty"`
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"`
}
func (value *Link) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(value)
}
func (value *Link) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, value)
}
func (value *Link) Validate(ctx context.Context) error {
if value.OperationID == "" && value.OperationRef == "" {
return errors.New("missing operationId or operationRef on link")
// MarshalJSON returns the JSON encoding of Link.
func (link Link) MarshalJSON() ([]byte, error) {
x, err := link.MarshalYAML()
if err != nil {
return nil, err
}
if value.OperationID != "" && value.OperationRef != "" {
return fmt.Errorf("operationId %q and operationRef %q are mutually exclusive", value.OperationID, value.OperationRef)
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of Link.
func (link Link) MarshalYAML() (any, error) {
m := make(map[string]any, 6+len(link.Extensions))
for k, v := range link.Extensions {
m[k] = v
}
if x := link.OperationRef; x != "" {
m["operationRef"] = x
}
if x := link.OperationID; x != "" {
m["operationId"] = x
}
if x := link.Description; x != "" {
m["description"] = x
}
if x := link.Parameters; len(x) != 0 {
m["parameters"] = x
}
if x := link.Server; x != nil {
m["server"] = x
}
if x := link.RequestBody; x != nil {
m["requestBody"] = x
}
return m, nil
}
// UnmarshalJSON sets Link to a copy of data.
func (link *Link) UnmarshalJSON(data []byte) error {
type LinkBis Link
var x LinkBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "operationRef")
delete(x.Extensions, "operationId")
delete(x.Extensions, "description")
delete(x.Extensions, "parameters")
delete(x.Extensions, "server")
delete(x.Extensions, "requestBody")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*link = Link(x)
return nil
}
// Validate returns an error if Link does not comply with the OpenAPI spec.
func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if link.OperationID == "" && link.OperationRef == "" {
return errors.New("missing operationId or operationRef on link")
}
if link.OperationID != "" && link.OperationRef != "" {
return fmt.Errorf("operationId %q and operationRef %q are mutually exclusive", link.OperationID, link.OperationRef)
}
return validateExtensions(ctx, link.Extensions)
}
// UnmarshalJSON sets Links to a copy of data.
func (links *Links) UnmarshalJSON(data []byte) (err error) {
*links, _, err = unmarshalStringMapP[LinkRef](data)
return
}

File diff suppressed because it is too large Load diff

View file

@ -3,16 +3,20 @@ package openapi3
import (
"errors"
"fmt"
"io/ioutil"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
)
// ReadFromURIFunc defines a function which reads the contents of a resource
// located at a URI.
type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error)
var uriMu = &sync.RWMutex{}
// ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a
// given URI.
var ErrURINotSupported = errors.New("unsupported URI")
@ -60,19 +64,22 @@ func ReadFromHTTP(cl *http.Client) ReadFromURIFunc {
if resp.StatusCode > 399 {
return nil, fmt.Errorf("error loading %q: request returned status code %d", location.String(), resp.StatusCode)
}
return ioutil.ReadAll(resp.Body)
return io.ReadAll(resp.Body)
}
}
func is_file(location *url.URL) bool {
return location.Path != "" &&
location.Host == "" &&
(location.Scheme == "" || location.Scheme == "file")
}
// ReadFromFile is a ReadFromURIFunc which reads local file URIs.
func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) {
if location.Host != "" {
if !is_file(location) {
return nil, ErrURINotSupported
}
if location.Scheme != "" && location.Scheme != "file" {
return nil, ErrURINotSupported
}
return ioutil.ReadFile(location.Path)
return os.ReadFile(filepath.FromSlash(location.Path))
}
// URIMapCache returns a ReadFromURIFunc that caches the contents read from URI
@ -92,12 +99,17 @@ func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc {
}
uri := location.String()
var ok bool
uriMu.RLock()
if buf, ok = cache[uri]; ok {
uriMu.RUnlock()
return
}
uriMu.RUnlock()
if buf, err = reader(loader, location); err != nil {
return
}
uriMu.Lock()
defer uriMu.Unlock()
cache[uri] = buf
return
}

View file

@ -0,0 +1,435 @@
package openapi3
import (
"encoding/json"
"sort"
"strings"
"github.com/go-openapi/jsonpointer"
)
// NewResponsesWithCapacity builds a responses object of the given capacity.
func NewResponsesWithCapacity(cap int) *Responses {
if cap == 0 {
return &Responses{m: make(map[string]*ResponseRef)}
}
return &Responses{m: make(map[string]*ResponseRef, cap)}
}
// Value returns the responses for key or nil
func (responses *Responses) Value(key string) *ResponseRef {
if responses.Len() == 0 {
return nil
}
return responses.m[key]
}
// Set adds or replaces key 'key' of 'responses' with 'value'.
// Note: 'responses' MUST be non-nil
func (responses *Responses) Set(key string, value *ResponseRef) {
if responses.m == nil {
responses.m = make(map[string]*ResponseRef)
}
responses.m[key] = value
}
// Len returns the amount of keys in responses excluding responses.Extensions.
func (responses *Responses) Len() int {
if responses == nil || responses.m == nil {
return 0
}
return len(responses.m)
}
// Delete removes the entry associated with key 'key' from 'responses'.
func (responses *Responses) Delete(key string) {
if responses != nil && responses.m != nil {
delete(responses.m, key)
}
}
// Map returns responses as a 'map'.
// Note: iteration on Go maps is not ordered.
func (responses *Responses) Map() (m map[string]*ResponseRef) {
if responses == nil || len(responses.m) == 0 {
return make(map[string]*ResponseRef)
}
m = make(map[string]*ResponseRef, len(responses.m))
for k, v := range responses.m {
m[k] = v
}
return
}
var _ jsonpointer.JSONPointable = (*Responses)(nil)
// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable
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
} else if ref := v.Ref; ref != "" {
return &Ref{Ref: ref}, nil
} else {
var vv *Response = v.Value
return vv, nil
}
}
// MarshalYAML returns the YAML encoding of Responses.
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
}
for k, v := range responses.Map() {
m[k] = v
}
return m, nil
}
// MarshalJSON returns the JSON encoding of Responses.
func (responses *Responses) MarshalJSON() ([]byte, error) {
responsesYaml, err := responses.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(responsesYaml)
}
// UnmarshalJSON sets Responses to a copy of data.
func (responses *Responses) UnmarshalJSON(data []byte) (err error) {
var m map[string]any
if err = json.Unmarshal(data, &m); err != nil {
return
}
ks := make([]string, 0, len(m))
for k := range m {
ks = append(ks, k)
}
sort.Strings(ks)
x := Responses{
Extensions: make(map[string]any),
m: make(map[string]*ResponseRef, len(m)),
}
for _, k := range ks {
v := m[k]
if strings.HasPrefix(k, "x-") {
x.Extensions[k] = v
continue
}
if k == originKey {
var data []byte
if data, err = json.Marshal(v); err != nil {
return
}
if err = json.Unmarshal(data, &x.Origin); err != nil {
return
}
continue
}
var data []byte
if data, err = json.Marshal(v); err != nil {
return
}
var vv ResponseRef
if err = vv.UnmarshalJSON(data); err != nil {
return
}
x.m[k] = &vv
}
*responses = x
return
}
// NewCallbackWithCapacity builds a callback object of the given capacity.
func NewCallbackWithCapacity(cap int) *Callback {
if cap == 0 {
return &Callback{m: make(map[string]*PathItem)}
}
return &Callback{m: make(map[string]*PathItem, cap)}
}
// Value returns the callback for key or nil
func (callback *Callback) Value(key string) *PathItem {
if callback.Len() == 0 {
return nil
}
return callback.m[key]
}
// Set adds or replaces key 'key' of 'callback' with 'value'.
// Note: 'callback' MUST be non-nil
func (callback *Callback) Set(key string, value *PathItem) {
if callback.m == nil {
callback.m = make(map[string]*PathItem)
}
callback.m[key] = value
}
// Len returns the amount of keys in callback excluding callback.Extensions.
func (callback *Callback) Len() int {
if callback == nil || callback.m == nil {
return 0
}
return len(callback.m)
}
// Delete removes the entry associated with key 'key' from 'callback'.
func (callback *Callback) Delete(key string) {
if callback != nil && callback.m != nil {
delete(callback.m, key)
}
}
// Map returns callback as a 'map'.
// Note: iteration on Go maps is not ordered.
func (callback *Callback) Map() (m map[string]*PathItem) {
if callback == nil || len(callback.m) == 0 {
return make(map[string]*PathItem)
}
m = make(map[string]*PathItem, len(callback.m))
for k, v := range callback.m {
m[k] = v
}
return
}
var _ jsonpointer.JSONPointable = (*Callback)(nil)
// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable
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
} else if ref := v.Ref; ref != "" {
return &Ref{Ref: ref}, nil
} else {
var vv *PathItem = v
return vv, nil
}
}
// MarshalYAML returns the YAML encoding of Callback.
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
}
for k, v := range callback.Map() {
m[k] = v
}
return m, nil
}
// MarshalJSON returns the JSON encoding of Callback.
func (callback *Callback) MarshalJSON() ([]byte, error) {
callbackYaml, err := callback.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(callbackYaml)
}
// UnmarshalJSON sets Callback to a copy of data.
func (callback *Callback) UnmarshalJSON(data []byte) (err error) {
var m map[string]any
if err = json.Unmarshal(data, &m); err != nil {
return
}
ks := make([]string, 0, len(m))
for k := range m {
ks = append(ks, k)
}
sort.Strings(ks)
x := Callback{
Extensions: make(map[string]any),
m: make(map[string]*PathItem, len(m)),
}
for _, k := range ks {
v := m[k]
if strings.HasPrefix(k, "x-") {
x.Extensions[k] = v
continue
}
if k == originKey {
var data []byte
if data, err = json.Marshal(v); err != nil {
return
}
if err = json.Unmarshal(data, &x.Origin); err != nil {
return
}
continue
}
var data []byte
if data, err = json.Marshal(v); err != nil {
return
}
var vv PathItem
if err = vv.UnmarshalJSON(data); err != nil {
return
}
x.m[k] = &vv
}
*callback = x
return
}
// NewPathsWithCapacity builds a paths object of the given capacity.
func NewPathsWithCapacity(cap int) *Paths {
if cap == 0 {
return &Paths{m: make(map[string]*PathItem)}
}
return &Paths{m: make(map[string]*PathItem, cap)}
}
// Value returns the paths for key or nil
func (paths *Paths) Value(key string) *PathItem {
if paths.Len() == 0 {
return nil
}
return paths.m[key]
}
// Set adds or replaces key 'key' of 'paths' with 'value'.
// Note: 'paths' MUST be non-nil
func (paths *Paths) Set(key string, value *PathItem) {
if paths.m == nil {
paths.m = make(map[string]*PathItem)
}
paths.m[key] = value
}
// Len returns the amount of keys in paths excluding paths.Extensions.
func (paths *Paths) Len() int {
if paths == nil || paths.m == nil {
return 0
}
return len(paths.m)
}
// Delete removes the entry associated with key 'key' from 'paths'.
func (paths *Paths) Delete(key string) {
if paths != nil && paths.m != nil {
delete(paths.m, key)
}
}
// Map returns paths as a 'map'.
// Note: iteration on Go maps is not ordered.
func (paths *Paths) Map() (m map[string]*PathItem) {
if paths == nil || len(paths.m) == 0 {
return make(map[string]*PathItem)
}
m = make(map[string]*PathItem, len(paths.m))
for k, v := range paths.m {
m[k] = v
}
return
}
var _ jsonpointer.JSONPointable = (*Paths)(nil)
// JSONLookup implements https://github.com/go-openapi/jsonpointer#JSONPointable
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
} else if ref := v.Ref; ref != "" {
return &Ref{Ref: ref}, nil
} else {
var vv *PathItem = v
return vv, nil
}
}
// MarshalYAML returns the YAML encoding of Paths.
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
}
for k, v := range paths.Map() {
m[k] = v
}
return m, nil
}
// MarshalJSON returns the JSON encoding of Paths.
func (paths *Paths) MarshalJSON() ([]byte, error) {
pathsYaml, err := paths.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(pathsYaml)
}
// UnmarshalJSON sets Paths to a copy of data.
func (paths *Paths) UnmarshalJSON(data []byte) (err error) {
var m map[string]any
if err = json.Unmarshal(data, &m); err != nil {
return
}
ks := make([]string, 0, len(m))
for k := range m {
ks = append(ks, k)
}
sort.Strings(ks)
x := Paths{
Extensions: make(map[string]any),
m: make(map[string]*PathItem, len(m)),
}
for _, k := range ks {
v := m[k]
if strings.HasPrefix(k, "x-") {
x.Extensions[k] = v
continue
}
if k == originKey {
var data []byte
if data, err = json.Marshal(v); err != nil {
return
}
if err = json.Unmarshal(data, &x.Origin); err != nil {
return
}
continue
}
var data []byte
if data, err = json.Marshal(v); err != nil {
return
}
var vv PathItem
if err = vv.UnmarshalJSON(data); err != nil {
return
}
x.m[k] = &vv
}
*paths = x
return
}

34
vendor/github.com/getkin/kin-openapi/openapi3/marsh.go generated vendored Normal file
View file

@ -0,0 +1,34 @@
package openapi3
import (
"encoding/json"
"fmt"
"strings"
"github.com/oasdiff/yaml"
)
func unmarshalError(jsonUnmarshalErr error) error {
if before, after, found := strings.Cut(jsonUnmarshalErr.Error(), "Bis"); found && before != "" && after != "" {
before = strings.ReplaceAll(before, " Go struct ", " ")
return fmt.Errorf("%s%s", before, strings.ReplaceAll(after, "Bis", ""))
}
return jsonUnmarshalErr
}
func unmarshal(data []byte, v any, includeOrigin bool) error {
var jsonErr, yamlErr error
// See https://github.com/getkin/kin-openapi/issues/680
if jsonErr = json.Unmarshal(data, v); jsonErr == nil {
return nil
}
// UnmarshalStrict(data, v) TODO: investigate how ymlv3 handles duplicate map keys
if yamlErr = yaml.UnmarshalWithOrigin(data, v, includeOrigin); yamlErr == nil {
return nil
}
// If both unmarshaling attempts fail, return a new error that includes both errors
return fmt.Errorf("failed to unmarshal data: json error: %v, yaml error: %v", jsonErr, yamlErr)
}

View file

@ -2,18 +2,22 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
)
// MediaType is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#media-type-object
type MediaType struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
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"`
}
@ -38,7 +42,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)
@ -60,27 +64,103 @@ func (mediaType *MediaType) WithEncoding(name string, enc *Encoding) *MediaType
return mediaType
}
func (mediaType *MediaType) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(mediaType)
// MarshalJSON returns the JSON encoding of MediaType.
func (mediaType MediaType) MarshalJSON() ([]byte, error) {
x, err := mediaType.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of MediaType.
func (mediaType MediaType) MarshalYAML() (any, error) {
m := make(map[string]any, 4+len(mediaType.Extensions))
for k, v := range mediaType.Extensions {
m[k] = v
}
if x := mediaType.Schema; x != nil {
m["schema"] = x
}
if x := mediaType.Example; x != nil {
m["example"] = x
}
if x := mediaType.Examples; len(x) != 0 {
m["examples"] = x
}
if x := mediaType.Encoding; len(x) != 0 {
m["encoding"] = x
}
return m, nil
}
// UnmarshalJSON sets MediaType to a copy of data.
func (mediaType *MediaType) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, mediaType)
}
func (value *MediaType) Validate(ctx context.Context) error {
if value == nil {
return nil
type MediaTypeBis MediaType
var x MediaTypeBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
if schema := value.Schema; schema != nil {
if err := schema.Validate(ctx); err != nil {
return err
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "schema")
delete(x.Extensions, "example")
delete(x.Extensions, "examples")
delete(x.Extensions, "encoding")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*mediaType = MediaType(x)
return nil
}
func (mediaType MediaType) JSONLookup(token string) (interface{}, error) {
// Validate returns an error if MediaType does not comply with the OpenAPI spec.
func (mediaType *MediaType) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if mediaType == nil {
return nil
}
if schema := mediaType.Schema; schema != nil {
if err := schema.Validate(ctx); err != nil {
return err
}
if mediaType.Example != nil && mediaType.Examples != nil {
return errors.New("example and examples are mutually exclusive")
}
if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled {
if example := mediaType.Example; example != nil {
if err := validateExampleValue(ctx, example, schema.Value); err != nil {
return fmt.Errorf("invalid example: %w", err)
}
}
if examples := mediaType.Examples; examples != nil {
names := make([]string, 0, len(examples))
for name := range examples {
names = append(names, name)
}
sort.Strings(names)
for _, k := range names {
v := examples[k]
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("example %s: %w", k, err)
}
if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
return fmt.Errorf("example %s: %w", k, err)
}
}
}
}
}
return validateExtensions(ctx, mediaType.Extensions)
}
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
func (mediaType MediaType) JSONLookup(token string) (any, error) {
switch token {
case "schema":
if mediaType.Schema != nil {
@ -96,6 +176,6 @@ func (mediaType MediaType) JSONLookup(token string) (interface{}, error) {
case "encoding":
return mediaType.Encoding, nil
}
v, _, err := jsonpointer.GetForToken(mediaType.ExtensionProps, token)
v, _, err := jsonpointer.GetForToken(mediaType.Extensions, token)
return v, err
}

View file

@ -2,45 +2,129 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
)
// T is the root of an OpenAPI v3 document
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oasObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#openapi-object
type T struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
OpenAPI string `json:"openapi" yaml:"openapi"` // Required
Components Components `json:"components,omitempty" yaml:"components,omitempty"`
Components *Components `json:"components,omitempty" yaml:"components,omitempty"`
Info *Info `json:"info" yaml:"info"` // Required
Paths Paths `json:"paths" yaml:"paths"` // Required
Paths *Paths `json:"paths" yaml:"paths"` // Required
Security SecurityRequirements `json:"security,omitempty" yaml:"security,omitempty"`
Servers Servers `json:"servers,omitempty" yaml:"servers,omitempty"`
Tags Tags `json:"tags,omitempty" yaml:"tags,omitempty"`
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) (any, error) {
switch token {
case "openapi":
return doc.OpenAPI, nil
case "components":
return doc.Components, nil
case "info":
return doc.Info, nil
case "paths":
return doc.Paths, nil
case "security":
return doc.Security, nil
case "servers":
return doc.Servers, nil
case "tags":
return doc.Tags, nil
case "externalDocs":
return doc.ExternalDocs, nil
}
v, _, err := jsonpointer.GetForToken(doc.Extensions, token)
return v, err
}
// MarshalJSON returns the JSON encoding of T.
func (doc *T) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(doc)
x, err := doc.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of T.
func (doc *T) MarshalYAML() (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
}
m["openapi"] = doc.OpenAPI
if x := doc.Components; x != nil {
m["components"] = x
}
m["info"] = doc.Info
m["paths"] = doc.Paths
if x := doc.Security; len(x) != 0 {
m["security"] = x
}
if x := doc.Servers; len(x) != 0 {
m["servers"] = x
}
if x := doc.Tags; len(x) != 0 {
m["tags"] = x
}
if x := doc.ExternalDocs; x != nil {
m["externalDocs"] = x
}
return m, nil
}
// UnmarshalJSON sets T to a copy of data.
func (doc *T) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, doc)
type TBis T
var x TBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, "openapi")
delete(x.Extensions, "components")
delete(x.Extensions, "info")
delete(x.Extensions, "paths")
delete(x.Extensions, "security")
delete(x.Extensions, "servers")
delete(x.Extensions, "tags")
delete(x.Extensions, "externalDocs")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*doc = T(x)
return nil
}
func (doc *T) AddOperation(path string, method string, operation *Operation) {
paths := doc.Paths
if paths == nil {
paths = make(Paths)
doc.Paths = paths
if doc.Paths == nil {
doc.Paths = NewPaths()
}
pathItem := paths[path]
pathItem := doc.Paths.Value(path)
if pathItem == nil {
pathItem = &PathItem{}
paths[path] = pathItem
doc.Paths.Set(path, pathItem)
}
pathItem.SetOperation(method, operation)
}
@ -49,77 +133,73 @@ func (doc *T) AddServer(server *Server) {
doc.Servers = append(doc.Servers, server)
}
func (value *T) Validate(ctx context.Context) error {
if value.OpenAPI == "" {
func (doc *T) AddServers(servers ...*Server) {
doc.Servers = append(doc.Servers, servers...)
}
// Validate returns an error if T does not comply with the OpenAPI spec.
// Validations Options can be provided to modify the validation behavior.
func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if doc.OpenAPI == "" {
return errors.New("value of openapi must be a non-empty string")
}
// NOTE: only mention info/components/paths/... key in this func's errors.
var wrap func(error) error
{
wrap := func(e error) error { return fmt.Errorf("invalid components: %v", e) }
if err := value.Components.Validate(ctx); err != nil {
wrap = func(e error) error { return fmt.Errorf("invalid components: %w", e) }
if v := doc.Components; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
}
{
wrap := func(e error) error { return fmt.Errorf("invalid info: %v", e) }
if v := value.Info; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
} else {
return wrap(errors.New("must be an object"))
wrap = func(e error) error { return fmt.Errorf("invalid info: %w", e) }
if v := doc.Info; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
} else {
return wrap(errors.New("must be an object"))
}
wrap = func(e error) error { return fmt.Errorf("invalid paths: %w", e) }
if v := doc.Paths; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
} else {
return wrap(errors.New("must be an object"))
}
wrap = func(e error) error { return fmt.Errorf("invalid security: %w", e) }
if v := doc.Security; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
}
{
wrap := func(e error) error { return fmt.Errorf("invalid paths: %v", e) }
if v := value.Paths; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
} else {
return wrap(errors.New("must be an object"))
wrap = func(e error) error { return fmt.Errorf("invalid servers: %w", e) }
if v := doc.Servers; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
}
{
wrap := func(e error) error { return fmt.Errorf("invalid security: %v", e) }
if v := value.Security; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
wrap = func(e error) error { return fmt.Errorf("invalid tags: %w", e) }
if v := doc.Tags; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
}
{
wrap := func(e error) error { return fmt.Errorf("invalid servers: %v", e) }
if v := value.Servers; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
wrap = func(e error) error { return fmt.Errorf("invalid external docs: %w", e) }
if v := doc.ExternalDocs; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
}
{
wrap := func(e error) error { return fmt.Errorf("invalid tags: %w", e) }
if v := value.Tags; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
}
}
{
wrap := func(e error) error { return fmt.Errorf("invalid external docs: %w", e) }
if v := value.ExternalDocs; v != nil {
if err := v.Validate(ctx); err != nil {
return wrap(err)
}
}
}
return nil
return validateExtensions(ctx, doc.Extensions)
}

View file

@ -2,18 +2,19 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
)
// Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object
type Operation struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
// Optional tags for documentation.
Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"`
@ -34,7 +35,7 @@ type Operation struct {
RequestBody *RequestBodyRef `json:"requestBody,omitempty" yaml:"requestBody,omitempty"`
// Responses.
Responses Responses `json:"responses" yaml:"responses"` // Required
Responses *Responses `json:"responses" yaml:"responses"` // Required
// Optional callbacks
Callbacks Callbacks `json:"callbacks,omitempty" yaml:"callbacks,omitempty"`
@ -56,15 +57,88 @@ func NewOperation() *Operation {
return &Operation{}
}
func (operation *Operation) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(operation)
// MarshalJSON returns the JSON encoding of Operation.
func (operation Operation) MarshalJSON() ([]byte, error) {
x, err := operation.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of Operation.
func (operation Operation) MarshalYAML() (any, error) {
m := make(map[string]any, 12+len(operation.Extensions))
for k, v := range operation.Extensions {
m[k] = v
}
if x := operation.Tags; len(x) != 0 {
m["tags"] = x
}
if x := operation.Summary; x != "" {
m["summary"] = x
}
if x := operation.Description; x != "" {
m["description"] = x
}
if x := operation.OperationID; x != "" {
m["operationId"] = x
}
if x := operation.Parameters; len(x) != 0 {
m["parameters"] = x
}
if x := operation.RequestBody; x != nil {
m["requestBody"] = x
}
m["responses"] = operation.Responses
if x := operation.Callbacks; len(x) != 0 {
m["callbacks"] = x
}
if x := operation.Deprecated; x {
m["deprecated"] = x
}
if x := operation.Security; x != nil {
m["security"] = x
}
if x := operation.Servers; x != nil {
m["servers"] = x
}
if x := operation.ExternalDocs; x != nil {
m["externalDocs"] = x
}
return m, nil
}
// UnmarshalJSON sets Operation to a copy of data.
func (operation *Operation) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, operation)
type OperationBis Operation
var x OperationBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "tags")
delete(x.Extensions, "summary")
delete(x.Extensions, "description")
delete(x.Extensions, "operationId")
delete(x.Extensions, "parameters")
delete(x.Extensions, "requestBody")
delete(x.Extensions, "responses")
delete(x.Extensions, "callbacks")
delete(x.Extensions, "deprecated")
delete(x.Extensions, "security")
delete(x.Extensions, "servers")
delete(x.Extensions, "externalDocs")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*operation = Operation(x)
return nil
}
func (operation Operation) JSONLookup(token string) (interface{}, error) {
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
func (operation Operation) JSONLookup(token string) (any, error) {
switch token {
case "requestBody":
if operation.RequestBody != nil {
@ -97,53 +171,54 @@ func (operation Operation) JSONLookup(token string) (interface{}, error) {
return operation.ExternalDocs, nil
}
v, _, err := jsonpointer.GetForToken(operation.ExtensionProps, token)
v, _, err := jsonpointer.GetForToken(operation.Extensions, token)
return v, err
}
func (operation *Operation) AddParameter(p *Parameter) {
operation.Parameters = append(operation.Parameters, &ParameterRef{
Value: p,
})
operation.Parameters = append(operation.Parameters, &ParameterRef{Value: p})
}
func (operation *Operation) AddResponse(status int, response *Response) {
responses := operation.Responses
if responses == nil {
responses = NewResponses()
operation.Responses = responses
}
code := "default"
if status != 0 {
if 0 < status && status < 1000 {
code = strconv.FormatInt(int64(status), 10)
}
responses[code] = &ResponseRef{
Value: response,
if operation.Responses == nil {
operation.Responses = NewResponses()
}
operation.Responses.Set(code, &ResponseRef{Value: response})
}
func (value *Operation) Validate(ctx context.Context) error {
if v := value.Parameters; v != nil {
// Validate returns an error if Operation does not comply with the OpenAPI spec.
func (operation *Operation) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if v := operation.Parameters; v != nil {
if err := v.Validate(ctx); err != nil {
return err
}
}
if v := value.RequestBody; v != nil {
if v := operation.RequestBody; v != nil {
if err := v.Validate(ctx); err != nil {
return err
}
}
if v := value.Responses; v != nil {
if v := operation.Responses; v != nil {
if err := v.Validate(ctx); err != nil {
return err
}
} else {
return errors.New("value of responses must be an object")
}
if v := value.ExternalDocs; v != nil {
if v := operation.ExternalDocs; v != nil {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("invalid external docs: %w", err)
}
}
return nil
return validateExtensions(ctx, operation.Extensions)
}

View file

@ -0,0 +1,17 @@
package openapi3
const originKey = "__origin__"
// Origin contains the origin of a collection.
// Key is the location of the collection itself.
// Fields is a map of the location of each field in the collection.
type Origin struct {
Key *Location `json:"key,omitempty" yaml:"key,omitempty"`
Fields map[string]Location `json:"fields,omitempty" yaml:"fields,omitempty"`
}
// Location is a struct that contains the location of a field.
type Location struct {
Line int `json:"line,omitempty" yaml:"line,omitempty"`
Column int `json:"column,omitempty" yaml:"column,omitempty"`
}

View file

@ -2,47 +2,31 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
)
type ParametersMap map[string]*ParameterRef
var _ jsonpointer.JSONPointable = (*ParametersMap)(nil)
func (p ParametersMap) JSONLookup(token string) (interface{}, error) {
ref, ok := p[token]
if ref == nil || ok == false {
return nil, fmt.Errorf("object has no field %q", token)
}
if ref.Ref != "" {
return &Ref{Ref: ref.Ref}, nil
}
return ref.Value, nil
}
// Parameters is specified by OpenAPI/Swagger 3.0 standard.
type Parameters []*ParameterRef
var _ jsonpointer.JSONPointable = (*Parameters)(nil)
func (p Parameters) JSONLookup(token string) (interface{}, error) {
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
func (p Parameters) JSONLookup(token string) (any, error) {
index, err := strconv.Atoi(token)
if err != nil {
return nil, err
}
if index < 0 || index >= len(p) {
return nil, fmt.Errorf("index %d out of bounds of array of length %d", index, len(p))
}
ref := p[index]
if ref != nil && ref.Ref != "" {
return &Ref{Ref: ref.Ref}, nil
}
@ -64,10 +48,13 @@ func (parameters Parameters) GetByInAndName(in string, name string) *Parameter {
return nil
}
func (value Parameters) Validate(ctx context.Context) error {
// Validate returns an error if Parameters does not comply with the OpenAPI spec.
func (parameters Parameters) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
dupes := make(map[string]struct{})
for _, item := range value {
if v := item.Value; v != nil {
for _, parameterRef := range parameters {
if v := parameterRef.Value; v != nil {
key := v.In + ":" + v.Name
if _, ok := dupes[key]; ok {
return fmt.Errorf("more than one %q parameter has name %q", v.In, v.Name)
@ -75,7 +62,7 @@ func (value Parameters) Validate(ctx context.Context) error {
dupes[key] = struct{}{}
}
if err := item.Validate(ctx); err != nil {
if err := parameterRef.Validate(ctx); err != nil {
return err
}
}
@ -83,23 +70,24 @@ func (value Parameters) Validate(ctx context.Context) error {
}
// Parameter is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameterObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#parameter-object
type Parameter struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
In string `json:"in,omitempty" yaml:"in,omitempty"`
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"`
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)
@ -161,50 +149,133 @@ func (parameter *Parameter) WithSchema(value *Schema) *Parameter {
return parameter
}
func (parameter *Parameter) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(parameter)
// MarshalJSON returns the JSON encoding of Parameter.
func (parameter Parameter) MarshalJSON() ([]byte, error) {
x, err := parameter.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
func (parameter *Parameter) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, parameter)
}
func (value Parameter) JSONLookup(token string) (interface{}, error) {
switch token {
case "schema":
if value.Schema != nil {
if value.Schema.Ref != "" {
return &Ref{Ref: value.Schema.Ref}, nil
}
return value.Schema.Value, nil
}
case "name":
return value.Name, nil
case "in":
return value.In, nil
case "description":
return value.Description, nil
case "style":
return value.Style, nil
case "explode":
return value.Explode, nil
case "allowEmptyValue":
return value.AllowEmptyValue, nil
case "allowReserved":
return value.AllowReserved, nil
case "deprecated":
return value.Deprecated, nil
case "required":
return value.Required, nil
case "example":
return value.Example, nil
case "examples":
return value.Examples, nil
case "content":
return value.Content, nil
// MarshalYAML returns the YAML encoding of Parameter.
func (parameter Parameter) MarshalYAML() (any, error) {
m := make(map[string]any, 13+len(parameter.Extensions))
for k, v := range parameter.Extensions {
m[k] = v
}
v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token)
if x := parameter.Name; x != "" {
m["name"] = x
}
if x := parameter.In; x != "" {
m["in"] = x
}
if x := parameter.Description; x != "" {
m["description"] = x
}
if x := parameter.Style; x != "" {
m["style"] = x
}
if x := parameter.Explode; x != nil {
m["explode"] = x
}
if x := parameter.AllowEmptyValue; x {
m["allowEmptyValue"] = x
}
if x := parameter.AllowReserved; x {
m["allowReserved"] = x
}
if x := parameter.Deprecated; x {
m["deprecated"] = x
}
if x := parameter.Required; x {
m["required"] = x
}
if x := parameter.Schema; x != nil {
m["schema"] = x
}
if x := parameter.Example; x != nil {
m["example"] = x
}
if x := parameter.Examples; len(x) != 0 {
m["examples"] = x
}
if x := parameter.Content; len(x) != 0 {
m["content"] = x
}
return m, nil
}
// UnmarshalJSON sets Parameter to a copy of data.
func (parameter *Parameter) UnmarshalJSON(data []byte) error {
type ParameterBis Parameter
var x ParameterBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "name")
delete(x.Extensions, "in")
delete(x.Extensions, "description")
delete(x.Extensions, "style")
delete(x.Extensions, "explode")
delete(x.Extensions, "allowEmptyValue")
delete(x.Extensions, "allowReserved")
delete(x.Extensions, "deprecated")
delete(x.Extensions, "required")
delete(x.Extensions, "schema")
delete(x.Extensions, "example")
delete(x.Extensions, "examples")
delete(x.Extensions, "content")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*parameter = Parameter(x)
return nil
}
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
func (parameter Parameter) JSONLookup(token string) (any, error) {
switch token {
case "schema":
if parameter.Schema != nil {
if parameter.Schema.Ref != "" {
return &Ref{Ref: parameter.Schema.Ref}, nil
}
return parameter.Schema.Value, nil
}
case "name":
return parameter.Name, nil
case "in":
return parameter.In, nil
case "description":
return parameter.Description, nil
case "style":
return parameter.Style, nil
case "explode":
return parameter.Explode, nil
case "allowEmptyValue":
return parameter.AllowEmptyValue, nil
case "allowReserved":
return parameter.AllowReserved, nil
case "deprecated":
return parameter.Deprecated, nil
case "required":
return parameter.Required, nil
case "example":
return parameter.Example, nil
case "examples":
return parameter.Examples, nil
case "content":
return parameter.Content, nil
}
v, _, err := jsonpointer.GetForToken(parameter.Extensions, token)
return v, err
}
@ -238,11 +309,14 @@ func (parameter *Parameter) SerializationMethod() (*SerializationMethod, error)
}
}
func (value *Parameter) Validate(ctx context.Context) error {
if value.Name == "" {
// Validate returns an error if Parameter does not comply with the OpenAPI spec.
func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if parameter.Name == "" {
return errors.New("parameter name can't be blank")
}
in := value.In
in := parameter.In
switch in {
case
ParameterInPath,
@ -250,61 +324,101 @@ func (value *Parameter) Validate(ctx context.Context) error {
ParameterInHeader,
ParameterInCookie:
default:
return fmt.Errorf("parameter can't have 'in' value %q", value.In)
return fmt.Errorf("parameter can't have 'in' value %q", parameter.In)
}
if in == ParameterInPath && !value.Required {
return fmt.Errorf("path parameter %q must be required", value.Name)
if in == ParameterInPath && !parameter.Required {
return fmt.Errorf("path parameter %q must be required", parameter.Name)
}
// Validate a parameter's serialization method.
sm, err := value.SerializationMethod()
sm, err := parameter.SerializationMethod()
if err != nil {
return err
}
var smSupported bool
switch {
case value.In == ParameterInPath && sm.Style == SerializationSimple && !sm.Explode,
value.In == ParameterInPath && sm.Style == SerializationSimple && sm.Explode,
value.In == ParameterInPath && sm.Style == SerializationLabel && !sm.Explode,
value.In == ParameterInPath && sm.Style == SerializationLabel && sm.Explode,
value.In == ParameterInPath && sm.Style == SerializationMatrix && !sm.Explode,
value.In == ParameterInPath && sm.Style == SerializationMatrix && sm.Explode,
case parameter.In == ParameterInPath && sm.Style == SerializationSimple && !sm.Explode,
parameter.In == ParameterInPath && sm.Style == SerializationSimple && sm.Explode,
parameter.In == ParameterInPath && sm.Style == SerializationLabel && !sm.Explode,
parameter.In == ParameterInPath && sm.Style == SerializationLabel && sm.Explode,
parameter.In == ParameterInPath && sm.Style == SerializationMatrix && !sm.Explode,
parameter.In == ParameterInPath && sm.Style == SerializationMatrix && sm.Explode,
value.In == ParameterInQuery && sm.Style == SerializationForm && sm.Explode,
value.In == ParameterInQuery && sm.Style == SerializationForm && !sm.Explode,
value.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && sm.Explode,
value.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && !sm.Explode,
value.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && sm.Explode,
value.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && !sm.Explode,
value.In == ParameterInQuery && sm.Style == SerializationDeepObject && sm.Explode,
parameter.In == ParameterInQuery && sm.Style == SerializationForm && sm.Explode,
parameter.In == ParameterInQuery && sm.Style == SerializationForm && !sm.Explode,
parameter.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && sm.Explode,
parameter.In == ParameterInQuery && sm.Style == SerializationSpaceDelimited && !sm.Explode,
parameter.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && sm.Explode,
parameter.In == ParameterInQuery && sm.Style == SerializationPipeDelimited && !sm.Explode,
parameter.In == ParameterInQuery && sm.Style == SerializationDeepObject && sm.Explode,
value.In == ParameterInHeader && sm.Style == SerializationSimple && !sm.Explode,
value.In == ParameterInHeader && sm.Style == SerializationSimple && sm.Explode,
parameter.In == ParameterInHeader && sm.Style == SerializationSimple && !sm.Explode,
parameter.In == ParameterInHeader && sm.Style == SerializationSimple && sm.Explode,
value.In == ParameterInCookie && sm.Style == SerializationForm && !sm.Explode,
value.In == ParameterInCookie && sm.Style == SerializationForm && sm.Explode:
parameter.In == ParameterInCookie && sm.Style == SerializationForm && !sm.Explode,
parameter.In == ParameterInCookie && sm.Style == SerializationForm && sm.Explode:
smSupported = true
}
if !smSupported {
e := fmt.Errorf("serialization method with style=%q and explode=%v is not supported by a %s parameter", sm.Style, sm.Explode, in)
return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, e)
return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, e)
}
if (value.Schema == nil) == (value.Content == nil) {
if (parameter.Schema == nil) == (len(parameter.Content) == 0) {
e := errors.New("parameter must contain exactly one of content and schema")
return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, e)
return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, e)
}
if schema := value.Schema; schema != nil {
if err := schema.Validate(ctx); err != nil {
return fmt.Errorf("parameter %q schema is invalid: %v", value.Name, err)
if content := parameter.Content; content != nil {
e := errors.New("parameter content must only contain one entry")
if len(content) > 1 {
return fmt.Errorf("parameter %q content is invalid: %w", parameter.Name, e)
}
if err := content.Validate(ctx); err != nil {
return fmt.Errorf("parameter %q content is invalid: %w", parameter.Name, err)
}
}
if content := value.Content; content != nil {
if err := content.Validate(ctx); err != nil {
return fmt.Errorf("parameter %q content is invalid: %v", value.Name, err)
if schema := parameter.Schema; schema != nil {
if err := schema.Validate(ctx); err != nil {
return fmt.Errorf("parameter %q schema is invalid: %w", parameter.Name, err)
}
if parameter.Example != nil && parameter.Examples != nil {
return fmt.Errorf("parameter %q example and examples are mutually exclusive", parameter.Name)
}
if vo := getValidationOptions(ctx); vo.examplesValidationDisabled {
return nil
}
if example := parameter.Example; example != nil {
if err := validateExampleValue(ctx, example, schema.Value); err != nil {
return fmt.Errorf("invalid example: %w", err)
}
} else if examples := parameter.Examples; examples != nil {
names := make([]string, 0, len(examples))
for name := range examples {
names = append(names, name)
}
sort.Strings(names)
for _, k := range names {
v := examples[k]
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("%s: %w", k, err)
}
if err := validateExampleValue(ctx, v.Value.Value, schema.Value); err != nil {
return fmt.Errorf("%s: %w", k, err)
}
}
}
}
return nil
return validateExtensions(ctx, parameter.Extensions)
}
// UnmarshalJSON sets ParametersMap to a copy of data.
func (parametersMap *ParametersMap) UnmarshalJSON(data []byte) (err error) {
*parametersMap, _, err = unmarshalStringMapP[ParameterRef](data)
return
}

View file

@ -2,16 +2,17 @@ package openapi3
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/getkin/kin-openapi/jsoninfo"
"sort"
)
// PathItem is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathItemObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#path-item-object
type PathItem struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
@ -29,16 +30,99 @@ type PathItem struct {
Parameters Parameters `json:"parameters,omitempty" yaml:"parameters,omitempty"`
}
func (pathItem *PathItem) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(pathItem)
// MarshalJSON returns the JSON encoding of PathItem.
func (pathItem PathItem) MarshalJSON() ([]byte, error) {
x, err := pathItem.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of PathItem.
func (pathItem PathItem) MarshalYAML() (any, error) {
if ref := pathItem.Ref; ref != "" {
return Ref{Ref: ref}, nil
}
m := make(map[string]any, 13+len(pathItem.Extensions))
for k, v := range pathItem.Extensions {
m[k] = v
}
if x := pathItem.Summary; x != "" {
m["summary"] = x
}
if x := pathItem.Description; x != "" {
m["description"] = x
}
if x := pathItem.Connect; x != nil {
m["connect"] = x
}
if x := pathItem.Delete; x != nil {
m["delete"] = x
}
if x := pathItem.Get; x != nil {
m["get"] = x
}
if x := pathItem.Head; x != nil {
m["head"] = x
}
if x := pathItem.Options; x != nil {
m["options"] = x
}
if x := pathItem.Patch; x != nil {
m["patch"] = x
}
if x := pathItem.Post; x != nil {
m["post"] = x
}
if x := pathItem.Put; x != nil {
m["put"] = x
}
if x := pathItem.Trace; x != nil {
m["trace"] = x
}
if x := pathItem.Servers; len(x) != 0 {
m["servers"] = x
}
if x := pathItem.Parameters; len(x) != 0 {
m["parameters"] = x
}
return m, nil
}
// UnmarshalJSON sets PathItem to a copy of data.
func (pathItem *PathItem) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, pathItem)
type PathItemBis PathItem
var x PathItemBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "$ref")
delete(x.Extensions, "summary")
delete(x.Extensions, "description")
delete(x.Extensions, "connect")
delete(x.Extensions, "delete")
delete(x.Extensions, "get")
delete(x.Extensions, "head")
delete(x.Extensions, "options")
delete(x.Extensions, "patch")
delete(x.Extensions, "post")
delete(x.Extensions, "put")
delete(x.Extensions, "trace")
delete(x.Extensions, "servers")
delete(x.Extensions, "parameters")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*pathItem = PathItem(x)
return nil
}
func (pathItem *PathItem) Operations() map[string]*Operation {
operations := make(map[string]*Operation, 4)
operations := make(map[string]*Operation)
if v := pathItem.Connect; v != nil {
operations[http.MethodConnect] = v
}
@ -119,11 +203,48 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) {
}
}
func (value *PathItem) Validate(ctx context.Context) error {
for _, operation := range value.Operations() {
// Validate returns an error if PathItem does not comply with the OpenAPI spec.
func (pathItem *PathItem) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
operations := pathItem.Operations()
methods := make([]string, 0, len(operations))
for method := range operations {
methods = append(methods, method)
}
sort.Strings(methods)
for _, method := range methods {
operation := operations[method]
if err := operation.Validate(ctx); err != nil {
return fmt.Errorf("invalid operation %s: %v", method, err)
}
}
if v := pathItem.Parameters; v != nil {
if err := v.Validate(ctx); err != nil {
return err
}
}
return nil
return validateExtensions(ctx, pathItem.Extensions)
}
// isEmpty's introduced in 546590b1
func (pathItem *PathItem) isEmpty() bool {
// NOTE: ignores pathItem.Extensions
// NOTE: ignores pathItem.Ref
return pathItem.Summary == "" &&
pathItem.Description == "" &&
pathItem.Connect == nil &&
pathItem.Delete == nil &&
pathItem.Get == nil &&
pathItem.Head == nil &&
pathItem.Options == nil &&
pathItem.Patch == nil &&
pathItem.Post == nil &&
pathItem.Put == nil &&
pathItem.Trace == nil &&
len(pathItem.Servers) == 0 &&
len(pathItem.Parameters) == 0
}

View file

@ -3,23 +3,60 @@ package openapi3
import (
"context"
"fmt"
"sort"
"strings"
)
// 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 map[string]*PathItem
type Paths struct {
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
func (value Paths) Validate(ctx context.Context) error {
normalizedPaths := make(map[string]string)
for path, pathItem := range value {
m map[string]*PathItem
}
// NewPaths builds a paths object with path items in insertion order.
func NewPaths(opts ...NewPathsOption) *Paths {
paths := NewPathsWithCapacity(len(opts))
for _, opt := range opts {
opt(paths)
}
return paths
}
// NewPathsOption describes options to NewPaths func
type NewPathsOption func(*Paths)
// WithPath adds a named path item
func WithPath(path string, pathItem *PathItem) NewPathsOption {
return func(paths *Paths) {
if p := pathItem; p != nil && path != "" {
paths.Set(path, p)
}
}
}
// Validate returns an error if Paths does not comply with the OpenAPI spec.
func (paths *Paths) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
normalizedPaths := make(map[string]string, paths.Len())
keys := make([]string, 0, paths.Len())
for key := range paths.Map() {
keys = append(keys, key)
}
sort.Strings(keys)
for _, path := range keys {
pathItem := paths.Value(path)
if path == "" || path[0] != '/' {
return fmt.Errorf("path %q does not start with a forward slash (/)", path)
}
if pathItem == nil {
value[path] = &PathItem{}
pathItem = value[path]
pathItem = &PathItem{}
paths.Set(path, pathItem)
}
normalizedPath, _, varsInPath := normalizeTemplatedPath(path)
@ -36,7 +73,14 @@ func (value Paths) Validate(ctx context.Context) error {
}
}
}
for method, operation := range pathItem.Operations() {
operations := pathItem.Operations()
methods := make([]string, 0, len(operations))
for method := range operations {
methods = append(methods, method)
}
sort.Strings(methods)
for _, method := range methods {
operation := operations[method]
var setParams []string
for _, parameterRef := range operation.Parameters {
if parameterRef != nil {
@ -80,15 +124,46 @@ func (value Paths) Validate(ctx context.Context) error {
}
if err := pathItem.Validate(ctx); err != nil {
return err
return fmt.Errorf("invalid path %s: %v", path, err)
}
}
if err := value.validateUniqueOperationIDs(); err != nil {
if err := paths.validateUniqueOperationIDs(); err != nil {
return err
}
return nil
return validateExtensions(ctx, paths.Extensions)
}
// InMatchingOrder returns paths in the order they are matched against URLs.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#paths-object
// When matching URLs, concrete (non-templated) paths would be matched
// before their templated counterparts.
func (paths *Paths) InMatchingOrder() []string {
// NOTE: sorting by number of variables ASC then by descending lexicographical
// order seems to be a good heuristic.
if paths.Len() == 0 {
return nil
}
vars := make(map[int][]string)
max := 0
for path := range paths.Map() {
count := strings.Count(path, "}")
vars[count] = append(vars[count], path)
if count > max {
max = count
}
}
ordered := make([]string, 0, paths.Len())
for c := 0; c <= max; c++ {
if ps, ok := vars[c]; ok {
sort.Sort(sort.Reverse(sort.StringSlice(ps)))
ordered = append(ordered, ps...)
}
}
return ordered
}
// Find returns a path that matches the key.
@ -97,21 +172,21 @@ func (value Paths) Validate(ctx context.Context) error {
//
// For example:
//
// paths := openapi3.Paths {
// "/person/{personName}": &openapi3.PathItem{},
// }
// pathItem := path.Find("/person/{name}")
// paths := openapi3.Paths {
// "/person/{personName}": &openapi3.PathItem{},
// }
// pathItem := path.Find("/person/{name}")
//
// would return the correct path item.
func (paths Paths) Find(key string) *PathItem {
func (paths *Paths) Find(key string) *PathItem {
// Try directly access the map
pathItem := paths[key]
pathItem := paths.Value(key)
if pathItem != nil {
return pathItem
}
normalizedPath, expected, _ := normalizeTemplatedPath(key)
for path, pathItem := range paths {
for path, pathItem := range paths.Map() {
pathNormalized, got, _ := normalizeTemplatedPath(path)
if got == expected && pathNormalized == normalizedPath {
return pathItem
@ -120,9 +195,9 @@ func (paths Paths) Find(key string) *PathItem {
return nil
}
func (value Paths) validateUniqueOperationIDs() error {
func (paths *Paths) validateUniqueOperationIDs() error {
operationIDs := make(map[string]string)
for urlPath, pathItem := range value {
for urlPath, pathItem := range paths.Map() {
if pathItem == nil {
continue
}

10
vendor/github.com/getkin/kin-openapi/openapi3/ref.go generated vendored Normal file
View file

@ -0,0 +1,10 @@
package openapi3
//go:generate go run refsgenerator.go
// Ref is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#reference-object
type Ref struct {
Ref string `json:"$ref" yaml:"$ref"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
}

File diff suppressed because it is too large Load diff

153
vendor/github.com/getkin/kin-openapi/openapi3/refs.tmpl generated vendored Normal file
View file

@ -0,0 +1,153 @@
// Code generated by go generate; DO NOT EDIT.
package {{ .Package }}
import (
"context"
"encoding/json"
"fmt"
"net/url"
"sort"
"strings"
"github.com/go-openapi/jsonpointer"
"github.com/perimeterx/marshmallow"
)
{{ range $type := .Types }}
// {{ $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.Name }}Ref struct {
// Extensions only captures fields starting with 'x-' as no other fields
// are allowed by the openapi spec.
Extensions map[string]any
Origin *Origin
Ref string
Value *{{ $type.Name }}
extra []string
refPath *url.URL
}
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 }
// CollectionName returns the JSON string used for a collection of these components.
func (x *{{ $type.Name }}Ref) CollectionName() string { return "{{ $type.CollectionName }}" }
// RefPath returns the path of the $ref relative to the root document.
func (x *{{ $type.Name }}Ref) RefPath() *url.URL { return copyURI(x.refPath) }
func (x *{{ $type.Name }}Ref) setRefPath(u *url.URL) {
// Once the refPath is set don't override. References can be loaded
// multiple times not all with access to the correct path info.
if x.refPath != nil {
return
}
x.refPath = copyURI(u)
}
// MarshalYAML returns the YAML encoding of {{ $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.Name }}Ref.
func (x {{ $type.Name }}Ref) MarshalJSON() ([]byte, error) {
y, err := x.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(y)
}
// 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
x.Origin = refOnly.Origin
if len(extra) != 0 {
x.extra = make([]string, 0, len(extra))
for key := range extra {
x.extra = append(x.extra, key)
}
sort.Strings(x.extra)
for k := range extra {
if !strings.HasPrefix(k, "x-") {
delete(extra, k)
}
}
if len(extra) != 0 {
x.Extensions = extra
}
}
return nil
}
return json.Unmarshal(data, &x.Value)
}
// 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 {
allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
for _, ex := range extra {
if allowed != nil {
if _, ok := allowed[ex]; ok {
continue
}
}
// extras in the Extensions checked below
if _, ok := x.Extensions[ex]; !ok {
extras = append(extras, ex)
}
}
}
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.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
}
{{ end -}}

View file

@ -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 -}}

View file

@ -2,33 +2,15 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
)
type RequestBodies map[string]*RequestBodyRef
var _ jsonpointer.JSONPointable = (*RequestBodyRef)(nil)
func (r RequestBodies) JSONLookup(token string) (interface{}, error) {
ref, ok := r[token]
if ok == false {
return nil, fmt.Errorf("object has no field %q", token)
}
if ref != nil && ref.Ref != "" {
return &Ref{Ref: ref.Ref}, nil
}
return ref.Value, nil
}
// RequestBody is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#requestBodyObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#request-body-object
type RequestBody struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Required bool `json:"required,omitempty" yaml:"required,omitempty"`
@ -92,17 +74,73 @@ func (requestBody *RequestBody) GetMediaType(mediaType string) *MediaType {
return m[mediaType]
}
func (requestBody *RequestBody) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(requestBody)
// MarshalJSON returns the JSON encoding of RequestBody.
func (requestBody RequestBody) MarshalJSON() ([]byte, error) {
x, err := requestBody.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of RequestBody.
func (requestBody RequestBody) MarshalYAML() (any, error) {
m := make(map[string]any, 3+len(requestBody.Extensions))
for k, v := range requestBody.Extensions {
m[k] = v
}
if x := requestBody.Description; x != "" {
m["description"] = requestBody.Description
}
if x := requestBody.Required; x {
m["required"] = x
}
if x := requestBody.Content; true {
m["content"] = x
}
return m, nil
}
// UnmarshalJSON sets RequestBody to a copy of data.
func (requestBody *RequestBody) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, requestBody)
type RequestBodyBis RequestBody
var x RequestBodyBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "description")
delete(x.Extensions, "required")
delete(x.Extensions, "content")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*requestBody = RequestBody(x)
return nil
}
func (value *RequestBody) Validate(ctx context.Context) error {
if value.Content == nil {
// Validate returns an error if RequestBody does not comply with the OpenAPI spec.
func (requestBody *RequestBody) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if requestBody.Content == nil {
return errors.New("content of the request body is required")
}
return value.Content.Validate(ctx)
if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled {
vo.examplesValidationAsReq, vo.examplesValidationAsRes = true, false
}
if err := requestBody.Content.Validate(ctx); err != nil {
return err
}
return validateExtensions(ctx, requestBody.Extensions)
}
// UnmarshalJSON sets RequestBodies to a copy of data.
func (requestBodies *RequestBodies) UnmarshalJSON(data []byte) (err error) {
*requestBodies, _, err = unmarshalStringMapP[RequestBodyRef](data)
return
}

View file

@ -2,62 +2,108 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
)
// Responses is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responsesObject
type Responses map[string]*ResponseRef
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responses-object
type Responses struct {
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"-" yaml:"-"`
var _ jsonpointer.JSONPointable = (*Responses)(nil)
func NewResponses() Responses {
r := make(Responses)
r["default"] = &ResponseRef{Value: NewResponse().WithDescription("")}
return r
m map[string]*ResponseRef
}
func (responses Responses) Default() *ResponseRef {
return responses["default"]
}
func (responses Responses) Get(status int) *ResponseRef {
return responses[strconv.FormatInt(int64(status), 10)]
}
func (value Responses) Validate(ctx context.Context) error {
if len(value) == 0 {
return errors.New("the responses object MUST contain at least one response code")
// NewResponses builds a responses object with response objects in insertion order.
// Given no arguments, NewResponses returns a valid responses object containing a default match-all reponse.
func NewResponses(opts ...NewResponsesOption) *Responses {
if len(opts) == 0 {
return NewResponses(WithName("default", NewResponse().WithDescription("")))
}
for _, v := range value {
if err := v.Validate(ctx); err != nil {
return err
responses := NewResponsesWithCapacity(len(opts))
for _, opt := range opts {
opt(responses)
}
return responses
}
// NewResponsesOption describes options to NewResponses func
type NewResponsesOption func(*Responses)
// WithStatus adds a status code keyed ResponseRef
func WithStatus(status int, responseRef *ResponseRef) NewResponsesOption {
return func(responses *Responses) {
if r := responseRef; r != nil {
code := strconv.FormatInt(int64(status), 10)
responses.Set(code, r)
}
}
}
// WithName adds a name-keyed Response
func WithName(name string, response *Response) NewResponsesOption {
return func(responses *Responses) {
if r := response; r != nil && name != "" {
responses.Set(name, &ResponseRef{Value: r})
}
}
}
// Default returns the default response
func (responses *Responses) Default() *ResponseRef {
return responses.Value("default")
}
// Status returns a ResponseRef for the given status
// If an exact match isn't initially found a patterned field is checked using
// the first digit to determine the range (eg: 201 to 2XX)
// See https://spec.openapis.org/oas/v3.0.3#patterned-fields-0
func (responses *Responses) Status(status int) *ResponseRef {
st := strconv.FormatInt(int64(status), 10)
if rref := responses.Value(st); rref != nil {
return rref
}
if 99 < status && status < 600 {
st = string(st[0]) + "XX"
switch st {
case "1XX", "2XX", "3XX", "4XX", "5XX":
return responses.Value(st)
}
}
return nil
}
func (responses Responses) JSONLookup(token string) (interface{}, error) {
ref, ok := responses[token]
if ok == false {
return nil, fmt.Errorf("invalid token reference: %q", token)
// Validate returns an error if Responses does not comply with the OpenAPI spec.
func (responses *Responses) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if responses.Len() == 0 {
return errors.New("the responses object MUST contain at least one response code")
}
if ref != nil && ref.Ref != "" {
return &Ref{Ref: ref.Ref}, nil
keys := make([]string, 0, responses.Len())
for key := range responses.Map() {
keys = append(keys, key)
}
return ref.Value, nil
sort.Strings(keys)
for _, key := range keys {
v := responses.Value(key)
if err := v.Validate(ctx); err != nil {
return err
}
}
return validateExtensions(ctx, responses.Extensions)
}
// Response is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responseObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#response-object
type Response struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Description *string `json:"description,omitempty" yaml:"description,omitempty"`
Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"`
@ -89,34 +135,102 @@ func (response *Response) WithJSONSchemaRef(schema *SchemaRef) *Response {
return response
}
func (response *Response) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(response)
// MarshalJSON returns the JSON encoding of Response.
func (response Response) MarshalJSON() ([]byte, error) {
x, err := response.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of Response.
func (response Response) MarshalYAML() (any, error) {
m := make(map[string]any, 4+len(response.Extensions))
for k, v := range response.Extensions {
m[k] = v
}
if x := response.Description; x != nil {
m["description"] = x
}
if x := response.Headers; len(x) != 0 {
m["headers"] = x
}
if x := response.Content; len(x) != 0 {
m["content"] = x
}
if x := response.Links; len(x) != 0 {
m["links"] = x
}
return m, nil
}
// UnmarshalJSON sets Response to a copy of data.
func (response *Response) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, response)
type ResponseBis Response
var x ResponseBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "description")
delete(x.Extensions, "headers")
delete(x.Extensions, "content")
delete(x.Extensions, "links")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*response = Response(x)
return nil
}
func (value *Response) Validate(ctx context.Context) error {
if value.Description == nil {
// Validate returns an error if Response does not comply with the OpenAPI spec.
func (response *Response) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if response.Description == nil {
return errors.New("a short description of the response is required")
}
if vo := getValidationOptions(ctx); !vo.examplesValidationDisabled {
vo.examplesValidationAsReq, vo.examplesValidationAsRes = false, true
}
if content := value.Content; content != nil {
if content := response.Content; content != nil {
if err := content.Validate(ctx); err != nil {
return err
}
}
for _, header := range value.Headers {
headers := make([]string, 0, len(response.Headers))
for name := range response.Headers {
headers = append(headers, name)
}
sort.Strings(headers)
for _, name := range headers {
header := response.Headers[name]
if err := header.Validate(ctx); err != nil {
return err
}
}
for _, link := range value.Links {
links := make([]string, 0, len(response.Links))
for name := range response.Links {
links = append(links, name)
}
sort.Strings(links)
for _, name := range links {
link := response.Links[name]
if err := link.Validate(ctx); err != nil {
return err
}
}
return nil
return validateExtensions(ctx, response.Extensions)
}
// UnmarshalJSON sets ResponseBodies to a copy of data.
func (responseBodies *ResponseBodies) UnmarshalJSON(data []byte) (err error) {
*responseBodies, _, err = unmarshalStringMapP[ResponseRef](data)
return
}

File diff suppressed because it is too large Load diff

View file

@ -2,104 +2,170 @@ 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 (
// FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122
FormatOfStringForUUIDOfRFC4122 = `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$`
FormatOfStringForUUIDOfRFC4122 = `^(?:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000)$`
// 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[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])$`
// FormatOfStringDateTime is a RFC3339 date-time format regexp, for example "2017-07-21T17:32:28Z".
FormatOfStringDateTime = `^[0-9]{4}-(0[1-9]|10|11|12)-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)[0-9]{2}:[0-9]{2})?$`
)
//FormatCallback custom check on exotic formats
type FormatCallback func(Val string) error
type Format struct {
regexp *regexp.Regexp
callback FormatCallback
}
//SchemaStringFormats allows for validating strings format
var SchemaStringFormats = make(map[string]Format, 8)
//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: %v", name, pattern, err)
panic(err)
}
SchemaStringFormats[name] = Format{regexp: re}
}
// DefineStringFormatCallback adds a validation function for a specific schema format entry
func DefineStringFormatCallback(name string, callback FormatCallback) {
SchemaStringFormats[name] = Format{callback: callback}
}
func validateIP(ip string) error {
parsed := net.ParseIP(ip)
if parsed == nil {
return &SchemaError{
Value: ip,
Reason: "Not an IP address",
}
}
return nil
}
func validateIPv4(ip string) error {
if err := validateIP(ip); err != nil {
return err
}
if !(strings.Count(ip, ":") < 2) {
return &SchemaError{
Value: ip,
Reason: "Not an IPv4 address (it's IPv6)",
}
}
return nil
}
func validateIPv6(ip string) error {
if err := validateIP(ip); err != nil {
return err
}
if !(strings.Count(ip, ":") >= 2) {
return &SchemaError{
Value: ip,
Reason: "Not an IPv6 address (it's IPv4)",
}
}
return nil
}
func init() {
// This pattern catches only some suspiciously wrong-looking email addresses.
// Use DefineStringFormat(...) if you need something stricter.
DefineStringFormat("email", `^[^@]+@[^@<>",\s]+$`)
// Base64
// The pattern supports base64 and b./ase64url. Padding ('=') is supported.
DefineStringFormat("byte", `(^$|^[a-zA-Z0-9+/\-_]*=*$)`)
// date
DefineStringFormat("date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)$`)
// 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})?$`)
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)))
}
// DefineIPv4Format opts in ipv4 format validation on top of OAS 3 spec
func DefineIPv4Format() {
DefineStringFormatCallback("ipv4", validateIPv4)
DefineStringFormatValidator("ipv4", NewIPValidator(true))
}
// DefineIPv6Format opts in ipv6 format validation on top of OAS 3 spec
func DefineIPv6Format() {
DefineStringFormatCallback("ipv6", validateIPv6)
DefineStringFormatValidator("ipv6", NewIPValidator(false))
}
type stringRegexpFormatValidator struct {
re *regexp.Regexp
}
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
}
type callbackValidator[T any] struct {
fn func(T) error
}
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
}
// 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}
}
// 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 stringRegexpFormatValidator{re: re}
}
// 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}
}
// DefineStringFormatValidator defines a custom format validator for a given string format.
func DefineStringFormatValidator(name string, validator StringFormatValidator) {
SchemaStringFormats[name] = validator
}
// 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
}
// DefineStringFormat defines a regexp pattern for a given string format
//
// Deprecated: Use openapi3.DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern)) instead.
func DefineStringFormat(name string, pattern string) {
DefineStringFormatValidator(name, NewRegexpFormatValidator(pattern))
}
// DefineStringFormatCallback defines a callback function for a given string format
//
// Deprecated: Use openapi3.DefineStringFormatValidator(name, NewCallbackValidator(fn)) instead.
func DefineStringFormatCallback(name string, callback func(string) error) {
DefineStringFormatValidator(name, NewCallbackValidator(callback))
}
// 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
}}
}

View file

@ -0,0 +1,35 @@
package openapi3
import (
"fmt"
"regexp"
)
var patRewriteCodepoints = regexp.MustCompile(`(?P<replaced_with_slash_x>\\u)(?P<code>[0-9A-F]{4})`)
// See https://pkg.go.dev/regexp/syntax
func intoGoRegexp(re string) string {
return patRewriteCodepoints.ReplaceAllString(re, `\x{${code}}`)
}
// NOTE: racey WRT [writes to schema.Pattern] vs [reads schema.Pattern then writes to compiledPatterns]
func (schema *Schema) compilePattern(c RegexCompilerFunc) (cp RegexMatcher, err error) {
pattern := schema.Pattern
if c != nil {
cp, err = c(pattern)
} else {
cp, err = regexp.Compile(intoGoRegexp(pattern))
}
if err != nil {
err = &SchemaError{
Schema: schema,
SchemaField: "pattern",
Origin: err,
Reason: fmt.Sprintf("cannot compile pattern %q: %v", pattern, err),
}
return
}
var _ bool = compiledPatterns.CompareAndSwap(pattern, nil, cp)
return
}

View file

@ -1,12 +1,33 @@
package openapi3
import (
"sync"
)
// SchemaValidationOption describes options a user has when validating request / response bodies.
type SchemaValidationOption func(*schemaValidationSettings)
type RegexCompilerFunc func(expr string) (RegexMatcher, error)
type RegexMatcher interface {
MatchString(s string) bool
}
type schemaValidationSettings struct {
failfast bool
multiError bool
asreq, asrep bool // exclusive (XOR) fields
failfast bool
multiError bool
asreq, asrep bool // exclusive (XOR) fields
formatValidationEnabled bool
patternValidationDisabled bool
readOnlyValidationDisabled bool
writeOnlyValidationDisabled bool
regexCompiler RegexCompilerFunc
onceSettingDefaults sync.Once
defaultsSet func()
customizeMessageError func(err *SchemaError) string
}
// FailFast returns schema validation errors quicker.
@ -21,10 +42,47 @@ func MultiErrors() SchemaValidationOption {
func VisitAsRequest() SchemaValidationOption {
return func(s *schemaValidationSettings) { s.asreq, s.asrep = true, false }
}
func VisitAsResponse() SchemaValidationOption {
return func(s *schemaValidationSettings) { s.asreq, s.asrep = false, true }
}
// EnableFormatValidation setting makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification.
func EnableFormatValidation() SchemaValidationOption {
return func(s *schemaValidationSettings) { s.formatValidationEnabled = true }
}
// DisablePatternValidation setting makes Validate not return an error when validating patterns that are not supported by the Go regexp engine.
func DisablePatternValidation() SchemaValidationOption {
return func(s *schemaValidationSettings) { s.patternValidationDisabled = true }
}
// DisableReadOnlyValidation setting makes Validate not return an error when validating properties marked as read-only
func DisableReadOnlyValidation() SchemaValidationOption {
return func(s *schemaValidationSettings) { s.readOnlyValidationDisabled = true }
}
// DisableWriteOnlyValidation setting makes Validate not return an error when validating properties marked as write-only
func DisableWriteOnlyValidation() SchemaValidationOption {
return func(s *schemaValidationSettings) { s.writeOnlyValidationDisabled = true }
}
// DefaultsSet executes the given callback (once) IFF schema validation set default values.
func DefaultsSet(f func()) SchemaValidationOption {
return func(s *schemaValidationSettings) { s.defaultsSet = f }
}
// SetSchemaErrorMessageCustomizer allows to override the schema error message.
// If the passed function returns an empty string, it returns to the previous Error() implementation.
func SetSchemaErrorMessageCustomizer(f func(err *SchemaError) string) SchemaValidationOption {
return func(s *schemaValidationSettings) { s.customizeMessageError = f }
}
// SetSchemaRegexCompiler allows to override the regex implementation used to validate field "pattern".
func SetSchemaRegexCompiler(c RegexCompilerFunc) SchemaValidationOption {
return func(s *schemaValidationSettings) { s.regexCompiler = c }
}
func newSchemaValidationSettings(opts ...SchemaValidationOption) *schemaValidationSettings {
settings := &schemaValidationSettings{}
for _, opt := range opts {

View file

@ -15,9 +15,12 @@ func (srs *SecurityRequirements) With(securityRequirement SecurityRequirement) *
return srs
}
func (value SecurityRequirements) Validate(ctx context.Context) error {
for _, item := range value {
if err := item.Validate(ctx); err != nil {
// Validate returns an error if SecurityRequirements does not comply with the OpenAPI spec.
func (srs SecurityRequirements) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
for _, security := range srs {
if err := security.Validate(ctx); err != nil {
return err
}
}
@ -25,7 +28,7 @@ func (value SecurityRequirements) Validate(ctx context.Context) error {
}
// SecurityRequirement is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securityRequirementObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-requirement-object
type SecurityRequirement map[string][]string
func NewSecurityRequirement() SecurityRequirement {
@ -40,6 +43,15 @@ func (security SecurityRequirement) Authenticate(provider string, scopes ...stri
return security
}
func (value SecurityRequirement) Validate(ctx context.Context) error {
// Validate returns an error if SecurityRequirement does not comply with the OpenAPI spec.
func (security *SecurityRequirement) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
return nil
}
// UnmarshalJSON sets SecurityRequirement to a copy of data.
func (security *SecurityRequirement) UnmarshalJSON(data []byte) (err error) {
*security, _, err = unmarshalStringMap[[]string](data)
return
}

View file

@ -2,33 +2,17 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
"github.com/go-openapi/jsonpointer"
"net/url"
)
type SecuritySchemes map[string]*SecuritySchemeRef
func (s SecuritySchemes) JSONLookup(token string) (interface{}, error) {
ref, ok := s[token]
if ref == nil || ok == false {
return nil, fmt.Errorf("object has no field %q", token)
}
if ref.Ref != "" {
return &Ref{Ref: ref.Ref}, nil
}
return ref.Value, nil
}
var _ jsonpointer.JSONPointable = (*SecuritySchemes)(nil)
// SecurityScheme is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securitySchemeObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#security-scheme-object
type SecurityScheme struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
@ -67,12 +51,70 @@ func NewJWTSecurityScheme() *SecurityScheme {
}
}
func (ss *SecurityScheme) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(ss)
// MarshalJSON returns the JSON encoding of SecurityScheme.
func (ss SecurityScheme) MarshalJSON() ([]byte, error) {
x, err := ss.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of SecurityScheme.
func (ss SecurityScheme) MarshalYAML() (any, error) {
m := make(map[string]any, 8+len(ss.Extensions))
for k, v := range ss.Extensions {
m[k] = v
}
if x := ss.Type; x != "" {
m["type"] = x
}
if x := ss.Description; x != "" {
m["description"] = x
}
if x := ss.Name; x != "" {
m["name"] = x
}
if x := ss.In; x != "" {
m["in"] = x
}
if x := ss.Scheme; x != "" {
m["scheme"] = x
}
if x := ss.BearerFormat; x != "" {
m["bearerFormat"] = x
}
if x := ss.Flows; x != nil {
m["flows"] = x
}
if x := ss.OpenIdConnectUrl; x != "" {
m["openIdConnectUrl"] = x
}
return m, nil
}
// UnmarshalJSON sets SecurityScheme to a copy of data.
func (ss *SecurityScheme) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, ss)
type SecuritySchemeBis SecurityScheme
var x SecuritySchemeBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "type")
delete(x.Extensions, "description")
delete(x.Extensions, "name")
delete(x.Extensions, "in")
delete(x.Extensions, "scheme")
delete(x.Extensions, "bearerFormat")
delete(x.Extensions, "flows")
delete(x.Extensions, "openIdConnectUrl")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*ss = SecurityScheme(x)
return nil
}
func (ss *SecurityScheme) WithType(value string) *SecurityScheme {
@ -105,15 +147,18 @@ func (ss *SecurityScheme) WithBearerFormat(value string) *SecurityScheme {
return ss
}
func (value *SecurityScheme) Validate(ctx context.Context) error {
// Validate returns an error if SecurityScheme does not comply with the OpenAPI spec.
func (ss *SecurityScheme) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
hasIn := false
hasBearerFormat := false
hasFlow := false
switch value.Type {
switch ss.Type {
case "apiKey":
hasIn = true
case "http":
scheme := value.Scheme
scheme := ss.Scheme
switch scheme {
case "bearer":
hasBearerFormat = true
@ -124,54 +169,56 @@ func (value *SecurityScheme) Validate(ctx context.Context) error {
case "oauth2":
hasFlow = true
case "openIdConnect":
if value.OpenIdConnectUrl == "" {
return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", value.Name)
if ss.OpenIdConnectUrl == "" {
return fmt.Errorf("no OIDC URL found for openIdConnect security scheme %q", ss.Name)
}
default:
return fmt.Errorf("security scheme 'type' can't be %q", value.Type)
return fmt.Errorf("security scheme 'type' can't be %q", ss.Type)
}
// Validate "in" and "name"
if hasIn {
switch value.In {
switch ss.In {
case "query", "header", "cookie":
default:
return fmt.Errorf("security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not %q", value.In)
return fmt.Errorf("security scheme of type 'apiKey' should have 'in'. It can be 'query', 'header' or 'cookie', not %q", ss.In)
}
if value.Name == "" {
if ss.Name == "" {
return errors.New("security scheme of type 'apiKey' should have 'name'")
}
} else if len(value.In) > 0 {
return fmt.Errorf("security scheme of type %q can't have 'in'", value.Type)
} else if len(value.Name) > 0 {
return errors.New("security scheme of type 'apiKey' can't have 'name'")
} else if len(ss.In) > 0 {
return fmt.Errorf("security scheme of type %q can't have 'in'", ss.Type)
} else if len(ss.Name) > 0 {
return fmt.Errorf("security scheme of type %q can't have 'name'", ss.Type)
}
// Validate "format"
// "bearerFormat" is an arbitrary string so we only check if the scheme supports it
if !hasBearerFormat && len(value.BearerFormat) > 0 {
return fmt.Errorf("security scheme of type %q can't have 'bearerFormat'", value.Type)
if !hasBearerFormat && len(ss.BearerFormat) > 0 {
return fmt.Errorf("security scheme of type %q can't have 'bearerFormat'", ss.Type)
}
// Validate "flow"
if hasFlow {
flow := value.Flows
flow := ss.Flows
if flow == nil {
return fmt.Errorf("security scheme of type %q should have 'flows'", value.Type)
return fmt.Errorf("security scheme of type %q should have 'flows'", ss.Type)
}
if err := flow.Validate(ctx); err != nil {
return fmt.Errorf("security scheme 'flow' is invalid: %v", err)
return fmt.Errorf("security scheme 'flow' is invalid: %w", err)
}
} else if value.Flows != nil {
return fmt.Errorf("security scheme of type %q can't have 'flows'", value.Type)
} else if ss.Flows != nil {
return fmt.Errorf("security scheme of type %q can't have 'flows'", ss.Type)
}
return nil
return validateExtensions(ctx, ss.Extensions)
}
// OAuthFlows is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowsObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flows-object
type OAuthFlows struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"`
Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"`
@ -188,62 +235,208 @@ const (
oAuthFlowAuthorizationCode
)
func (flows *OAuthFlows) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(flows)
// MarshalJSON returns the JSON encoding of OAuthFlows.
func (flows OAuthFlows) MarshalJSON() ([]byte, error) {
x, err := flows.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of OAuthFlows.
func (flows OAuthFlows) MarshalYAML() (any, error) {
m := make(map[string]any, 4+len(flows.Extensions))
for k, v := range flows.Extensions {
m[k] = v
}
if x := flows.Implicit; x != nil {
m["implicit"] = x
}
if x := flows.Password; x != nil {
m["password"] = x
}
if x := flows.ClientCredentials; x != nil {
m["clientCredentials"] = x
}
if x := flows.AuthorizationCode; x != nil {
m["authorizationCode"] = x
}
return m, nil
}
// UnmarshalJSON sets OAuthFlows to a copy of data.
func (flows *OAuthFlows) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, flows)
type OAuthFlowsBis OAuthFlows
var x OAuthFlowsBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "implicit")
delete(x.Extensions, "password")
delete(x.Extensions, "clientCredentials")
delete(x.Extensions, "authorizationCode")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*flows = OAuthFlows(x)
return nil
}
func (flows *OAuthFlows) Validate(ctx context.Context) error {
// Validate returns an error if OAuthFlows does not comply with the OpenAPI spec.
func (flows *OAuthFlows) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if v := flows.Implicit; v != nil {
return v.Validate(ctx, oAuthFlowTypeImplicit)
if err := v.validate(ctx, oAuthFlowTypeImplicit, opts...); err != nil {
return fmt.Errorf("the OAuth flow 'implicit' is invalid: %w", err)
}
}
if v := flows.Password; v != nil {
return v.Validate(ctx, oAuthFlowTypePassword)
if err := v.validate(ctx, oAuthFlowTypePassword, opts...); err != nil {
return fmt.Errorf("the OAuth flow 'password' is invalid: %w", err)
}
}
if v := flows.ClientCredentials; v != nil {
return v.Validate(ctx, oAuthFlowTypeClientCredentials)
if err := v.validate(ctx, oAuthFlowTypeClientCredentials, opts...); err != nil {
return fmt.Errorf("the OAuth flow 'clientCredentials' is invalid: %w", err)
}
}
if v := flows.AuthorizationCode; v != nil {
return v.Validate(ctx, oAuthFlowAuthorizationCode)
if err := v.validate(ctx, oAuthFlowAuthorizationCode, opts...); err != nil {
return fmt.Errorf("the OAuth flow 'authorizationCode' is invalid: %w", err)
}
}
return errors.New("no OAuth flow is defined")
return validateExtensions(ctx, flows.Extensions)
}
// OAuthFlow is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object
type OAuthFlow struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"`
Scopes map[string]string `json:"scopes" yaml:"scopes"`
AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"`
Scopes StringMap `json:"scopes" yaml:"scopes"` // required
}
func (flow *OAuthFlow) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(flow)
// MarshalJSON returns the JSON encoding of OAuthFlow.
func (flow OAuthFlow) MarshalJSON() ([]byte, error) {
x, err := flow.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of OAuthFlow.
func (flow OAuthFlow) MarshalYAML() (any, error) {
m := make(map[string]any, 4+len(flow.Extensions))
for k, v := range flow.Extensions {
m[k] = v
}
if x := flow.AuthorizationURL; x != "" {
m["authorizationUrl"] = x
}
if x := flow.TokenURL; x != "" {
m["tokenUrl"] = x
}
if x := flow.RefreshURL; x != "" {
m["refreshUrl"] = x
}
m["scopes"] = flow.Scopes
return m, nil
}
// UnmarshalJSON sets OAuthFlow to a copy of data.
func (flow *OAuthFlow) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, flow)
}
type OAuthFlowBis OAuthFlow
var x OAuthFlowBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
func (flow *OAuthFlow) Validate(ctx context.Context, typ oAuthFlowType) error {
if typ == oAuthFlowAuthorizationCode || typ == oAuthFlowTypeImplicit {
if v := flow.AuthorizationURL; v == "" {
return errors.New("an OAuth flow is missing 'authorizationUrl in authorizationCode or implicit '")
}
}
if typ != oAuthFlowTypeImplicit {
if v := flow.TokenURL; v == "" {
return errors.New("an OAuth flow is missing 'tokenUrl in not implicit'")
}
}
if v := flow.Scopes; v == nil {
return errors.New("an OAuth flow is missing 'scopes'")
delete(x.Extensions, originKey)
delete(x.Extensions, "authorizationUrl")
delete(x.Extensions, "tokenUrl")
delete(x.Extensions, "refreshUrl")
delete(x.Extensions, "scopes")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*flow = OAuthFlow(x)
return nil
}
// Validate returns an error if OAuthFlows does not comply with the OpenAPI spec.
func (flow *OAuthFlow) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if v := flow.RefreshURL; v != "" {
if _, err := url.Parse(v); err != nil {
return fmt.Errorf("field 'refreshUrl' is invalid: %w", err)
}
}
if flow.Scopes == nil {
return errors.New("field 'scopes' is missing")
}
return validateExtensions(ctx, flow.Extensions)
}
func (flow *OAuthFlow) validate(ctx context.Context, typ oAuthFlowType, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
typeIn := func(types ...oAuthFlowType) bool {
for _, ty := range types {
if ty == typ {
return true
}
}
return false
}
if in := typeIn(oAuthFlowTypeImplicit, oAuthFlowAuthorizationCode); true {
switch {
case flow.AuthorizationURL == "" && in:
return errors.New("field 'authorizationUrl' is empty or missing")
case flow.AuthorizationURL != "" && !in:
return errors.New("field 'authorizationUrl' should not be set")
case flow.AuthorizationURL != "":
if _, err := url.Parse(flow.AuthorizationURL); err != nil {
return fmt.Errorf("field 'authorizationUrl' is invalid: %w", err)
}
}
}
if in := typeIn(oAuthFlowTypePassword, oAuthFlowTypeClientCredentials, oAuthFlowAuthorizationCode); true {
switch {
case flow.TokenURL == "" && in:
return errors.New("field 'tokenUrl' is empty or missing")
case flow.TokenURL != "" && !in:
return errors.New("field 'tokenUrl' should not be set")
case flow.TokenURL != "":
if _, err := url.Parse(flow.TokenURL); err != nil {
return fmt.Errorf("field 'tokenUrl' is invalid: %w", err)
}
}
}
return flow.Validate(ctx, opts...)
}
// UnmarshalJSON sets SecuritySchemes to a copy of data.
func (securitySchemes *SecuritySchemes) UnmarshalJSON(data []byte) (err error) {
*securitySchemes, _, err = unmarshalStringMapP[SecuritySchemeRef](data)
return
}

View file

@ -2,21 +2,22 @@ package openapi3
import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
"net/url"
"sort"
"strings"
"github.com/getkin/kin-openapi/jsoninfo"
)
// Servers is specified by OpenAPI/Swagger standard version 3.
type Servers []*Server
// Validate ensures servers are per the OpenAPIv3 specification.
func (value Servers) Validate(ctx context.Context) error {
for _, v := range value {
// Validate returns an error if Servers does not comply with the OpenAPI spec.
func (servers Servers) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
for _, v := range servers {
if err := v.Validate(ctx); err != nil {
return err
}
@ -24,6 +25,14 @@ func (value Servers) Validate(ctx context.Context) error {
return nil
}
// BasePath returns the base path of the first server in the list, or /.
func (servers Servers) BasePath() (string, error) {
for _, server := range servers {
return server.BasePath()
}
return "/", nil
}
func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string) {
rawURL := parsedURL.String()
if i := strings.IndexByte(rawURL, '?'); i >= 0 {
@ -39,21 +48,82 @@ 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#serverObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-object
type Server struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
URL string `json:"url" yaml:"url"`
URL string `json:"url" yaml:"url"` // Required
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"`
}
func (server *Server) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(server)
// BasePath returns the base path extracted from the default values of variables, if any.
// Assumes a valid struct (per Validate()).
func (server *Server) BasePath() (string, error) {
if server == nil {
return "/", nil
}
uri := server.URL
for name, svar := range server.Variables {
uri = strings.ReplaceAll(uri, "{"+name+"}", svar.Default)
}
u, err := url.ParseRequestURI(uri)
if err != nil {
return "", err
}
if bp := u.Path; bp != "" {
return bp, nil
}
return "/", nil
}
// MarshalJSON returns the JSON encoding of Server.
func (server Server) MarshalJSON() ([]byte, error) {
x, err := server.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of Server.
func (server Server) MarshalYAML() (any, error) {
m := make(map[string]any, 3+len(server.Extensions))
for k, v := range server.Extensions {
m[k] = v
}
m["url"] = server.URL
if x := server.Description; x != "" {
m["description"] = x
}
if x := server.Variables; len(x) != 0 {
m["variables"] = x
}
return m, nil
}
// UnmarshalJSON sets Server to a copy of data.
func (server *Server) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, server)
type ServerBis Server
var x ServerBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "url")
delete(x.Extensions, "description")
delete(x.Extensions, "variables")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*server = Server(x)
return nil
}
func (server Server) ParameterNames() ([]string, error) {
@ -103,7 +173,7 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) {
} else if ns < 0 {
i = np
} else {
i = int(math.Min(float64(np), float64(ns)))
i = min(np, ns)
}
if i < 0 {
i = len(input)
@ -127,53 +197,109 @@ func (server Server) MatchRawURL(input string) ([]string, string, bool) {
return params, input, true
}
func (value *Server) Validate(ctx context.Context) (err error) {
if value.URL == "" {
// Validate returns an error if Server does not comply with the OpenAPI spec.
func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) (err error) {
ctx = WithValidationOptions(ctx, opts...)
if server.URL == "" {
return errors.New("value of url must be a non-empty string")
}
opening, closing := strings.Count(value.URL, "{"), strings.Count(value.URL, "}")
opening, closing := strings.Count(server.URL, "{"), strings.Count(server.URL, "}")
if opening != closing {
return errors.New("server URL has mismatched { and }")
}
if opening != len(value.Variables) {
if opening != len(server.Variables) {
return errors.New("server has undeclared variables")
}
for name, v := range value.Variables {
if !strings.Contains(value.URL, fmt.Sprintf("{%s}", name)) {
variables := make([]string, 0, len(server.Variables))
for name := range server.Variables {
variables = append(variables, name)
}
sort.Strings(variables)
for _, name := range variables {
v := server.Variables[name]
if !strings.Contains(server.URL, "{"+name+"}") {
return errors.New("server has undeclared variables")
}
if err = v.Validate(ctx); err != nil {
return
}
}
return
return validateExtensions(ctx, server.Extensions)
}
// ServerVariable is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object
type ServerVariable struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"`
Default string `json:"default,omitempty" yaml:"default,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
}
func (serverVariable *ServerVariable) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(serverVariable)
// MarshalJSON returns the JSON encoding of ServerVariable.
func (serverVariable ServerVariable) MarshalJSON() ([]byte, error) {
x, err := serverVariable.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of ServerVariable.
func (serverVariable ServerVariable) MarshalYAML() (any, error) {
m := make(map[string]any, 4+len(serverVariable.Extensions))
for k, v := range serverVariable.Extensions {
m[k] = v
}
if x := serverVariable.Enum; len(x) != 0 {
m["enum"] = x
}
if x := serverVariable.Default; x != "" {
m["default"] = x
}
if x := serverVariable.Description; x != "" {
m["description"] = x
}
return m, nil
}
// UnmarshalJSON sets ServerVariable to a copy of data.
func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, serverVariable)
type ServerVariableBis ServerVariable
var x ServerVariableBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "enum")
delete(x.Extensions, "default")
delete(x.Extensions, "description")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*serverVariable = ServerVariable(x)
return nil
}
func (value *ServerVariable) Validate(ctx context.Context) error {
if value.Default == "" {
data, err := value.MarshalJSON()
// Validate returns an error if ServerVariable does not comply with the OpenAPI spec.
func (serverVariable *ServerVariable) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if serverVariable.Default == "" {
data, err := serverVariable.MarshalJSON()
if err != nil {
return err
}
return fmt.Errorf("field default is required in %s", data)
}
return nil
return validateExtensions(ctx, serverVariable.Extensions)
}

View file

@ -0,0 +1,88 @@
package openapi3
import "encoding/json"
// StringMap is a map[string]string that ignores the origin in the underlying json representation.
type StringMap map[string]string
// UnmarshalJSON sets StringMap to a copy of data.
func (stringMap *StringMap) UnmarshalJSON(data []byte) (err error) {
*stringMap, _, err = unmarshalStringMap[string](data)
return
}
// unmarshalStringMapP unmarshals given json into a map[string]*V
func unmarshalStringMapP[V any](data []byte) (map[string]*V, *Origin, error) {
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
return nil, nil, err
}
origin, err := popOrigin(m, originKey)
if err != nil {
return nil, nil, err
}
result := make(map[string]*V, len(m))
for k, v := range m {
value, err := deepCast[V](v)
if err != nil {
return nil, nil, err
}
result[k] = value
}
return result, origin, nil
}
// unmarshalStringMap unmarshals given json into a map[string]V
func unmarshalStringMap[V any](data []byte) (map[string]V, *Origin, error) {
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
return nil, nil, err
}
origin, err := popOrigin(m, originKey)
if err != nil {
return nil, nil, err
}
result := make(map[string]V, len(m))
for k, v := range m {
value, err := deepCast[V](v)
if err != nil {
return nil, nil, err
}
result[k] = *value
}
return result, origin, nil
}
// deepCast casts any value to a value of type V.
func deepCast[V any](value any) (*V, error) {
data, err := json.Marshal(value)
if err != nil {
return nil, err
}
var result V
if err = json.Unmarshal(data, &result); err != nil {
return nil, err
}
return &result, nil
}
// popOrigin removes the origin from the map and returns it.
func popOrigin(m map[string]any, key string) (*Origin, error) {
if !IncludeOrigin {
return nil, nil
}
origin, err := deepCast[Origin](m[key])
if err != nil {
return nil, err
}
delete(m, key)
return origin, nil
}

View file

@ -2,9 +2,8 @@ package openapi3
import (
"context"
"encoding/json"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
)
// Tags is specified by OpenAPI/Swagger 3.0 standard.
@ -19,7 +18,10 @@ func (tags Tags) Get(name string) *Tag {
return nil
}
func (tags Tags) Validate(ctx context.Context) error {
// Validate returns an error if Tags does not comply with the OpenAPI spec.
func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
for _, v := range tags {
if err := v.Validate(ctx); err != nil {
return err
@ -29,28 +31,71 @@ func (tags Tags) Validate(ctx context.Context) error {
}
// Tag is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tagObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tag-object
type Tag struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
}
func (t *Tag) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(t)
// MarshalJSON returns the JSON encoding of Tag.
func (t Tag) MarshalJSON() ([]byte, error) {
x, err := t.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
// MarshalYAML returns the YAML encoding of Tag.
func (t Tag) MarshalYAML() (any, error) {
m := make(map[string]any, 3+len(t.Extensions))
for k, v := range t.Extensions {
m[k] = v
}
if x := t.Name; x != "" {
m["name"] = x
}
if x := t.Description; x != "" {
m["description"] = x
}
if x := t.ExternalDocs; x != nil {
m["externalDocs"] = x
}
return m, nil
}
// UnmarshalJSON sets Tag to a copy of data.
func (t *Tag) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, t)
type TagBis Tag
var x TagBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "name")
delete(x.Extensions, "description")
delete(x.Extensions, "externalDocs")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*t = Tag(x)
return nil
}
func (t *Tag) Validate(ctx context.Context) error {
// Validate returns an error if Tag does not comply with the OpenAPI spec.
func (t *Tag) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
if v := t.ExternalDocs; v != nil {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("invalid external docs: %w", err)
}
}
return nil
return validateExtensions(ctx, t.Extensions)
}

View file

@ -0,0 +1,142 @@
package openapi3
import "context"
// ValidationOption allows the modification of how the OpenAPI document is validated.
type ValidationOption func(options *ValidationOptions)
// ValidationOptions provides configuration for validating OpenAPI documents.
type ValidationOptions struct {
examplesValidationAsReq, examplesValidationAsRes bool
examplesValidationDisabled bool
schemaDefaultsValidationDisabled bool
schemaFormatValidationEnabled bool
schemaPatternValidationDisabled bool
schemaExtensionsInRefProhibited bool
regexCompilerFunc RegexCompilerFunc
extraSiblingFieldsAllowed map[string]struct{}
}
type validationOptionsKey struct{}
// AllowExtraSiblingFields called as AllowExtraSiblingFields("description") makes Validate not return an error when said field appears next to a $ref.
func AllowExtraSiblingFields(fields ...string) ValidationOption {
return func(options *ValidationOptions) {
if options.extraSiblingFieldsAllowed == nil && len(fields) != 0 {
options.extraSiblingFieldsAllowed = make(map[string]struct{}, len(fields))
}
for _, field := range fields {
options.extraSiblingFieldsAllowed[field] = struct{}{}
}
}
}
// EnableSchemaFormatValidation makes Validate not return an error when validating documents that mention schema formats that are not defined by the OpenAPIv3 specification.
// By default, schema format validation is disabled.
func EnableSchemaFormatValidation() ValidationOption {
return func(options *ValidationOptions) {
options.schemaFormatValidationEnabled = true
}
}
// DisableSchemaFormatValidation does the opposite of EnableSchemaFormatValidation.
// By default, schema format validation is disabled.
func DisableSchemaFormatValidation() ValidationOption {
return func(options *ValidationOptions) {
options.schemaFormatValidationEnabled = false
}
}
// EnableSchemaPatternValidation does the opposite of DisableSchemaPatternValidation.
// By default, schema pattern validation is enabled.
func EnableSchemaPatternValidation() ValidationOption {
return func(options *ValidationOptions) {
options.schemaPatternValidationDisabled = false
}
}
// DisableSchemaPatternValidation makes Validate not return an error when validating patterns that are not supported by the Go regexp engine.
func DisableSchemaPatternValidation() ValidationOption {
return func(options *ValidationOptions) {
options.schemaPatternValidationDisabled = true
}
}
// EnableSchemaDefaultsValidation does the opposite of DisableSchemaDefaultsValidation.
// By default, schema default values are validated against their schema.
func EnableSchemaDefaultsValidation() ValidationOption {
return func(options *ValidationOptions) {
options.schemaDefaultsValidationDisabled = false
}
}
// DisableSchemaDefaultsValidation disables schemas' default field validation.
// By default, schema default values are validated against their schema.
func DisableSchemaDefaultsValidation() ValidationOption {
return func(options *ValidationOptions) {
options.schemaDefaultsValidationDisabled = true
}
}
// EnableExamplesValidation does the opposite of DisableExamplesValidation.
// By default, all schema examples are validated.
func EnableExamplesValidation() ValidationOption {
return func(options *ValidationOptions) {
options.examplesValidationDisabled = false
}
}
// DisableExamplesValidation disables all example schema validation.
// By default, all schema examples are validated.
func DisableExamplesValidation() ValidationOption {
return func(options *ValidationOptions) {
options.examplesValidationDisabled = true
}
}
// 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
}
}
// SetRegexCompiler allows to override the regex implementation used to validate
// field "pattern".
func SetRegexCompiler(c RegexCompilerFunc) ValidationOption {
return func(options *ValidationOptions) {
options.regexCompilerFunc = c
}
}
// WithValidationOptions allows adding validation options to a context object that can be used when validating any OpenAPI type.
func WithValidationOptions(ctx context.Context, opts ...ValidationOption) context.Context {
if len(opts) == 0 {
return ctx
}
options := &ValidationOptions{}
for _, opt := range opts {
opt(options)
}
return context.WithValue(ctx, validationOptionsKey{}, options)
}
func getValidationOptions(ctx context.Context) *ValidationOptions {
if options, ok := ctx.Value(validationOptionsKey{}).(*ValidationOptions); ok {
return options
}
return &ValidationOptions{}
}

View file

@ -0,0 +1,41 @@
package openapi3
func newVisited() visitedComponent {
return visitedComponent{
header: make(map[*Header]struct{}),
schema: make(map[*Schema]struct{}),
}
}
type visitedComponent struct {
header map[*Header]struct{}
schema map[*Schema]struct{}
}
// resetVisited clears visitedComponent map
// should be called before recursion over doc *T
func (doc *T) resetVisited() {
doc.visited = newVisited()
}
// isVisitedHeader returns `true` if the *Header pointer was already visited
// otherwise it returns `false`
func (doc *T) isVisitedHeader(h *Header) bool {
if _, ok := doc.visited.header[h]; ok {
return true
}
doc.visited.header[h] = struct{}{}
return false
}
// isVisitedHeader returns `true` if the *Schema pointer was already visited
// otherwise it returns `false`
func (doc *T) isVisitedSchema(s *Schema) bool {
if _, ok := doc.visited.schema[s]; ok {
return true
}
doc.visited.schema[s] = struct{}{}
return false
}

View file

@ -2,14 +2,14 @@ package openapi3
import (
"context"
"github.com/getkin/kin-openapi/jsoninfo"
"encoding/json"
)
// XML is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xmlObject
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xml-object
type XML struct {
ExtensionProps
Extensions map[string]any `json:"-" yaml:"-"`
Origin *Origin `json:"__origin__,omitempty" yaml:"__origin__,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
@ -18,14 +18,63 @@ type XML struct {
Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"`
}
func (value *XML) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(value)
// MarshalJSON returns the JSON encoding of XML.
func (xml XML) MarshalJSON() ([]byte, error) {
x, err := xml.MarshalYAML()
if err != nil {
return nil, err
}
return json.Marshal(x)
}
func (value *XML) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, value)
// MarshalYAML returns the YAML encoding of XML.
func (xml XML) MarshalYAML() (any, error) {
m := make(map[string]any, 5+len(xml.Extensions))
for k, v := range xml.Extensions {
m[k] = v
}
if x := xml.Name; x != "" {
m["name"] = x
}
if x := xml.Namespace; x != "" {
m["namespace"] = x
}
if x := xml.Prefix; x != "" {
m["prefix"] = x
}
if x := xml.Attribute; x {
m["attribute"] = x
}
if x := xml.Wrapped; x {
m["wrapped"] = x
}
return m, nil
}
func (value *XML) Validate(ctx context.Context) error {
return nil // TODO
// UnmarshalJSON sets XML to a copy of data.
func (xml *XML) UnmarshalJSON(data []byte) error {
type XMLBis XML
var x XMLBis
if err := json.Unmarshal(data, &x); err != nil {
return unmarshalError(err)
}
_ = json.Unmarshal(data, &x.Extensions)
delete(x.Extensions, originKey)
delete(x.Extensions, "name")
delete(x.Extensions, "namespace")
delete(x.Extensions, "prefix")
delete(x.Extensions, "attribute")
delete(x.Extensions, "wrapped")
if len(x.Extensions) == 0 {
x.Extensions = nil
}
*xml = XML(x)
return nil
}
// Validate returns an error if XML does not comply with the OpenAPI spec.
func (xml *XML) Validate(ctx context.Context, opts ...ValidationOption) error {
ctx = WithValidationOptions(ctx, opts...)
return validateExtensions(ctx, xml.Extensions)
}