cloudapi: validate input

Validate incoming requests with openapi3. Remove unsupported
uuid format from the openapi spec. Similarly, change url to uri as
uri is a supported format and url is not.

Co-authored-by: Ondřej Budai <obudai@redhat.com>
Signed-off-by: Ondřej Budai <ondrej@budai.cz>
This commit is contained in:
Chloe Kaubisch 2022-03-10 14:16:36 +00:00
parent f616becf39
commit 13c79294b6
83 changed files with 4942 additions and 549 deletions

View file

@ -23,7 +23,8 @@ func (c Callbacks) JSONLookup(token string) (interface{}, error) {
return ref.Value, nil
}
// Callback is specified by OpenAPI/Swagger standard version 3.0.
// 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
func (value Callback) Validate(ctx context.Context) error {

View file

@ -8,9 +8,11 @@ import (
"github.com/getkin/kin-openapi/jsoninfo"
)
// Components is specified by OpenAPI/Swagger standard version 3.0.
// Components is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#componentsObject
type Components struct {
ExtensionProps
Schemas Schemas `json:"schemas,omitempty" yaml:"schemas,omitempty"`
Parameters ParametersMap `json:"parameters,omitempty" yaml:"parameters,omitempty"`
Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"`

View file

@ -6,9 +6,11 @@ import (
"github.com/getkin/kin-openapi/jsoninfo"
)
// Discriminator is specified by OpenAPI/Swagger standard version 3.0.
// Discriminator is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminatorObject
type Discriminator struct {
ExtensionProps
PropertyName string `json:"propertyName" yaml:"propertyName"`
Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"`
}

View file

@ -8,6 +8,7 @@ import (
)
// Encoding is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#encodingObject
type Encoding struct {
ExtensionProps

View file

@ -25,6 +25,7 @@ func (e Examples) JSONLookup(token string) (interface{}, error) {
}
// Example is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#exampleObject
type Example struct {
ExtensionProps

View file

@ -1,15 +1,21 @@
package openapi3
import (
"context"
"errors"
"fmt"
"net/url"
"github.com/getkin/kin-openapi/jsoninfo"
)
// ExternalDocs is specified by OpenAPI/Swagger standard version 3.0.
// 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
Description string `json:"description,omitempty"`
URL string `json:"url,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
URL string `json:"url,omitempty" yaml:"url,omitempty"`
}
func (e *ExternalDocs) MarshalJSON() ([]byte, error) {
@ -19,3 +25,13 @@ func (e *ExternalDocs) MarshalJSON() ([]byte, error) {
func (e *ExternalDocs) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, e)
}
func (e *ExternalDocs) Validate(ctx context.Context) error {
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
}

View file

@ -2,6 +2,7 @@ package openapi3
import (
"context"
"errors"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
@ -24,17 +25,10 @@ func (h Headers) JSONLookup(token string) (interface{}, error) {
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
type Header struct {
ExtensionProps
// Optional description. Should use CommonMark syntax.
Description string `json:"description,omitempty" yaml:"description,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"`
Parameter
}
var _ jsonpointer.JSONPointable = (*Header)(nil)
@ -43,10 +37,52 @@ func (value *Header) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, value)
}
// SerializationMethod returns a header's serialization method.
func (value *Header) SerializationMethod() (*SerializationMethod, error) {
style := value.Style
if style == "" {
style = SerializationSimple
}
explode := false
if value.Explode != nil {
explode = *value.Explode
}
return &SerializationMethod{Style: style, Explode: explode}, nil
}
func (value *Header) Validate(ctx context.Context) error {
if v := value.Schema; v != nil {
if err := v.Validate(ctx); err != nil {
return err
if value.Name != "" {
return errors.New("header 'name' MUST NOT be specified, it is given in the corresponding headers map")
}
if value.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()
if err != nil {
return err
}
if smSupported := false ||
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)
}
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 schema := value.Schema; schema != nil {
if err := schema.Validate(ctx); err != nil {
return fmt.Errorf("header schema is invalid: %v", err)
}
}
if content := value.Content; content != nil {
if err := content.Validate(ctx); err != nil {
return fmt.Errorf("header content is invalid: %v", err)
}
}
return nil
@ -61,8 +97,20 @@ func (value Header) JSONLookup(token string) (interface{}, error) {
}
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":

View file

@ -7,9 +7,11 @@ import (
"github.com/getkin/kin-openapi/jsoninfo"
)
// Info is specified by OpenAPI/Swagger standard version 3.0.
// Info is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#infoObject
type Info struct {
ExtensionProps
Title string `json:"title" yaml:"title"` // Required
Description string `json:"description,omitempty" yaml:"description,omitempty"`
TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"`
@ -50,9 +52,11 @@ func (value *Info) Validate(ctx context.Context) error {
return nil
}
// Contact is specified by OpenAPI/Swagger standard version 3.0.
// 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"`
@ -70,9 +74,11 @@ func (value *Contact) Validate(ctx context.Context) error {
return nil
}
// License is specified by OpenAPI/Swagger standard version 3.0.
// 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"`
}

View file

@ -0,0 +1,369 @@
package openapi3
import (
"context"
"path/filepath"
"strings"
)
type RefNameResolver func(string) string
// DefaultRefResolver is a default implementation of refNameResolver for the
// InternalizeRefs function.
//
// If a reference points to an element inside a document, it returns the last
// element in the reference using filepath.Base. Otherwise if the reference points
// to a file, it returns the file name trimmed of all extensions.
func DefaultRefNameResolver(ref string) string {
if ref == "" {
return ""
}
split := strings.SplitN(ref, "#", 2)
if len(split) == 2 {
return filepath.Base(split[1])
}
ref = split[0]
for ext := filepath.Ext(ref); len(ext) > 0; ext = filepath.Ext(ref) {
ref = strings.TrimSuffix(ref, ext)
}
return filepath.Base(ref)
}
func schemaNames(s Schemas) []string {
out := make([]string, 0, len(s))
for i := range s {
out = append(out, i)
}
return out
}
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) bool {
return ref != "" && !strings.HasPrefix(ref, "#/components/")
}
func (doc *T) addSchemaToSpec(s *SchemaRef, refNameResolver RefNameResolver) {
if s == nil || !isExternalRef(s.Ref) {
return
}
name := refNameResolver(s.Ref)
if _, ok := doc.Components.Schemas[name]; ok {
s.Ref = "#/components/schemas/" + name
return
}
if doc.Components.Schemas == nil {
doc.Components.Schemas = make(Schemas)
}
doc.Components.Schemas[name] = s.Value.NewRef()
s.Ref = "#/components/schemas/" + name
}
func (doc *T) addParameterToSpec(p *ParameterRef, refNameResolver RefNameResolver) {
if p == nil || !isExternalRef(p.Ref) {
return
}
name := refNameResolver(p.Ref)
if _, ok := doc.Components.Parameters[name]; ok {
p.Ref = "#/components/parameters/" + name
return
}
if doc.Components.Parameters == nil {
doc.Components.Parameters = make(ParametersMap)
}
doc.Components.Parameters[name] = &ParameterRef{Value: p.Value}
p.Ref = "#/components/parameters/" + name
}
func (doc *T) addHeaderToSpec(h *HeaderRef, refNameResolver RefNameResolver) {
if h == nil || !isExternalRef(h.Ref) {
return
}
name := refNameResolver(h.Ref)
if _, ok := doc.Components.Headers[name]; ok {
h.Ref = "#/components/headers/" + name
return
}
if doc.Components.Headers == nil {
doc.Components.Headers = make(Headers)
}
doc.Components.Headers[name] = &HeaderRef{Value: h.Value}
h.Ref = "#/components/headers/" + name
}
func (doc *T) addRequestBodyToSpec(r *RequestBodyRef, refNameResolver RefNameResolver) {
if r == nil || !isExternalRef(r.Ref) {
return
}
name := refNameResolver(r.Ref)
if _, ok := doc.Components.RequestBodies[name]; ok {
r.Ref = "#/components/requestBodies/" + name
return
}
if doc.Components.RequestBodies == nil {
doc.Components.RequestBodies = make(RequestBodies)
}
doc.Components.RequestBodies[name] = &RequestBodyRef{Value: r.Value}
r.Ref = "#/components/requestBodies/" + name
}
func (doc *T) addResponseToSpec(r *ResponseRef, refNameResolver RefNameResolver) {
if r == nil || !isExternalRef(r.Ref) {
return
}
name := refNameResolver(r.Ref)
if _, ok := doc.Components.Responses[name]; ok {
r.Ref = "#/components/responses/" + name
return
}
if doc.Components.Responses == nil {
doc.Components.Responses = make(Responses)
}
doc.Components.Responses[name] = &ResponseRef{Value: r.Value}
r.Ref = "#/components/responses/" + name
}
func (doc *T) addSecuritySchemeToSpec(ss *SecuritySchemeRef, refNameResolver RefNameResolver) {
if ss == nil || !isExternalRef(ss.Ref) {
return
}
name := refNameResolver(ss.Ref)
if _, ok := doc.Components.SecuritySchemes[name]; ok {
ss.Ref = "#/components/securitySchemes/" + name
return
}
if doc.Components.SecuritySchemes == nil {
doc.Components.SecuritySchemes = make(SecuritySchemes)
}
doc.Components.SecuritySchemes[name] = &SecuritySchemeRef{Value: ss.Value}
ss.Ref = "#/components/securitySchemes/" + name
}
func (doc *T) addExampleToSpec(e *ExampleRef, refNameResolver RefNameResolver) {
if e == nil || !isExternalRef(e.Ref) {
return
}
name := refNameResolver(e.Ref)
if _, ok := doc.Components.Examples[name]; ok {
e.Ref = "#/components/examples/" + name
return
}
if doc.Components.Examples == nil {
doc.Components.Examples = make(Examples)
}
doc.Components.Examples[name] = &ExampleRef{Value: e.Value}
e.Ref = "#/components/examples/" + name
}
func (doc *T) addLinkToSpec(l *LinkRef, refNameResolver RefNameResolver) {
if l == nil || !isExternalRef(l.Ref) {
return
}
name := refNameResolver(l.Ref)
if _, ok := doc.Components.Links[name]; ok {
l.Ref = "#/components/links/" + name
return
}
if doc.Components.Links == nil {
doc.Components.Links = make(Links)
}
doc.Components.Links[name] = &LinkRef{Value: l.Value}
l.Ref = "#/components/links/" + name
}
func (doc *T) addCallbackToSpec(c *CallbackRef, refNameResolver RefNameResolver) {
if c == nil || !isExternalRef(c.Ref) {
return
}
name := refNameResolver(c.Ref)
if _, ok := doc.Components.Callbacks[name]; ok {
c.Ref = "#/components/callbacks/" + name
}
if doc.Components.Callbacks == nil {
doc.Components.Callbacks = make(Callbacks)
}
doc.Components.Callbacks[name] = &CallbackRef{Value: c.Value}
c.Ref = "#/components/callbacks/" + name
}
func (doc *T) derefSchema(s *Schema, refNameResolver RefNameResolver) {
if s == nil {
return
}
for _, list := range []SchemaRefs{s.AllOf, s.AnyOf, s.OneOf} {
for _, s2 := range list {
doc.addSchemaToSpec(s2, refNameResolver)
if s2 != nil {
doc.derefSchema(s2.Value, refNameResolver)
}
}
}
for _, s2 := range s.Properties {
doc.addSchemaToSpec(s2, refNameResolver)
if s2 != nil {
doc.derefSchema(s2.Value, refNameResolver)
}
}
for _, ref := range []*SchemaRef{s.Not, s.AdditionalProperties, s.Items} {
doc.addSchemaToSpec(ref, refNameResolver)
if ref != nil {
doc.derefSchema(ref.Value, refNameResolver)
}
}
}
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) derefExamples(es Examples, refNameResolver RefNameResolver) {
for _, e := range es {
doc.addExampleToSpec(e, refNameResolver)
}
}
func (doc *T) derefContent(c Content, refNameResolver RefNameResolver) {
for _, mediatype := range c {
doc.addSchemaToSpec(mediatype.Schema, refNameResolver)
if mediatype.Schema != nil {
doc.derefSchema(mediatype.Schema.Value, refNameResolver)
}
doc.derefExamples(mediatype.Examples, refNameResolver)
for _, e := range mediatype.Encoding {
doc.derefHeaders(e.Headers, refNameResolver)
}
}
}
func (doc *T) derefLinks(ls Links, refNameResolver RefNameResolver) {
for _, l := range ls {
doc.addLinkToSpec(l, refNameResolver)
}
}
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) derefParameter(p Parameter, refNameResolver RefNameResolver) {
doc.addSchemaToSpec(p.Schema, refNameResolver)
doc.derefContent(p.Content, refNameResolver)
if p.Schema != nil {
doc.derefSchema(p.Schema.Value, refNameResolver)
}
}
func (doc *T) derefRequestBody(r RequestBody, refNameResolver RefNameResolver) {
doc.derefContent(r.Content, refNameResolver)
}
func (doc *T) derefPaths(paths map[string]*PathItem, refNameResolver RefNameResolver) {
for _, ops := range paths {
// 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 _, cb := range op.Callbacks {
doc.addCallbackToSpec(cb, refNameResolver)
if cb.Value != nil {
doc.derefPaths(*cb.Value, refNameResolver)
}
}
doc.derefResponses(op.Responses, refNameResolver)
for _, param := range op.Parameters {
doc.addParameterToSpec(param, refNameResolver)
if param.Value != nil {
doc.derefParameter(*param.Value, refNameResolver)
}
}
}
}
}
// InternalizeRefs removes all references to external files from the spec and moves them
// to the components section.
//
// 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.
//
// Example:
//
// doc.InternalizeRefs(context.Background(), nil)
func (doc *T) InternalizeRefs(ctx context.Context, refNameResolver func(ref string) string) {
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)
}
}
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)
}
}
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.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.derefPaths(doc.Paths, refNameResolver)
}

