392 lines
12 KiB
Go
392 lines
12 KiB
Go
package openapi3
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"net/url"
|
|
"slices"
|
|
|
|
"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#openapi-object
|
|
// and https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#openapi-object
|
|
type T struct {
|
|
Extensions map[string]any `json:"-" yaml:"-"`
|
|
Origin *Origin `json:"-" yaml:"-"`
|
|
|
|
OpenAPI string `json:"openapi" yaml:"openapi"` // Required
|
|
Components *Components `json:"components,omitempty" yaml:"components,omitempty"`
|
|
Info *Info `json:"info" yaml:"info"` // Required
|
|
Paths *Paths `json:"paths" yaml:"paths"` // Required in 3.0, optional in 3.1
|
|
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"`
|
|
Webhooks map[string]*PathItem `json:"webhooks,omitempty" yaml:"webhooks,omitempty"` // OpenAPI >=3.1
|
|
JSONSchemaDialect string `json:"jsonSchemaDialect,omitempty" yaml:"jsonSchemaDialect,omitempty"` // OpenAPI >=3.1
|
|
|
|
visited visitedComponent
|
|
url *url.URL
|
|
|
|
// Document-scoped format validators
|
|
// These validators are automatically used by all schemas in this document
|
|
stringFormats map[string]StringFormatValidator
|
|
numberFormats map[string]NumberFormatValidator
|
|
integerFormats map[string]IntegerFormatValidator
|
|
}
|
|
|
|
// IsOpenAPI30 returns whether doc is an OpenAPI document version 3.0.x.
|
|
// Returns true for 3, 3.0, 3.0.0, 3.0.1, 3.0.2, 3.0.3, 3.0.4, ...
|
|
// And false for 3.1.0, 3.2, ... and for invalid strings.
|
|
func (doc *T) IsOpenAPI30() bool {
|
|
return doc.OpenAPIMajorMinor() == "3.0"
|
|
}
|
|
|
|
// IsOpenAPI31OrLater returns whether doc is an OpenAPI document version >=3.1.
|
|
// Returns true for 3.1, 3.1.0, 3.1.1, 3.1.2, 3.2.0, ...
|
|
// And false for cases where IsOpenAPI30 returns true and for invalid strings.
|
|
func (doc *T) IsOpenAPI31OrLater() bool {
|
|
return slices.Contains([]string{"3.1", "3.2"}, doc.OpenAPIMajorMinor())
|
|
}
|
|
|
|
func errFieldFor31Plus(field string, origin *Origin) error {
|
|
return newFieldFor31Plus(field, origin)
|
|
}
|
|
|
|
func errValueOfFieldFor31Plus(value any, field string) error {
|
|
return fmt.Errorf("value %q of field %s is for OpenAPI >=3.1", value, field)
|
|
}
|
|
|
|
// OpenAPIMajorMinor returns 3.y of the OpenAPI "3.y" or "3.y.z" version of the document.
|
|
// Returns the empty string for invalid OpenAPI version strings.
|
|
func (doc *T) OpenAPIMajorMinor() string {
|
|
if doc == nil {
|
|
return ""
|
|
}
|
|
switch doc.OpenAPI {
|
|
case "3", "3.0", "3.0.0", "3.0.1", "3.0.2", "3.0.3", "3.0.4":
|
|
return "3.0"
|
|
case "3.1", "3.1.0", "3.1.1", "3.1.2":
|
|
return "3.1"
|
|
case "3.2", "3.2.0":
|
|
return "3.2"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
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
|
|
case "webhooks":
|
|
return doc.Webhooks, nil
|
|
case "jsonSchemaDialect":
|
|
return doc.JSONSchemaDialect, nil
|
|
}
|
|
|
|
v, _, err := jsonpointer.GetForToken(doc.Extensions, token)
|
|
return v, err
|
|
}
|
|
|
|
// MarshalJSON returns the JSON encoding of T.
|
|
func (doc *T) MarshalJSON() ([]byte, error) {
|
|
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, 10+len(doc.Extensions))
|
|
maps.Copy(m, doc.Extensions)
|
|
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
|
|
}
|
|
if x := doc.Webhooks; len(x) != 0 {
|
|
m["webhooks"] = x
|
|
}
|
|
if x := doc.JSONSchemaDialect; x != "" {
|
|
m["jsonSchemaDialect"] = x
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// UnmarshalJSON sets T to a copy of data.
|
|
func (doc *T) UnmarshalJSON(data []byte) error {
|
|
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")
|
|
delete(x.Extensions, "webhooks")
|
|
delete(x.Extensions, "jsonSchemaDialect")
|
|
if len(x.Extensions) == 0 {
|
|
x.Extensions = nil
|
|
}
|
|
delete(x.Webhooks, originKey)
|
|
*doc = T(x)
|
|
return nil
|
|
}
|
|
|
|
func (doc *T) AddOperation(path string, method string, operation *Operation) {
|
|
if doc.Paths == nil {
|
|
doc.Paths = NewPaths()
|
|
}
|
|
pathItem := doc.Paths.Value(path)
|
|
if pathItem == nil {
|
|
pathItem = &PathItem{}
|
|
doc.Paths.Set(path, pathItem)
|
|
}
|
|
pathItem.SetOperation(method, operation)
|
|
}
|
|
|
|
func (doc *T) AddServer(server *Server) {
|
|
doc.Servers = append(doc.Servers, server)
|
|
}
|
|
|
|
func (doc *T) AddServers(servers ...*Server) {
|
|
doc.Servers = append(doc.Servers, servers...)
|
|
}
|
|
|
|
// SetStringFormatValidators sets document-scoped string format validators.
|
|
// These validators are automatically used by all schemas in this document.
|
|
func (doc *T) SetStringFormatValidators(validators map[string]StringFormatValidator) {
|
|
doc.stringFormats = validators
|
|
}
|
|
|
|
// SetStringFormatValidator sets a single document-scoped string format validator.
|
|
func (doc *T) SetStringFormatValidator(name string, validator StringFormatValidator) {
|
|
if doc.stringFormats == nil {
|
|
doc.stringFormats = make(map[string]StringFormatValidator)
|
|
}
|
|
doc.stringFormats[name] = validator
|
|
}
|
|
|
|
// SetNumberFormatValidators sets document-scoped number format validators.
|
|
// These validators are automatically used by all schemas in this document.
|
|
func (doc *T) SetNumberFormatValidators(validators map[string]NumberFormatValidator) {
|
|
doc.numberFormats = validators
|
|
}
|
|
|
|
// SetNumberFormatValidator sets a single document-scoped number format validator.
|
|
func (doc *T) SetNumberFormatValidator(name string, validator NumberFormatValidator) {
|
|
if doc.numberFormats == nil {
|
|
doc.numberFormats = make(map[string]NumberFormatValidator)
|
|
}
|
|
doc.numberFormats[name] = validator
|
|
}
|
|
|
|
// SetIntegerFormatValidators sets document-scoped integer format validators.
|
|
// These validators are automatically used by all schemas in this document.
|
|
func (doc *T) SetIntegerFormatValidators(validators map[string]IntegerFormatValidator) {
|
|
doc.integerFormats = validators
|
|
}
|
|
|
|
// SetIntegerFormatValidator sets a single document-scoped integer format validator.
|
|
func (doc *T) SetIntegerFormatValidator(name string, validator IntegerFormatValidator) {
|
|
if doc.integerFormats == nil {
|
|
doc.integerFormats = make(map[string]IntegerFormatValidator)
|
|
}
|
|
doc.integerFormats[name] = validator
|
|
}
|
|
|
|
// GetSchemaValidationOptions returns SchemaValidationOptions that include
|
|
// this document's format validators. Use this when validating schemas from this document.
|
|
func (doc *T) GetSchemaValidationOptions() []SchemaValidationOption {
|
|
var opts []SchemaValidationOption
|
|
if doc.stringFormats != nil {
|
|
opts = append(opts, WithStringFormatValidators(doc.stringFormats))
|
|
}
|
|
if doc.numberFormats != nil {
|
|
opts = append(opts, WithNumberFormatValidators(doc.numberFormats))
|
|
}
|
|
if doc.integerFormats != nil {
|
|
opts = append(opts, WithIntegerFormatValidators(doc.integerFormats))
|
|
}
|
|
return opts
|
|
}
|
|
|
|
// Validate returns an error if T does not comply with the OpenAPI spec.
|
|
// Validations Options can be provided to modify the validation behavior.
|
|
//
|
|
// By default, doc.OpenAPI's field dictates whether "JSON Schema Draft 2020-12" validation
|
|
// is enabled.
|
|
func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error {
|
|
if doc.IsOpenAPI31OrLater() {
|
|
opts = append(opts, IsOpenAPI31OrLater())
|
|
}
|
|
ctx = WithValidationOptions(ctx, opts...)
|
|
me := newErrCollector(ctx)
|
|
|
|
if doc.OpenAPI == "" {
|
|
if err := me.emit(newOpenAPIVersionRequired(doc.Origin)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if doc.Webhooks != nil && !doc.IsOpenAPI31OrLater() {
|
|
if err := me.emit(newWebhooksFieldFor31Plus(doc.Origin)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if doc.JSONSchemaDialect != "" && !doc.IsOpenAPI31OrLater() {
|
|
if err := me.emit(newJSONSchemaDialectFieldFor31Plus(doc.Origin)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
wrapSection := func(section string) func(error) error {
|
|
return func(e error) error { return &SectionValidationError{Section: section, Cause: e} }
|
|
}
|
|
|
|
var wrap func(error) error
|
|
|
|
wrap = wrapSection("components")
|
|
if v := doc.Components; v != nil {
|
|
if err := me.emitWrapped(wrap, v.Validate(ctx)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
wrap = wrapSection("info")
|
|
if v := doc.Info; v != nil {
|
|
if err := me.emitWrapped(wrap, v.Validate(ctx)); err != nil {
|
|
return err
|
|
}
|
|
} else if err := me.emit(wrap(newInfoRequired(doc.Origin))); err != nil {
|
|
return err
|
|
}
|
|
|
|
wrap = wrapSection("paths")
|
|
if v := doc.Paths; v != nil {
|
|
if err := me.emitWrapped(wrap, v.Validate(ctx)); err != nil {
|
|
return err
|
|
}
|
|
} else if doc.IsOpenAPI30() {
|
|
if err := me.emit(wrap(newPathsRequired(doc.Origin))); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
wrap = wrapSection("security")
|
|
if v := doc.Security; v != nil {
|
|
if err := me.emitWrapped(wrap, v.Validate(ctx)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
wrap = wrapSection("servers")
|
|
if v := doc.Servers; v != nil {
|
|
if err := me.emitWrapped(wrap, v.Validate(ctx)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
wrap = wrapSection("tags")
|
|
if v := doc.Tags; v != nil {
|
|
if err := me.emitWrapped(wrap, v.Validate(ctx)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
wrap = wrapSection("external docs")
|
|
if v := doc.ExternalDocs; v != nil {
|
|
if err := me.emitWrapped(wrap, v.Validate(ctx)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
wrap = wrapSection("webhooks")
|
|
for _, name := range componentNames(doc.Webhooks) {
|
|
pathItem := doc.Webhooks[name]
|
|
if pathItem == nil {
|
|
if err := me.emit(wrap(newWebhookNil(name))); err != nil {
|
|
return err
|
|
}
|
|
// Nothing to descend into for a nil webhook; the nil itself
|
|
// is the only finding under this name until the entry is
|
|
// populated, so continue to the next webhook.
|
|
continue
|
|
}
|
|
wrapWebhook := func(e error) error { return wrap(&WebhookValidationError{Name: name, Cause: e}) }
|
|
if err := me.emitWrapped(wrapWebhook, pathItem.Validate(ctx)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
wrap = wrapSection("jsonSchemaDialect")
|
|
if doc.JSONSchemaDialect != "" {
|
|
u, err := url.Parse(doc.JSONSchemaDialect)
|
|
if err != nil {
|
|
if err = me.emit(wrap(err)); err != nil {
|
|
return err
|
|
}
|
|
} else if u.Scheme == "" {
|
|
if err := me.emit(wrap(newJSONSchemaDialectAbsoluteURIRequired(doc.Origin))); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return me.finalize(validateExtensions(ctx, doc.Extensions, doc.Origin))
|
|
}
|
|
|
|
// ValidateSchemaJSON validates data against a schema using this document's format validators.
|
|
// This is a convenience method that automatically applies the document's format validators.
|
|
func (doc *T) ValidateSchemaJSON(schema *Schema, value any, opts ...SchemaValidationOption) error {
|
|
// Combine document's validators with any additional options
|
|
allOpts := append(doc.GetSchemaValidationOptions(), opts...)
|
|
return schema.VisitJSON(value, allOpts...)
|
|
}
|