View file

@ -25,11 +25,13 @@ func (l Links) JSONLookup(token string) (interface{}, error) {
var _ jsonpointer.JSONPointable = (*Links)(nil)
// Link is specified by OpenAPI/Swagger standard version 3.0.
// Link is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#linkObject
type Link struct {
ExtensionProps
OperationID string `json:"operationId,omitempty" yaml:"operationId,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"`

View file

@ -5,8 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path"
"path/filepath"
@ -31,10 +29,12 @@ type Loader struct {
IsExternalRefsAllowed bool
// ReadFromURIFunc allows overriding the any file/URL reading func
ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error)
ReadFromURIFunc ReadFromURIFunc
Context context.Context
rootDir string
visitedPathItemRefs map[string]struct{}
visitedDocuments map[string]*T
@ -66,6 +66,7 @@ func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) {
// LoadFromFile loads a spec from a local file path
func (loader *Loader) LoadFromFile(location string) (*T, error) {
loader.rootDir = path.Dir(location)
return loader.LoadFromURI(&url.URL{Path: filepath.ToSlash(location)})
}
@ -118,22 +119,7 @@ func (loader *Loader) readURL(location *url.URL) ([]byte, error) {
if f := loader.ReadFromURIFunc; f != nil {
return f(loader, location)
}
if location.Scheme != "" && location.Host != "" {
resp, err := http.Get(location.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
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)
}
if location.Scheme != "" || location.Host != "" || location.RawQuery != "" {
return nil, fmt.Errorf("unsupported URI: %q", location.String())
}
return ioutil.ReadFile(location.Path)
return DefaultReadFromURI(loader, location)
}
// LoadFromData loads a spec from a byte array
@ -305,6 +291,9 @@ func (loader *Loader) resolveComponent(
}
var cursor interface{}
if cursor, err = drill(doc); err != nil {
if path == nil {
return nil, err
}
var err2 error
data, err2 := loader.readURL(path)
if err2 != nil {
@ -346,6 +335,14 @@ func (loader *Loader) resolveComponent(
}
func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) {
// Special case due to multijson
if s, ok := cursor.(*SchemaRef); ok && fieldName == "additionalProperties" {
if ap := s.Value.AdditionalPropertiesAllowed; ap != nil {
return *ap, nil
}
return s.Value.AdditionalProperties, nil
}
switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() {
case reflect.Map:
elementValue := val.MapIndex(reflect.ValueOf(fieldName))
@ -372,6 +369,10 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) {
field := val.Type().Field(i)
tagValue := field.Tag.Get("yaml")
yamlKey := strings.Split(tagValue, ",")[0]
if yamlKey == "-" {
tagValue := field.Tag.Get("multijson")
yamlKey = strings.Split(tagValue, ",")[0]
}
if yamlKey == fieldName {
return val.Field(i).Interface(), nil
}
@ -400,6 +401,14 @@ func drillIntoField(cursor interface{}, fieldName string) (interface{}, error) {
}
}
func (loader *Loader) documentPathForRecursiveRef(current *url.URL, resolvedRef string) *url.URL {
if loader.rootDir == "" {
return current
}
return &url.URL{Path: path.Join(loader.rootDir, resolvedRef)}
}
func (loader *Loader) resolveRef(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) {
if ref != "" && ref[0] == '#' {
return doc, ref, path, nil
@ -459,6 +468,7 @@ func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPat
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -506,6 +516,7 @@ func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, docum
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -562,6 +573,7 @@ func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, d
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -617,6 +629,7 @@ func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documen
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -686,6 +699,7 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -749,7 +763,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme
if ref := component.Ref; ref != "" {
if isSingleRefElement(ref) {
var scheme SecurityScheme
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil {
if _, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil {
return err
}
component.Value = &scheme
@ -763,6 +777,7 @@ func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecurityScheme
return err
}
component.Value = resolved.Value
_ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
return nil
@ -785,7 +800,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP
if ref := component.Ref; ref != "" {
if isSingleRefElement(ref) {
var example Example
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil {
if _, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil {
return err
}
component.Value = &example
@ -799,6 +814,7 @@ func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentP
return err
}
component.Value = resolved.Value
_ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
return nil
@ -826,6 +842,7 @@ func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documen
return err
}
component.Value = resolved.Value
documentPath = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
value := component.Value
@ -909,7 +926,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u
if ref := component.Ref; ref != "" {
if isSingleRefElement(ref) {
var link Link
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil {
if _, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil {
return err
}
component.Value = &link
@ -923,6 +940,7 @@ func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *u
return err
}
component.Value = resolved.Value
_ = loader.documentPathForRecursiveRef(documentPath, resolved.Ref)
}
}
return nil

View file

@ -0,0 +1,104 @@
package openapi3
import (
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"path/filepath"
)
// 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)
// ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a
// given URI.
var ErrURINotSupported = errors.New("unsupported URI")
// ReadFromURIs returns a ReadFromURIFunc which tries to read a URI using the
// given reader functions, in the same order. If a reader function does not
// support the URI and returns ErrURINotSupported, the next function is checked
// until a match is found, or the URI is not supported by any.
func ReadFromURIs(readers ...ReadFromURIFunc) ReadFromURIFunc {
return func(loader *Loader, url *url.URL) ([]byte, error) {
for i := range readers {
buf, err := readers[i](loader, url)
if err == ErrURINotSupported {
continue
} else if err != nil {
return nil, err
}
return buf, nil
}
return nil, ErrURINotSupported
}
}
// DefaultReadFromURI returns a caching ReadFromURIFunc which can read remote
// HTTP URIs and local file URIs.
var DefaultReadFromURI = URIMapCache(ReadFromURIs(ReadFromHTTP(http.DefaultClient), ReadFromFile))
// ReadFromHTTP returns a ReadFromURIFunc which uses the given http.Client to
// read the contents from a remote HTTP URI. This client may be customized to
// implement timeouts, RFC 7234 caching, etc.
func ReadFromHTTP(cl *http.Client) ReadFromURIFunc {
return func(loader *Loader, location *url.URL) ([]byte, error) {
if location.Scheme == "" || location.Host == "" {
return nil, ErrURINotSupported
}
req, err := http.NewRequest("GET", location.String(), nil)
if err != nil {
return nil, err
}
resp, err := cl.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
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)
}
}
// ReadFromFile is a ReadFromURIFunc which reads local file URIs.
func ReadFromFile(loader *Loader, location *url.URL) ([]byte, error) {
if location.Host != "" {
return nil, ErrURINotSupported
}
if location.Scheme != "" && location.Scheme != "file" {
return nil, ErrURINotSupported
}
return ioutil.ReadFile(location.Path)
}
// URIMapCache returns a ReadFromURIFunc that caches the contents read from URI
// locations in a simple map. This cache implementation is suitable for
// short-lived processes such as command-line tools which process OpenAPI
// documents.
func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc {
cache := map[string][]byte{}
return func(loader *Loader, location *url.URL) (buf []byte, err error) {
if location.Scheme == "" || location.Scheme == "file" {
if !filepath.IsAbs(location.Path) {
// Do not cache relative file paths; this can cause trouble if
// the current working directory changes when processing
// multiple top-level documents.
return reader(loader, location)
}
}
uri := location.String()
var ok bool
if buf, ok = cache[uri]; ok {
return
}
if buf, err = reader(loader, location); err != nil {
return
}
cache[uri] = buf
return
}
}

View file

@ -8,6 +8,7 @@ import (
)
// MediaType is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#mediaTypeObject
type MediaType struct {
ExtensionProps

View file

@ -9,8 +9,10 @@ import (
)
// T is the root of an OpenAPI v3 document
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oasObject
type T struct {
ExtensionProps
OpenAPI string `json:"openapi" yaml:"openapi"` // Required
Components Components `json:"components,omitempty" yaml:"components,omitempty"`
Info *Info `json:"info" yaml:"info"` // Required
@ -101,5 +103,23 @@ func (value *T) Validate(ctx context.Context) error {
}
}
{
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
}

View file

@ -3,6 +3,7 @@ package openapi3
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/getkin/kin-openapi/jsoninfo"
@ -10,6 +11,7 @@ import (
)
// Operation represents "operation" specified by" OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#operation-object
type Operation struct {
ExtensionProps
@ -138,5 +140,10 @@ func (value *Operation) Validate(ctx context.Context) error {
} else {
return errors.New("value of responses must be an object")
}
if v := value.ExternalDocs; v != nil {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("invalid external docs: %w", err)
}
}
return nil
}

View file

@ -83,8 +83,10 @@ 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
type Parameter struct {
ExtensionProps
Name string `json:"name,omitempty" yaml:"name,omitempty"`
In string `json:"in,omitempty" yaml:"in,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
@ -167,42 +169,42 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, parameter)
}
func (parameter Parameter) JSONLookup(token string) (interface{}, error) {
func (value Parameter) JSONLookup(token string) (interface{}, error) {
switch token {
case "schema":
if parameter.Schema != nil {
if parameter.Schema.Ref != "" {
return &Ref{Ref: parameter.Schema.Ref}, nil
if value.Schema != nil {
if value.Schema.Ref != "" {
return &Ref{Ref: value.Schema.Ref}, nil
}
return parameter.Schema.Value, nil
return value.Schema.Value, nil
}
case "name":
return parameter.Name, nil
return value.Name, nil
case "in":
return parameter.In, nil
return value.In, nil
case "description":
return parameter.Description, nil
return value.Description, nil
case "style":
return parameter.Style, nil
return value.Style, nil
case "explode":
return parameter.Explode, nil
return value.Explode, nil
case "allowEmptyValue":
return parameter.AllowEmptyValue, nil
return value.AllowEmptyValue, nil
case "allowReserved":
return parameter.AllowReserved, nil
return value.AllowReserved, nil
case "deprecated":
return parameter.Deprecated, nil
return value.Deprecated, nil
case "required":
return parameter.Required, nil
return value.Required, nil
case "example":
return parameter.Example, nil
return value.Example, nil
case "examples":
return parameter.Examples, nil
return value.Examples, nil
case "content":
return parameter.Content, nil
return value.Content, nil
}
v, _, err := jsonpointer.GetForToken(parameter.ExtensionProps, token)
v, _, err := jsonpointer.GetForToken(value.ExtensionProps, token)
return v, err
}
@ -251,6 +253,10 @@ func (value *Parameter) Validate(ctx context.Context) error {
return fmt.Errorf("parameter can't have 'in' value %q", value.In)
}
if in == ParameterInPath && !value.Required {
return fmt.Errorf("path parameter %q must be required", value.Name)
}
// Validate a parameter's serialization method.
sm, err := value.SerializationMethod()
if err != nil {
@ -294,6 +300,7 @@ func (value *Parameter) Validate(ctx context.Context) error {
return fmt.Errorf("parameter %q schema is invalid: %v", value.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)

View file

@ -8,8 +8,11 @@ import (
"github.com/getkin/kin-openapi/jsoninfo"
)
// PathItem is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#pathItemObject
type PathItem struct {
ExtensionProps
Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
Summary string `json:"summary,omitempty" yaml:"summary,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`

View file

@ -6,7 +6,8 @@ import (
"strings"
)
// Paths is specified by OpenAPI/Swagger standard version 3.0.
// 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
func (value Paths) Validate(ctx context.Context) error {
@ -21,31 +22,60 @@ func (value Paths) Validate(ctx context.Context) error {
pathItem = value[path]
}
normalizedPath, pathParamsCount := normalizeTemplatedPath(path)
normalizedPath, _, varsInPath := normalizeTemplatedPath(path)
if oldPath, ok := normalizedPaths[normalizedPath]; ok {
return fmt.Errorf("conflicting paths %q and %q", path, oldPath)
}
normalizedPaths[path] = path
var globalCount uint
var commonParams []string
for _, parameterRef := range pathItem.Parameters {
if parameterRef != nil {
if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath {
globalCount++
commonParams = append(commonParams, parameter.Name)
}
}
}
for method, operation := range pathItem.Operations() {
var count uint
var setParams []string
for _, parameterRef := range operation.Parameters {
if parameterRef != nil {
if parameter := parameterRef.Value; parameter != nil && parameter.In == ParameterInPath {
count++
setParams = append(setParams, parameter.Name)
}
}
}
if count+globalCount != pathParamsCount {
return fmt.Errorf("operation %s %s must define exactly all path parameters", method, path)
if expected := len(setParams) + len(commonParams); expected != len(varsInPath) {
expected -= len(varsInPath)
if expected < 0 {
expected *= -1
}
missing := make(map[string]struct{}, expected)
definedParams := append(setParams, commonParams...)
for _, name := range definedParams {
if _, ok := varsInPath[name]; !ok {
missing[name] = struct{}{}
}
}
for name := range varsInPath {
got := false
for _, othername := range definedParams {
if othername == name {
got = true
break
}
}
if !got {
missing[name] = struct{}{}
}
}
if len(missing) != 0 {
missings := make([]string, 0, len(missing))
for name := range missing {
missings = append(missings, name)
}
return fmt.Errorf("operation %s %s must define exactly all path parameters (missing: %v)", method, path, missings)
}
}
}
@ -53,6 +83,11 @@ func (value Paths) Validate(ctx context.Context) error {
return err
}
}
if err := value.validateUniqueOperationIDs(); err != nil {
return err
}
return nil
}
@ -75,9 +110,9 @@ func (paths Paths) Find(key string) *PathItem {
return pathItem
}
normalizedPath, expected := normalizeTemplatedPath(key)
normalizedPath, expected, _ := normalizeTemplatedPath(key)
for path, pathItem := range paths {
pathNormalized, got := normalizeTemplatedPath(path)
pathNormalized, got, _ := normalizeTemplatedPath(path)
if got == expected && pathNormalized == normalizedPath {
return pathItem
}
@ -85,43 +120,75 @@ func (paths Paths) Find(key string) *PathItem {
return nil
}
func normalizeTemplatedPath(path string) (string, uint) {
func (value Paths) validateUniqueOperationIDs() error {
operationIDs := make(map[string]string)
for urlPath, pathItem := range value {
if pathItem == nil {
continue
}
for httpMethod, operation := range pathItem.Operations() {
if operation == nil || operation.OperationID == "" {
continue
}
endpoint := httpMethod + " " + urlPath
if endpointDup, ok := operationIDs[operation.OperationID]; ok {
if endpoint > endpointDup { // For make error message a bit more deterministic. May be useful for tests.
endpoint, endpointDup = endpointDup, endpoint
}
return fmt.Errorf("operations %q and %q have the same operation id %q",
endpoint, endpointDup, operation.OperationID)
}
operationIDs[operation.OperationID] = endpoint
}
}
return nil
}
func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) {
if strings.IndexByte(path, '{') < 0 {
return path, 0
return path, 0, nil
}
var buf strings.Builder
buf.Grow(len(path))
var buffTpl strings.Builder
buffTpl.Grow(len(path))
var (
cc rune
count uint
isVariable bool
vars = make(map[string]struct{})
buffVar strings.Builder
)
for i, c := range path {
if isVariable {
if c == '}' {
// End path variables
// End path variable
isVariable = false
vars[buffVar.String()] = struct{}{}
buffVar = strings.Builder{}
// First append possible '*' before this character
// The character '}' will be appended
if i > 0 && cc == '*' {
buf.WriteRune(cc)
buffTpl.WriteRune(cc)
}
isVariable = false
} else {
// Skip this character
buffVar.WriteRune(c)
continue
}
} else if c == '{' {
// Begin path variable
// The character '{' will be appended
isVariable = true
// The character '{' will be appended
count++
}
// Append the character
buf.WriteRune(c)
buffTpl.WriteRune(c)
cc = c
}
return buf.String(), count
return buffTpl.String(), count, vars
}

View file

@ -8,10 +8,13 @@ import (
)
// Ref is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#referenceObject
type Ref struct {
Ref string `json:"$ref" yaml:"$ref"`
}
// CallbackRef represents either a Callback or a $ref to a Callback.
// When serializing and both fields are set, Ref is preferred over Value.
type CallbackRef struct {
Ref string
Value *Callback
@ -43,6 +46,8 @@ func (value CallbackRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// ExampleRef represents either a Example or a $ref to a Example.
// When serializing and both fields are set, Ref is preferred over Value.
type ExampleRef struct {
Ref string
Value *Example
@ -74,6 +79,8 @@ func (value ExampleRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// HeaderRef represents either a Header or a $ref to a Header.
// When serializing and both fields are set, Ref is preferred over Value.
type HeaderRef struct {
Ref string
Value *Header
@ -105,6 +112,8 @@ func (value HeaderRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// LinkRef represents either a Link or a $ref to a Link.
// When serializing and both fields are set, Ref is preferred over Value.
type LinkRef struct {
Ref string
Value *Link
@ -125,6 +134,8 @@ func (value *LinkRef) Validate(ctx context.Context) error {
return foundUnresolvedRef(value.Ref)
}
// ParameterRef represents either a Parameter or a $ref to a Parameter.
// When serializing and both fields are set, Ref is preferred over Value.
type ParameterRef struct {
Ref string
Value *Parameter
@ -156,6 +167,8 @@ func (value ParameterRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// ResponseRef represents either a Response or a $ref to a Response.
// When serializing and both fields are set, Ref is preferred over Value.
type ResponseRef struct {
Ref string
Value *Response
@ -187,6 +200,8 @@ func (value ResponseRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// RequestBodyRef represents either a RequestBody or a $ref to a RequestBody.
// When serializing and both fields are set, Ref is preferred over Value.
type RequestBodyRef struct {
Ref string
Value *RequestBody
@ -218,6 +233,8 @@ func (value RequestBodyRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// SchemaRef represents either a Schema or a $ref to a Schema.
// When serializing and both fields are set, Ref is preferred over Value.
type SchemaRef struct {
Ref string
Value *Schema
@ -256,6 +273,8 @@ func (value SchemaRef) JSONLookup(token string) (interface{}, error) {
return ptr, err
}
// SecuritySchemeRef represents either a SecurityScheme or a $ref to a SecurityScheme.
// When serializing and both fields are set, Ref is preferred over Value.
type SecuritySchemeRef struct {
Ref string
Value *SecurityScheme

View file

@ -2,6 +2,7 @@ package openapi3
import (
"context"
"errors"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
@ -25,11 +26,13 @@ func (r RequestBodies) JSONLookup(token string) (interface{}, error) {
}
// RequestBody is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#requestBodyObject
type RequestBody struct {
ExtensionProps
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Required bool `json:"required,omitempty" yaml:"required,omitempty"`
Content Content `json:"content,omitempty" yaml:"content,omitempty"`
Content Content `json:"content" yaml:"content"`
}
func NewRequestBody() *RequestBody {
@ -98,10 +101,8 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error {
}
func (value *RequestBody) Validate(ctx context.Context) error {
if v := value.Content; v != nil {
if err := v.Validate(ctx); err != nil {
return err
}
if value.Content == nil {
return errors.New("content of the request body is required")
}
return nil
return value.Content.Validate(ctx)
}

View file

@ -11,6 +11,7 @@ import (
)
// Responses is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responsesObject
type Responses map[string]*ResponseRef
var _ jsonpointer.JSONPointable = (*Responses)(nil)
@ -54,8 +55,10 @@ func (responses Responses) JSONLookup(token string) (interface{}, error) {
}
// Response is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#responseObject
type Response struct {
ExtensionProps
Description *string `json:"description,omitempty" yaml:"description,omitempty"`
Headers Headers `json:"headers,omitempty" yaml:"headers,omitempty"`
Content Content `json:"content,omitempty" yaml:"content,omitempty"`
@ -104,5 +107,16 @@ func (value *Response) Validate(ctx context.Context) error {
return err
}
}
for _, header := range value.Headers {
if err := header.Validate(ctx); err != nil {
return err
}
}
for _, link := range value.Links {
if err := link.Validate(ctx); err != nil {
return err
}
}
return nil
}

View file

@ -16,6 +16,15 @@ import (
"github.com/go-openapi/jsonpointer"
)
const (
TypeArray = "array"
TypeBoolean = "boolean"
TypeInteger = "integer"
TypeNumber = "number"
TypeObject = "object"
TypeString = "string"
)
var (
// SchemaErrorDetailsDisabled disables printing of details about schema errors.
SchemaErrorDetailsDisabled = false
@ -93,6 +102,7 @@ func (s SchemaRefs) JSONLookup(token string) (interface{}, error) {
}
// Schema is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schemaObject
type Schema struct {
ExtensionProps
@ -109,20 +119,18 @@ type Schema struct {
Example interface{} `json:"example,omitempty" yaml:"example,omitempty"`
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
// Object-related, here for struct compactness
AdditionalPropertiesAllowed *bool `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"`
// Array-related, here for struct compactness
UniqueItems bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"`
// Number-related, here for struct compactness
ExclusiveMin bool `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"`
ExclusiveMax bool `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"`
// Properties
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"`
WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"`
AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
XML interface{} `json:"xml,omitempty" yaml:"xml,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
Nullable bool `json:"nullable,omitempty" yaml:"nullable,omitempty"`
ReadOnly bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"`
WriteOnly bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"`
AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"`
Deprecated bool `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
XML *XML `json:"xml,omitempty" yaml:"xml,omitempty"`
// Number
Min *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"`
@ -141,12 +149,13 @@ type Schema struct {
Items *SchemaRef `json:"items,omitempty" yaml:"items,omitempty"`
// Object
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"`
MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
AdditionalProperties *SchemaRef `json:"-" multijson:"additionalProperties,omitempty" yaml:"-"`
Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"`
MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
AdditionalPropertiesAllowed *bool `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // In this order...
AdditionalProperties *SchemaRef `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // ...for multijson
Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
}
var _ jsonpointer.JSONPointable = (*Schema)(nil)
@ -298,73 +307,73 @@ func NewAllOfSchema(schemas ...*Schema) *Schema {
func NewBoolSchema() *Schema {
return &Schema{
Type: "boolean",
Type: TypeBoolean,
}
}
func NewFloat64Schema() *Schema {
return &Schema{
Type: "number",
Type: TypeNumber,
}
}
func NewIntegerSchema() *Schema {
return &Schema{
Type: "integer",
Type: TypeInteger,
}
}
func NewInt32Schema() *Schema {
return &Schema{
Type: "integer",
Type: TypeInteger,
Format: "int32",
}
}
func NewInt64Schema() *Schema {
return &Schema{
Type: "integer",
Type: TypeInteger,
Format: "int64",
}
}
func NewStringSchema() *Schema {
return &Schema{
Type: "string",
Type: TypeString,
}
}
func NewDateTimeSchema() *Schema {
return &Schema{
Type: "string",
Type: TypeString,
Format: "date-time",
}
}
func NewUUIDSchema() *Schema {
return &Schema{
Type: "string",
Type: TypeString,
Format: "uuid",
}
}
func NewBytesSchema() *Schema {
return &Schema{
Type: "string",
Type: TypeString,
Format: "byte",
}
}
func NewArraySchema() *Schema {
return &Schema{
Type: "array",
Type: TypeArray,
}
}
func NewObjectSchema() *Schema {
return &Schema{
Type: "object",
Properties: make(map[string]*SchemaRef),
Type: TypeObject,
Properties: make(Schemas),
}
}
@ -485,7 +494,7 @@ func (schema *Schema) WithProperty(name string, propertySchema *Schema) *Schema
func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema {
properties := schema.Properties
if properties == nil {
properties = make(map[string]*SchemaRef)
properties = make(Schemas)
schema.Properties = properties
}
properties[name] = ref
@ -493,7 +502,7 @@ func (schema *Schema) WithPropertyRef(name string, ref *SchemaRef) *Schema {
}
func (schema *Schema) WithProperties(properties map[string]*Schema) *Schema {
result := make(map[string]*SchemaRef, len(properties))
result := make(Schemas, len(properties))
for k, v := range properties {
result[k] = &SchemaRef{
Value: v,
@ -638,8 +647,8 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
schemaType := schema.Type
switch schemaType {
case "":
case "boolean":
case "number":
case TypeBoolean:
case TypeNumber:
if format := schema.Format; len(format) > 0 {
switch format {
case "float", "double":
@ -649,7 +658,7 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
}
}
}
case "integer":
case TypeInteger:
if format := schema.Format; len(format) > 0 {
switch format {
case "int32", "int64":
@ -659,17 +668,21 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
}
}
}
case "string":
case TypeString:
if format := schema.Format; len(format) > 0 {
switch format {
// Supported by OpenAPIv3.0.1:
// Supported by OpenAPIv3.0.3:
// https://spec.openapis.org/oas/v3.0.3
case "byte", "binary", "date", "date-time", "password":
// In JSON Draft-07 (not validated yet though):
case "regex":
case "time", "email", "idn-email":
case "hostname", "idn-hostname", "ipv4", "ipv6":
case "uri", "uri-reference", "iri", "iri-reference", "uri-template":
case "json-pointer", "relative-json-pointer":
// In JSON Draft-07 (not validated yet though):
// https://json-schema.org/draft-07/json-schema-release-notes.html#formats
case "iri", "iri-reference", "uri-template", "idn-email", "idn-hostname":
case "json-pointer", "relative-json-pointer", "regex", "time":
// In JSON Draft 2019-09 (not validated yet though):
// https://json-schema.org/draft/2019-09/release-notes.html#format-vocabulary
case "duration", "uuid":
// Defined in some other specification
case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference":
default:
// Try to check for custom defined formats
if _, ok := SchemaStringFormats[format]; !ok && !SchemaFormatValidationDisabled {
@ -677,11 +690,16 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
}
}
}
case "array":
if schema.Pattern != "" {
if err = schema.compilePattern(); err != nil {
return err
}
}
case TypeArray:
if schema.Items == nil {
return errors.New("when schema type is 'array', schema 'items' must be non-null")
}
case "object":
case TypeObject:
default:
return fmt.Errorf("unsupported 'type' value %q", schemaType)
}
@ -716,6 +734,12 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) (err error)
}
}
if v := schema.ExternalDocs; v != nil {
if err = v.Validate(ctx); err != nil {
return fmt.Errorf("invalid external docs: %w", err)
}
}
return
}
@ -775,8 +799,6 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
}
switch value := value.(type) {
case nil:
return schema.visitJSONNull(settings)
case bool:
return schema.visitJSONBoolean(settings, value)
case float64:
@ -787,13 +809,22 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
return schema.visitJSONArray(settings, value)
case map[string]interface{}:
return schema.visitJSONObject(settings, value)
default:
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "type",
Reason: fmt.Sprintf("unhandled value of type %T", value),
case map[interface{}]interface{}: // for YAML cf. issue #444
values := make(map[string]interface{}, len(value))
for key, v := range value {
if k, ok := key.(string); ok {
values[k] = v
}
}
if len(value) == len(values) {
return schema.visitJSONObject(settings, values)
}
}
return &SchemaError{
Value: value,
Schema: schema,
SchemaField: "type",
Reason: fmt.Sprintf("unhandled value of type %T", value),
}
}
@ -820,11 +851,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
if v == nil {
return foundUnresolvedRef(ref.Ref)
}
var oldfailfast bool
oldfailfast, settings.failfast = settings.failfast, true
err := v.visitJSON(settings, value)
settings.failfast = oldfailfast
if err == nil {
if err := v.visitJSON(settings, value); err == nil {
if settings.failfast {
return errSchema
}
@ -837,33 +864,57 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
}
if v := schema.OneOf; len(v) > 0 {
var discriminatorRef string
if schema.Discriminator != nil {
pn := schema.Discriminator.PropertyName
if valuemap, okcheck := value.(map[string]interface{}); okcheck {
discriminatorVal, okcheck := valuemap[pn]
if !okcheck {
return errors.New("input does not contain the discriminator property")
}
discriminatorValString, okcheck := discriminatorVal.(string)
if !okcheck {
return errors.New("descriminator value is not a string")
}
if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck {
return errors.New("input does not contain a valid discriminator value")
}
}
}
ok := 0
validationErrors := []error{}
for _, item := range v {
v := item.Value
if v == nil {
return foundUnresolvedRef(item.Ref)
}
var oldfailfast bool
oldfailfast, settings.failfast = settings.failfast, true
err := v.visitJSON(settings, value)
settings.failfast = oldfailfast
if err == nil {
if schema.Discriminator != nil {
pn := schema.Discriminator.PropertyName
if valuemap, okcheck := value.(map[string]interface{}); okcheck {
if discriminatorVal, okcheck := valuemap[pn]; okcheck == true {
mapref, okcheck := schema.Discriminator.Mapping[discriminatorVal.(string)]
if okcheck && mapref == item.Ref {
ok++
}
}
}
} else {
ok++
}
if discriminatorRef != "" && discriminatorRef != item.Ref {
continue
}
if err := v.visitJSON(settings, value); err != nil {
validationErrors = append(validationErrors, err)
continue
}
ok++
}
if ok != 1 {
if len(validationErrors) > 1 {
errorMessage := ""
for _, err := range validationErrors {
if errorMessage != "" {
errorMessage += " Or "
}
errorMessage += err.Error()
}
return errors.New("doesn't match schema due to: " + errorMessage)
}
if settings.failfast {
return errSchema
}
@ -874,7 +925,10 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
}
if ok > 1 {
e.Origin = ErrOneOfConflict
} else if len(validationErrors) == 1 {
e.Origin = validationErrors[0]
}
return e
}
}
@ -886,11 +940,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
if v == nil {
return foundUnresolvedRef(item.Ref)
}
var oldfailfast bool
oldfailfast, settings.failfast = settings.failfast, true
err := v.visitJSON(settings, value)
settings.failfast = oldfailfast
if err == nil {
if err := v.visitJSON(settings, value); err == nil {
ok = true
break
}
@ -912,11 +962,7 @@ func (schema *Schema) visitSetOperations(settings *schemaValidationSettings, val
if v == nil {
return foundUnresolvedRef(item.Ref)
}
var oldfailfast bool
oldfailfast, settings.failfast = settings.failfast, false
err := v.visitJSON(settings, value)
settings.failfast = oldfailfast
if err != nil {
if err := v.visitJSON(settings, value); err != nil {
if settings.failfast {
return errSchema
}
@ -952,8 +998,8 @@ func (schema *Schema) VisitJSONBoolean(value bool) error {
}
func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) {
if schemaType := schema.Type; schemaType != "" && schemaType != "boolean" {
return schema.expectedType(settings, "boolean")
if schemaType := schema.Type; schemaType != "" && schemaType != TypeBoolean {
return schema.expectedType(settings, TypeBoolean)
}
return
}
@ -982,7 +1028,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
}
me = append(me, err)
}
} else if schemaType != "" && schemaType != "number" {
} else if schemaType != "" && schemaType != TypeNumber {
return schema.expectedType(settings, "number, integer")
}
@ -1046,7 +1092,7 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value
Value: value,
Schema: schema,
SchemaField: "maximum",
Reason: fmt.Sprintf("number must be most %g", *v),
Reason: fmt.Sprintf("number must be at most %g", *v),
}
if !settings.multiError {
return err
@ -1087,8 +1133,8 @@ func (schema *Schema) VisitJSONString(value string) error {
}
func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error {
if schemaType := schema.Type; schemaType != "" && schemaType != "string" {
return schema.expectedType(settings, "string")
if schemaType := schema.Type; schemaType != "" && schemaType != TypeString {
return schema.expectedType(settings, TypeString)
}
var me MultiError
@ -1139,15 +1185,9 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
}
// "pattern"
if pattern := schema.Pattern; pattern != "" && schema.compiledPattern == nil {
if schema.Pattern != "" && schema.compiledPattern == nil {
var err error
if schema.compiledPattern, err = regexp.Compile(pattern); err != nil {
err = &SchemaError{
Value: value,
Schema: schema,
SchemaField: "pattern",
Reason: fmt.Sprintf("cannot compile pattern %q: %v", pattern, err),
}
if err = schema.compilePattern(); err != nil {
if !settings.multiError {
return err
}
@ -1159,7 +1199,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
Value: value,
Schema: schema,
SchemaField: "pattern",
Reason: fmt.Sprintf("string doesn't match the regular expression %q", schema.Pattern),
Reason: fmt.Sprintf(`string doesn't match the regular expression "%s"`, schema.Pattern),
}
if !settings.multiError {
return err
@ -1174,7 +1214,7 @@ func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value
switch {
case f.regexp != nil && f.callback == nil:
if cp := f.regexp; !cp.MatchString(value) {
formatErr = fmt.Sprintf("string doesn't match the format %q (regular expression %q)", format, cp.String())
formatErr = fmt.Sprintf(`string doesn't match the format %q (regular expression "%s")`, format, cp.String())
}
case f.regexp == nil && f.callback != nil:
if err := f.callback(value); err != nil {
@ -1212,8 +1252,8 @@ func (schema *Schema) VisitJSONArray(value []interface{}) error {
}
func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error {
if schemaType := schema.Type; schemaType != "" && schemaType != "array" {
return schema.expectedType(settings, "array")
if schemaType := schema.Type; schemaType != "" && schemaType != TypeArray {
return schema.expectedType(settings, TypeArray)
}
var me MultiError
@ -1308,8 +1348,8 @@ func (schema *Schema) VisitJSONObject(value map[string]interface{}) error {
}
func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error {
if schemaType := schema.Type; schemaType != "" && schemaType != "object" {
return schema.expectedType(settings, "object")
if schemaType := schema.Type; schemaType != "" && schemaType != TypeObject {
return schema.expectedType(settings, TypeObject)
}
var me MultiError
@ -1383,7 +1423,7 @@ func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value
}
}
allowed := schema.AdditionalPropertiesAllowed
if additionalProperties != nil || allowed == nil || (allowed != nil && *allowed) {
if additionalProperties != nil || allowed == nil || *allowed {
if additionalProperties != nil {
if err := additionalProperties.visitJSON(settings, v); err != nil {
if settings.failfast {
@ -1461,6 +1501,17 @@ func (schema *Schema) expectedType(settings *schemaValidationSettings, typ strin
}
}
func (schema *Schema) compilePattern() (err error) {
if schema.compiledPattern, err = regexp.Compile(schema.Pattern); err != nil {
return &SchemaError{
Schema: schema,
SchemaField: "pattern",
Reason: fmt.Sprintf("cannot compile pattern %q: %v", schema.Pattern, err),
}
}
return nil
}
type SchemaError struct {
Value interface{}
reversePath []string
@ -1470,6 +1521,8 @@ type SchemaError struct {
Origin error
}
var _ interface{ Unwrap() error } = SchemaError{}
func markSchemaErrorKey(err error, key string) error {
if v, ok := err.(*SchemaError); ok {
v.reversePath = append(v.reversePath, key)
@ -1545,6 +1598,10 @@ func (err *SchemaError) Error() string {
return buf.String()
}
func (err SchemaError) Unwrap() error {
return err.Origin
}
func isSliceOfUniqueItems(xs []interface{}) bool {
s := len(xs)
m := make(map[string]struct{}, s)

View file

@ -4,11 +4,12 @@ import (
"fmt"
"net"
"regexp"
"strings"
)
const (
// FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122
FormatOfStringForUUIDOfRFC4122 = `^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-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}$`
)
//FormatCallback custom check on exotic formats
@ -37,24 +38,23 @@ func DefineStringFormatCallback(name string, callback FormatCallback) {
SchemaStringFormats[name] = Format{callback: callback}
}
func validateIP(ip string) (*net.IP, error) {
func validateIP(ip string) error {
parsed := net.ParseIP(ip)
if parsed == nil {
return nil, &SchemaError{
return &SchemaError{
Value: ip,
Reason: "Not an IP address",
}
}
return &parsed, nil
return nil
}
func validateIPv4(ip string) error {
parsed, err := validateIP(ip)
if err != nil {
if err := validateIP(ip); err != nil {
return err
}
if parsed.To4() == nil {
if !(strings.Count(ip, ":") < 2) {
return &SchemaError{
Value: ip,
Reason: "Not an IPv4 address (it's IPv6)",
@ -62,13 +62,13 @@ func validateIPv4(ip string) error {
}
return nil
}
func validateIPv6(ip string) error {
parsed, err := validateIP(ip)
if err != nil {
if err := validateIP(ip); err != nil {
return err
}
if parsed.To4() != nil {
if !(strings.Count(ip, ":") >= 2) {
return &SchemaError{
Value: ip,
Reason: "Not an IPv6 address (it's IPv4)",
@ -90,7 +90,7 @@ func init() {
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})?$`)
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})?$`)
}

View file

@ -24,6 +24,8 @@ func (value SecurityRequirements) Validate(ctx context.Context) error {
return nil
}
// SecurityRequirement is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#securityRequirementObject
type SecurityRequirement map[string][]string
func NewSecurityRequirement() SecurityRequirement {

View file

@ -25,6 +25,8 @@ func (s SecuritySchemes) JSONLookup(token string) (interface{}, error) {
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
type SecurityScheme struct {
ExtensionProps
@ -166,8 +168,11 @@ func (value *SecurityScheme) Validate(ctx context.Context) error {
return nil
}
// OAuthFlows is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowsObject
type OAuthFlows struct {
ExtensionProps
Implicit *OAuthFlow `json:"implicit,omitempty" yaml:"implicit,omitempty"`
Password *OAuthFlow `json:"password,omitempty" yaml:"password,omitempty"`
ClientCredentials *OAuthFlow `json:"clientCredentials,omitempty" yaml:"clientCredentials,omitempty"`
@ -207,8 +212,11 @@ func (flows *OAuthFlows) Validate(ctx context.Context) error {
return errors.New("no OAuth flow is defined")
}
// OAuthFlow is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauthFlowObject
type OAuthFlow struct {
ExtensionProps
AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"`
TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"`
RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"`

View file

@ -11,7 +11,7 @@ import (
"github.com/getkin/kin-openapi/jsoninfo"
)
// Servers is specified by OpenAPI/Swagger standard version 3.0.
// Servers is specified by OpenAPI/Swagger standard version 3.
type Servers []*Server
// Validate ensures servers are per the OpenAPIv3 specification.
@ -38,9 +38,11 @@ func (servers Servers) MatchURL(parsedURL *url.URL) (*Server, []string, string)
return nil, nil, ""
}
// Server is specified by OpenAPI/Swagger standard version 3.0.
// Server is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#serverObject
type Server struct {
ExtensionProps
URL string `json:"url" yaml:"url"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
Variables map[string]*ServerVariable `json:"variables,omitempty" yaml:"variables,omitempty"`
@ -147,9 +149,11 @@ func (value *Server) Validate(ctx context.Context) (err error) {
return
}
// ServerVariable is specified by OpenAPI/Swagger standard version 3.0.
// 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
Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"`
Default string `json:"default,omitempty" yaml:"default,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`

View file

@ -1,6 +1,11 @@
package openapi3
import "github.com/getkin/kin-openapi/jsoninfo"
import (
"context"
"fmt"
"github.com/getkin/kin-openapi/jsoninfo"
)
// Tags is specified by OpenAPI/Swagger 3.0 standard.
type Tags []*Tag
@ -14,9 +19,20 @@ func (tags Tags) Get(name string) *Tag {
return nil
}
func (tags Tags) Validate(ctx context.Context) error {
for _, v := range tags {
if err := v.Validate(ctx); err != nil {
return err
}
}
return nil
}
// Tag is specified by OpenAPI/Swagger 3.0 standard.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#tagObject
type Tag struct {
ExtensionProps
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Description string `json:"description,omitempty" yaml:"description,omitempty"`
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"`
@ -29,3 +45,12 @@ func (t *Tag) MarshalJSON() ([]byte, error) {
func (t *Tag) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, t)
}
func (t *Tag) Validate(ctx context.Context) error {
if v := t.ExternalDocs; v != nil {
if err := v.Validate(ctx); err != nil {
return fmt.Errorf("invalid external docs: %w", err)
}
}
return nil
}

View file

@ -0,0 +1,2 @@
type: string
example: bar

View file

@ -0,0 +1,4 @@
type: object
properties:
bar:
$ref: ../openapi.yml#/components/schemas/Bar

View file

@ -0,0 +1,4 @@
type: object
properties:
foo:
$ref: ../../openapi.yml#/components/schemas/Foo

View file

@ -0,0 +1,15 @@
openapi: "3.0.3"
info:
title: Recursive refs example
version: "1.0"
paths:
/foo:
$ref: ./paths/foo.yml
components:
schemas:
Foo:
$ref: ./components/Foo.yml
Foo2:
$ref: ./components/Foo/Foo2.yml
Bar:
$ref: ./components/Bar.yml

View file

@ -0,0 +1,11 @@
get:
responses:
"200":
description: OK
content:
application/json:
schema:
type: object
properties:
foo2:
$ref: ../openapi.yml#/components/schemas/Foo2

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

@ -0,0 +1,31 @@
package openapi3
import (
"context"
"github.com/getkin/kin-openapi/jsoninfo"
)
// XML is specified by OpenAPI/Swagger standard version 3.
// See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#xmlObject
type XML struct {
ExtensionProps
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
Attribute bool `json:"attribute,omitempty" yaml:"attribute,omitempty"`
Wrapped bool `json:"wrapped,omitempty" yaml:"wrapped,omitempty"`
}
func (value *XML) MarshalJSON() ([]byte, error) {
return jsoninfo.MarshalStrictStruct(value)
}
func (value *XML) UnmarshalJSON(data []byte) error {
return jsoninfo.UnmarshalStrictStruct(data, value)
}
func (value *XML) Validate(ctx context.Context) error {
return nil // TODO
}