1504 lines
42 KiB
Go
1504 lines
42 KiB
Go
package openapi3
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// IncludeOrigin specifies whether to include the origin of the OpenAPI elements.
|
|
// Deprecated: set Loader.IncludeOrigin instead. This global is read by NewLoader
|
|
// for backward compatibility but is not safe for concurrent use.
|
|
var IncludeOrigin = false
|
|
|
|
func failedToResolveRefFragmentPart(value, what string) error {
|
|
return fmt.Errorf("failed to resolve %q in fragment in URI: %q", what, value)
|
|
}
|
|
|
|
// Loader helps deserialize an OpenAPIv3 document
|
|
type Loader struct {
|
|
// IsExternalRefsAllowed enables visiting other files. Enforced only when
|
|
// ReadFromURIFunc is nil; a custom ReadFromURIFunc bypasses this flag and
|
|
// owns the access policy itself — see ReadFromURIFunc.
|
|
IsExternalRefsAllowed bool
|
|
|
|
// IncludeOrigin enables recording the file/line/column of each OpenAPI element.
|
|
// Prefer this over the package-level IncludeOrigin global, which is not safe for
|
|
// concurrent use.
|
|
IncludeOrigin bool
|
|
|
|
// ReadFromURIFunc overrides how the loader reads a referenced file or URL.
|
|
//
|
|
// SECURITY: when a custom ReadFromURIFunc is set, IsExternalRefsAllowed is
|
|
// NOT enforced — this function alone decides which locations may be read. A
|
|
// func that reads whatever URI it is handed (e.g. by delegating to
|
|
// DefaultReadFromURI) resolves external $refs even when IsExternalRefsAllowed
|
|
// is false, which on untrusted documents enables local file reads
|
|
// (`$ref: "/etc/passwd"`) and SSRF (`$ref: "http://169.254.169.254/..."`).
|
|
// A custom func must apply its own scheme/host allowlist, or re-check
|
|
// IsExternalRefsAllowed, before reading.
|
|
ReadFromURIFunc ReadFromURIFunc
|
|
|
|
// JoinFunc allows overriding how relative $ref paths are resolved against
|
|
// a base path. When set, it is called instead of the default join logic
|
|
// that uses path.Dir and path.Join. This is useful when loading specs from
|
|
// non-filesystem sources (e.g. git objects, remote archives) where the base
|
|
// path follows a different convention than filesystem paths.
|
|
JoinFunc func(basePath *url.URL, relativePath *url.URL) *url.URL
|
|
|
|
Context context.Context
|
|
|
|
rootDir string
|
|
rootLocation string
|
|
|
|
visitedPathItemRefs map[string]struct{}
|
|
|
|
visitedDocuments map[string]*T
|
|
|
|
visitedRefs map[string]struct{}
|
|
visitedPath []string
|
|
backtrack map[string][]func(value any)
|
|
}
|
|
|
|
// NewLoader returns an empty Loader
|
|
func NewLoader() *Loader {
|
|
return &Loader{
|
|
Context: context.Background(),
|
|
IncludeOrigin: IncludeOrigin,
|
|
}
|
|
}
|
|
|
|
func (loader *Loader) resetVisitedPathItemRefs() {
|
|
loader.visitedPathItemRefs = make(map[string]struct{})
|
|
loader.visitedRefs = make(map[string]struct{})
|
|
loader.visitedPath = nil
|
|
loader.backtrack = make(map[string][]func(value any))
|
|
}
|
|
|
|
// LoadFromURI loads a spec from a remote URL
|
|
func (loader *Loader) LoadFromURI(location *url.URL) (*T, error) {
|
|
loader.resetVisitedPathItemRefs()
|
|
return loader.loadFromURIInternal(location)
|
|
}
|
|
|
|
// 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)})
|
|
}
|
|
|
|
func (loader *Loader) loadFromURIInternal(location *url.URL) (*T, error) {
|
|
data, err := loader.readURL(location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return loader.loadFromDataWithPathInternal(data, location)
|
|
}
|
|
|
|
func (loader *Loader) allowsExternalRefs(ref string) (err error) {
|
|
if !loader.IsExternalRefsAllowed {
|
|
err = fmt.Errorf("encountered disallowed external reference: %q", ref)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (loader *Loader) loadSingleElementFromURI(ref string, rootPath *url.URL, element any) (*url.URL, error) {
|
|
// IsExternalRefsAllowed is enforced here only when no custom ReadFromURIFunc
|
|
// is installed; otherwise the custom func owns the access policy (see the
|
|
// SECURITY note on the ReadFromURIFunc field).
|
|
if loader.ReadFromURIFunc == nil {
|
|
if err := loader.allowsExternalRefs(ref); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
resolvedPath, err := loader.resolvePathWithRef(ref, rootPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if frag := resolvedPath.Fragment; frag != "" {
|
|
return nil, fmt.Errorf("unexpected ref fragment %q", frag)
|
|
}
|
|
|
|
data, err := loader.readURL(resolvedPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := unmarshal(data, element, loader.IncludeOrigin, resolvedPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return resolvedPath, nil
|
|
}
|
|
|
|
func (loader *Loader) readURL(location *url.URL) ([]byte, error) {
|
|
if f := loader.ReadFromURIFunc; f != nil {
|
|
return f(loader, location)
|
|
}
|
|
return DefaultReadFromURI(loader, location)
|
|
}
|
|
|
|
// LoadFromStdin loads a spec from stdin
|
|
func (loader *Loader) LoadFromStdin() (*T, error) {
|
|
return loader.LoadFromIoReader(os.Stdin)
|
|
}
|
|
|
|
// LoadFromStdin loads a spec from io.Reader
|
|
func (loader *Loader) LoadFromIoReader(reader io.Reader) (*T, error) {
|
|
if reader == nil {
|
|
return nil, fmt.Errorf("invalid reader: %v", reader)
|
|
}
|
|
|
|
data, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return loader.LoadFromData(data)
|
|
}
|
|
|
|
// LoadFromData loads a spec from a byte array
|
|
func (loader *Loader) LoadFromData(data []byte) (*T, error) {
|
|
loader.resetVisitedPathItemRefs()
|
|
doc := &T{}
|
|
if err := unmarshal(data, doc, loader.IncludeOrigin, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := loader.ResolveRefsIn(doc, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
return doc, nil
|
|
}
|
|
|
|
// LoadFromDataWithPath takes the OpenAPI document data in bytes and a path where the resolver can find referred
|
|
// elements and returns a *T with all resolved data or an error if unable to load data or resolve refs.
|
|
func (loader *Loader) LoadFromDataWithPath(data []byte, location *url.URL) (*T, error) {
|
|
loader.resetVisitedPathItemRefs()
|
|
return loader.loadFromDataWithPathInternal(data, location)
|
|
}
|
|
|
|
func (loader *Loader) loadFromDataWithPathInternal(data []byte, location *url.URL) (*T, error) {
|
|
if loader.visitedDocuments == nil {
|
|
loader.visitedDocuments = make(map[string]*T)
|
|
loader.rootLocation = location.Path
|
|
}
|
|
uri := location.String()
|
|
if doc, ok := loader.visitedDocuments[uri]; ok {
|
|
return doc, nil
|
|
}
|
|
|
|
doc := &T{}
|
|
loader.visitedDocuments[uri] = doc
|
|
|
|
if err := unmarshal(data, doc, loader.IncludeOrigin, location); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
doc.url = copyURI(location)
|
|
|
|
if err := loader.ResolveRefsIn(doc, location); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return doc, nil
|
|
}
|
|
|
|
// ResolveRefsIn expands references if for instance spec was just unmarshaled
|
|
func (loader *Loader) ResolveRefsIn(doc *T, location *url.URL) (err error) {
|
|
if loader.Context == nil {
|
|
loader.Context = context.Background()
|
|
}
|
|
|
|
if loader.visitedPathItemRefs == nil {
|
|
loader.resetVisitedPathItemRefs()
|
|
}
|
|
|
|
if components := doc.Components; components != nil {
|
|
for _, name := range componentNames(components.Headers) {
|
|
component := components.Headers[name]
|
|
if err = loader.resolveHeaderRef(doc, component, location); err != nil {
|
|
return
|
|
}
|
|
}
|
|
for _, name := range componentNames(components.Parameters) {
|
|
component := components.Parameters[name]
|
|
if err = loader.resolveParameterRef(doc, component, location); err != nil {
|
|
return
|
|
}
|
|
}
|
|
for _, name := range componentNames(components.RequestBodies) {
|
|
component := components.RequestBodies[name]
|
|
if err = loader.resolveRequestBodyRef(doc, component, location); err != nil {
|
|
return
|
|
}
|
|
}
|
|
for _, name := range componentNames(components.Responses) {
|
|
component := components.Responses[name]
|
|
if err = loader.resolveResponseRef(doc, component, location); err != nil {
|
|
return
|
|
}
|
|
}
|
|
for _, name := range componentNames(components.Schemas) {
|
|
component := components.Schemas[name]
|
|
if err = loader.resolveSchemaRef(doc, component, location, []string{}); err != nil {
|
|
return
|
|
}
|
|
}
|
|
for _, name := range componentNames(components.SecuritySchemes) {
|
|
component := components.SecuritySchemes[name]
|
|
if err = loader.resolveSecuritySchemeRef(doc, component, location); err != nil {
|
|
return
|
|
}
|
|
}
|
|
for _, name := range componentNames(components.Examples) {
|
|
component := components.Examples[name]
|
|
if err = loader.resolveExampleRef(doc, component, location); err != nil {
|
|
return
|
|
}
|
|
}
|
|
for _, name := range componentNames(components.Callbacks) {
|
|
component := components.Callbacks[name]
|
|
if err = loader.resolveCallbackRef(doc, component, location); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Visit all operations
|
|
pathItems := doc.Paths.Map()
|
|
for _, name := range componentNames(pathItems) {
|
|
pathItem := pathItems[name]
|
|
if pathItem == nil {
|
|
continue
|
|
}
|
|
if err = loader.resolvePathItemRef(doc, pathItem, location); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
for _, name := range componentNames(doc.Webhooks) {
|
|
if pathItem := doc.Webhooks[name]; pathItem != nil {
|
|
if err = loader.resolvePathItemRef(doc, pathItem, location); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func defaultJoin(basePath *url.URL, relativePath *url.URL) *url.URL {
|
|
if basePath == nil {
|
|
return relativePath
|
|
}
|
|
newPath := *basePath
|
|
newPath.Path = path.Join(path.Dir(newPath.Path), relativePath.Path)
|
|
return &newPath
|
|
}
|
|
|
|
func (loader *Loader) resolvePath(basePath *url.URL, componentPath *url.URL) *url.URL {
|
|
if is_file(componentPath) {
|
|
// support absolute paths
|
|
if filepath.IsAbs(componentPath.Path) {
|
|
return componentPath
|
|
}
|
|
if loader.JoinFunc != nil {
|
|
return loader.JoinFunc(basePath, componentPath)
|
|
}
|
|
return defaultJoin(basePath, componentPath)
|
|
}
|
|
return componentPath
|
|
}
|
|
|
|
func (loader *Loader) resolvePathWithRef(ref string, rootPath *url.URL) (*url.URL, error) {
|
|
parsedURL, err := url.Parse(ref)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot parse reference: %q: %w", ref, err)
|
|
}
|
|
|
|
resolvedPath := loader.resolvePath(rootPath, parsedURL)
|
|
resolvedPath.Fragment = parsedURL.Fragment
|
|
return resolvedPath, nil
|
|
}
|
|
|
|
func (loader *Loader) resolveRefPath(ref string, path *url.URL) (*url.URL, error) {
|
|
if ref != "" && ref[0] == '#' {
|
|
path = copyURI(path)
|
|
// Resolving internal refs of a doc loaded from memory
|
|
// has no path, so just set the Fragment.
|
|
if path == nil {
|
|
path = new(url.URL)
|
|
}
|
|
|
|
path.Fragment = ref
|
|
return path, nil
|
|
}
|
|
|
|
// IsExternalRefsAllowed is enforced here only when no custom ReadFromURIFunc
|
|
// is installed; otherwise the custom func owns the access policy (see the
|
|
// SECURITY note on the ReadFromURIFunc field).
|
|
if loader.ReadFromURIFunc == nil {
|
|
if err := loader.allowsExternalRefs(ref); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
resolvedPath, err := loader.resolvePathWithRef(ref, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return resolvedPath, nil
|
|
}
|
|
|
|
func isSingleRefElement(ref string) bool {
|
|
return !strings.Contains(ref, "#")
|
|
}
|
|
|
|
func (loader *Loader) visitRef(ref string) {
|
|
if loader.visitedRefs == nil {
|
|
loader.visitedRefs = make(map[string]struct{})
|
|
loader.backtrack = make(map[string][]func(value any))
|
|
}
|
|
loader.visitedPath = append(loader.visitedPath, ref)
|
|
loader.visitedRefs[ref] = struct{}{}
|
|
}
|
|
|
|
func (loader *Loader) unvisitRef(ref string, value any) {
|
|
if value != nil {
|
|
for _, fn := range loader.backtrack[ref] {
|
|
fn(value)
|
|
}
|
|
}
|
|
delete(loader.visitedRefs, ref)
|
|
delete(loader.backtrack, ref)
|
|
loader.visitedPath = loader.visitedPath[:len(loader.visitedPath)-1]
|
|
}
|
|
|
|
func (loader *Loader) shouldVisitRef(ref string, fn func(value any)) bool {
|
|
if _, ok := loader.visitedRefs[ref]; ok {
|
|
loader.backtrack[ref] = append(loader.backtrack[ref], fn)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (loader *Loader) resolveComponent(doc *T, ref string, path *url.URL, resolved any) (
|
|
componentDoc *T,
|
|
componentPath *url.URL,
|
|
err error,
|
|
) {
|
|
if componentDoc, ref, componentPath, err = loader.resolveRefAndDocument(doc, ref, path); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
parsedURL, err := url.Parse(ref)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("cannot parse reference: %q: %v", ref, parsedURL)
|
|
}
|
|
fragment := parsedURL.Fragment
|
|
if fragment == "" {
|
|
fragment = "/"
|
|
}
|
|
if fragment[0] != '/' {
|
|
return nil, nil, fmt.Errorf("expected fragment prefix '#/' in URI %q", ref)
|
|
}
|
|
|
|
drill := func(cursor any) (any, error) {
|
|
for pathPart := range strings.SplitSeq(fragment[1:], "/") {
|
|
pathPart = unescapeRefString(pathPart)
|
|
attempted := false
|
|
|
|
switch c := cursor.(type) {
|
|
// Special case of T
|
|
// See issue856: a ref to doc => we assume that doc is a T => things live in T.Extensions
|
|
case *T:
|
|
if pathPart == "" {
|
|
cursor = c.Extensions
|
|
attempted = true
|
|
}
|
|
|
|
// Special case due to multijson
|
|
case *SchemaRef:
|
|
if pathPart == "additionalProperties" {
|
|
if ap := c.Value.AdditionalProperties.Has; ap != nil {
|
|
cursor = *ap
|
|
} else {
|
|
cursor = c.Value.AdditionalProperties.Schema
|
|
}
|
|
attempted = true
|
|
}
|
|
|
|
case *Responses:
|
|
cursor = c.m // m map[string]*ResponseRef
|
|
case *Callback:
|
|
cursor = c.m // m map[string]*PathItem
|
|
case *Paths:
|
|
cursor = c.m // m map[string]*PathItem
|
|
}
|
|
|
|
if !attempted {
|
|
if cursor, err = drillIntoField(cursor, pathPart); err != nil {
|
|
e := failedToResolveRefFragmentPart(ref, pathPart)
|
|
return nil, fmt.Errorf("%s: %w", e, err)
|
|
}
|
|
}
|
|
|
|
if cursor == nil {
|
|
return nil, failedToResolveRefFragmentPart(ref, pathPart)
|
|
}
|
|
}
|
|
return cursor, nil
|
|
}
|
|
var cursor any
|
|
if cursor, err = drill(componentDoc); err != nil {
|
|
if path == nil {
|
|
return nil, nil, err
|
|
}
|
|
var err2 error
|
|
data, err2 := loader.readURL(path)
|
|
if err2 != nil {
|
|
return nil, nil, err
|
|
}
|
|
if err2 = unmarshal(data, &cursor, loader.IncludeOrigin, path); err2 != nil {
|
|
return nil, nil, err
|
|
}
|
|
if cursor, err2 = drill(cursor); err2 != nil || cursor == nil {
|
|
return nil, nil, err
|
|
}
|
|
err = nil
|
|
}
|
|
|
|
setPathRef := func(target any) {
|
|
if i, ok := target.(interface {
|
|
setRefPath(*url.URL)
|
|
}); ok {
|
|
pathRef := copyURI(componentPath)
|
|
// Resolving internal refs of a doc loaded from memory
|
|
// has no path, so just set the Fragment.
|
|
if pathRef == nil {
|
|
pathRef = new(url.URL)
|
|
}
|
|
pathRef.Fragment = fragment
|
|
|
|
i.setRefPath(pathRef)
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case reflect.TypeOf(cursor) == reflect.TypeOf(resolved):
|
|
setPathRef(cursor)
|
|
|
|
reflect.ValueOf(resolved).Elem().Set(reflect.ValueOf(cursor).Elem())
|
|
return componentDoc, componentPath, nil
|
|
|
|
case reflect.TypeOf(cursor) == reflect.TypeFor[map[string]any]():
|
|
codec := func(got, expect any) error {
|
|
enc, err := json.Marshal(got)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = json.Unmarshal(enc, expect); err != nil {
|
|
return err
|
|
}
|
|
|
|
setPathRef(expect)
|
|
return nil
|
|
}
|
|
if err := codec(cursor, resolved); err != nil {
|
|
return nil, nil, fmt.Errorf("bad data in %q (expecting %s)", ref, readableType(resolved))
|
|
}
|
|
return componentDoc, componentPath, nil
|
|
|
|
default:
|
|
return nil, nil, fmt.Errorf("bad data in %q (expecting %s)", ref, readableType(resolved))
|
|
}
|
|
}
|
|
|
|
func readableType(x any) string {
|
|
switch x.(type) {
|
|
case *Callback:
|
|
return "callback object"
|
|
case *CallbackRef:
|
|
return "ref to callback object"
|
|
case *ExampleRef:
|
|
return "ref to example object"
|
|
case *HeaderRef:
|
|
return "ref to header object"
|
|
case *LinkRef:
|
|
return "ref to link object"
|
|
case *ParameterRef:
|
|
return "ref to parameter object"
|
|
case *PathItem:
|
|
return "pathItem object"
|
|
case *RequestBodyRef:
|
|
return "ref to requestBody object"
|
|
case *ResponseRef:
|
|
return "ref to response object"
|
|
case *SchemaRef:
|
|
return "ref to schema object"
|
|
case *SecuritySchemeRef:
|
|
return "ref to securityScheme object"
|
|
default:
|
|
panic(fmt.Sprintf("unreachable %T", x))
|
|
}
|
|
}
|
|
|
|
func drillIntoField(cursor any, fieldName string) (any, error) {
|
|
switch val := reflect.Indirect(reflect.ValueOf(cursor)); val.Kind() {
|
|
|
|
case reflect.Map:
|
|
elementValue := val.MapIndex(reflect.ValueOf(fieldName))
|
|
if !elementValue.IsValid() {
|
|
return nil, fmt.Errorf("map key %q not found", fieldName)
|
|
}
|
|
return elementValue.Interface(), nil
|
|
|
|
case reflect.Slice:
|
|
i, err := strconv.ParseUint(fieldName, 10, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
index := int(i)
|
|
if 0 > index || index >= val.Len() {
|
|
return nil, errors.New("slice index out of bounds")
|
|
}
|
|
return val.Index(index).Interface(), nil
|
|
|
|
case reflect.Struct:
|
|
hasFields := false
|
|
for i := range val.NumField() {
|
|
hasFields = true
|
|
if yamlTag := val.Type().Field(i).Tag.Get("yaml"); yamlTag != "-" {
|
|
if tagName, _, _ := strings.Cut(yamlTag, ","); tagName != "" {
|
|
if fieldName == tagName {
|
|
return val.Field(i).Interface(), nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// if cursor is a "ref wrapper" struct (e.g. RequestBodyRef),
|
|
if _, ok := val.Type().FieldByName("Value"); ok {
|
|
// try digging into its Value field
|
|
return drillIntoField(val.FieldByName("Value").Interface(), fieldName)
|
|
}
|
|
if hasFields {
|
|
if ff := val.Type().Field(0); ff.PkgPath == "" && ff.Name == "Extensions" {
|
|
extensions := val.Field(0).Interface().(map[string]any)
|
|
if enc, ok := extensions[fieldName]; ok {
|
|
return enc, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("struct field %q not found", fieldName)
|
|
|
|
default:
|
|
return nil, errors.New("not a map, slice nor struct")
|
|
}
|
|
}
|
|
|
|
func (loader *Loader) resolveRefAndDocument(doc *T, ref string, path *url.URL) (*T, string, *url.URL, error) {
|
|
if ref != "" && ref[0] == '#' {
|
|
return doc, ref, path, nil
|
|
}
|
|
|
|
fragment, resolvedPath, err := loader.resolveRef(ref, path)
|
|
if err != nil {
|
|
return nil, "", nil, err
|
|
}
|
|
|
|
if doc, err = loader.loadFromURIInternal(resolvedPath); err != nil {
|
|
return nil, "", nil, fmt.Errorf("error resolving reference %q: %w", ref, err)
|
|
}
|
|
|
|
return doc, fragment, resolvedPath, nil
|
|
}
|
|
|
|
func (loader *Loader) resolveRef(ref string, path *url.URL) (string, *url.URL, error) {
|
|
resolvedPathRef, err := loader.resolveRefPath(ref, path)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
fragment := "#" + resolvedPathRef.Fragment
|
|
resolvedPathRef.Fragment = ""
|
|
return fragment, resolvedPathRef, nil
|
|
}
|
|
|
|
var (
|
|
errMUSTCallback = errors.New("invalid callback: value MUST be an object")
|
|
errMUSTExample = errors.New("invalid example: value MUST be an object")
|
|
errMUSTHeader = errors.New("invalid header: value MUST be an object")
|
|
errMUSTLink = errors.New("invalid link: value MUST be an object")
|
|
errMUSTParameter = errors.New("invalid parameter: value MUST be an object")
|
|
errMUSTPathItem = errors.New("invalid path item: value MUST be an object")
|
|
errMUSTRequestBody = errors.New("invalid requestBody: value MUST be an object")
|
|
errMUSTResponse = errors.New("invalid response: value MUST be an object")
|
|
errMUSTSchema = errors.New("invalid schema: value MUST be an object")
|
|
errMUSTSecurityScheme = errors.New("invalid securityScheme: value MUST be an object")
|
|
)
|
|
|
|
func (loader *Loader) resolveHeaderRef(doc *T, component *HeaderRef, documentPath *url.URL) (err error) {
|
|
if component.isEmpty() {
|
|
return errMUSTHeader
|
|
}
|
|
|
|
if ref := component.Ref; ref != "" {
|
|
if component.Value != nil {
|
|
return nil
|
|
}
|
|
if !loader.shouldVisitRef(ref, func(value any) {
|
|
component.Value = value.(*Header)
|
|
refPath, _ := loader.resolveRefPath(ref, documentPath)
|
|
component.setRefPath(refPath)
|
|
}) {
|
|
return nil
|
|
}
|
|
loader.visitRef(ref)
|
|
if isSingleRefElement(ref) {
|
|
var header Header
|
|
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &header); err != nil {
|
|
return err
|
|
}
|
|
component.Value = &header
|
|
component.setRefPath(documentPath)
|
|
} else {
|
|
var resolved HeaderRef
|
|
doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := loader.resolveHeaderRef(doc, &resolved, componentPath); err != nil {
|
|
if err == errMUSTHeader {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
component.Value = resolved.Value
|
|
component.setRefPath(resolved.RefPath())
|
|
}
|
|
defer loader.unvisitRef(ref, component.Value)
|
|
}
|
|
value := component.Value
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
if schema := value.Schema; schema != nil {
|
|
if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, k := range componentNames(value.Examples) {
|
|
if err := loader.resolveExampleRef(doc, value.Examples[k], documentPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (loader *Loader) resolveParameterRef(doc *T, component *ParameterRef, documentPath *url.URL) (err error) {
|
|
if component.isEmpty() {
|
|
return errMUSTParameter
|
|
}
|
|
|
|
if ref := component.Ref; ref != "" {
|
|
if component.Value != nil {
|
|
return nil
|
|
}
|
|
if !loader.shouldVisitRef(ref, func(value any) {
|
|
component.Value = value.(*Parameter)
|
|
refPath, _ := loader.resolveRefPath(ref, documentPath)
|
|
component.setRefPath(refPath)
|
|
}) {
|
|
return nil
|
|
}
|
|
loader.visitRef(ref)
|
|
if isSingleRefElement(ref) {
|
|
var param Parameter
|
|
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, ¶m); err != nil {
|
|
return err
|
|
}
|
|
component.Value = ¶m
|
|
component.setRefPath(documentPath)
|
|
} else {
|
|
var resolved ParameterRef
|
|
doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := loader.resolveParameterRef(doc, &resolved, componentPath); err != nil {
|
|
if err == errMUSTParameter {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
component.Value = resolved.Value
|
|
component.setRefPath(resolved.RefPath())
|
|
}
|
|
defer loader.unvisitRef(ref, component.Value)
|
|
}
|
|
value := component.Value
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
if value.Content != nil && value.Schema != nil {
|
|
return errors.New("cannot contain both schema and content in a parameter")
|
|
}
|
|
for _, name := range componentNames(value.Content) {
|
|
contentType := value.Content[name]
|
|
if schema := contentType.Schema; schema != nil {
|
|
if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, k := range componentNames(contentType.Examples) {
|
|
if err := loader.resolveExampleRef(doc, contentType.Examples[k], documentPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if schema := value.Schema; schema != nil {
|
|
if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, k := range componentNames(value.Examples) {
|
|
if err := loader.resolveExampleRef(doc, value.Examples[k], documentPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (loader *Loader) resolveRequestBodyRef(doc *T, component *RequestBodyRef, documentPath *url.URL) (err error) {
|
|
if component.isEmpty() {
|
|
return errMUSTRequestBody
|
|
}
|
|
|
|
if ref := component.Ref; ref != "" {
|
|
if component.Value != nil {
|
|
return nil
|
|
}
|
|
if !loader.shouldVisitRef(ref, func(value any) {
|
|
component.Value = value.(*RequestBody)
|
|
refPath, _ := loader.resolveRefPath(ref, documentPath)
|
|
component.setRefPath(refPath)
|
|
}) {
|
|
return nil
|
|
}
|
|
loader.visitRef(ref)
|
|
if isSingleRefElement(ref) {
|
|
var requestBody RequestBody
|
|
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &requestBody); err != nil {
|
|
return err
|
|
}
|
|
component.Value = &requestBody
|
|
component.setRefPath(documentPath)
|
|
} else {
|
|
var resolved RequestBodyRef
|
|
doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = loader.resolveRequestBodyRef(doc, &resolved, componentPath); err != nil {
|
|
if err == errMUSTRequestBody {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
component.Value = resolved.Value
|
|
component.setRefPath(resolved.RefPath())
|
|
}
|
|
defer loader.unvisitRef(ref, component.Value)
|
|
}
|
|
value := component.Value
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, name := range componentNames(value.Content) {
|
|
contentType := value.Content[name]
|
|
if contentType == nil {
|
|
continue
|
|
}
|
|
for _, name := range componentNames(contentType.Examples) {
|
|
example := contentType.Examples[name]
|
|
if err := loader.resolveExampleRef(doc, example, documentPath); err != nil {
|
|
return err
|
|
}
|
|
contentType.Examples[name] = example
|
|
}
|
|
if schema := contentType.Schema; schema != nil {
|
|
if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (loader *Loader) resolveResponseRef(doc *T, component *ResponseRef, documentPath *url.URL) (err error) {
|
|
if component.isEmpty() {
|
|
return errMUSTResponse
|
|
}
|
|
|
|
if ref := component.Ref; ref != "" {
|
|
if component.Value != nil {
|
|
return nil
|
|
}
|
|
if !loader.shouldVisitRef(ref, func(value any) {
|
|
component.Value = value.(*Response)
|
|
refPath, _ := loader.resolveRefPath(ref, documentPath)
|
|
component.setRefPath(refPath)
|
|
}) {
|
|
return nil
|
|
}
|
|
loader.visitRef(ref)
|
|
if isSingleRefElement(ref) {
|
|
var resp Response
|
|
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resp); err != nil {
|
|
return err
|
|
}
|
|
component.Value = &resp
|
|
component.setRefPath(documentPath)
|
|
} else {
|
|
var resolved ResponseRef
|
|
doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := loader.resolveResponseRef(doc, &resolved, componentPath); err != nil {
|
|
if err == errMUSTResponse {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
component.Value = resolved.Value
|
|
component.setRefPath(resolved.RefPath())
|
|
}
|
|
defer loader.unvisitRef(ref, component.Value)
|
|
}
|
|
value := component.Value
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, name := range componentNames(value.Headers) {
|
|
header := value.Headers[name]
|
|
if err := loader.resolveHeaderRef(doc, header, documentPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, name := range componentNames(value.Content) {
|
|
contentType := value.Content[name]
|
|
if contentType == nil {
|
|
continue
|
|
}
|
|
for _, name := range componentNames(contentType.Examples) {
|
|
example := contentType.Examples[name]
|
|
if err := loader.resolveExampleRef(doc, example, documentPath); err != nil {
|
|
return err
|
|
}
|
|
contentType.Examples[name] = example
|
|
}
|
|
if schema := contentType.Schema; schema != nil {
|
|
if err := loader.resolveSchemaRef(doc, schema, documentPath, []string{}); err != nil {
|
|
return err
|
|
}
|
|
contentType.Schema = schema
|
|
}
|
|
}
|
|
for _, name := range componentNames(value.Links) {
|
|
link := value.Links[name]
|
|
if err := loader.resolveLinkRef(doc, link, documentPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPath *url.URL, visited []string) (err error) {
|
|
if component.isEmpty() {
|
|
return errMUSTSchema
|
|
}
|
|
|
|
if ref := component.Ref; ref != "" {
|
|
if component.Value != nil {
|
|
return nil
|
|
}
|
|
if !loader.shouldVisitRef(ref, func(value any) {
|
|
component.Value = value.(*Schema)
|
|
refPath, _ := loader.resolveRefPath(ref, documentPath)
|
|
component.setRefPath(refPath)
|
|
}) {
|
|
return nil
|
|
}
|
|
loader.visitRef(ref)
|
|
if isSingleRefElement(ref) {
|
|
var schema Schema
|
|
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &schema); err != nil {
|
|
return err
|
|
}
|
|
component.Value = &schema
|
|
component.setRefPath(documentPath)
|
|
} else {
|
|
var resolved SchemaRef
|
|
doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := loader.resolveSchemaRef(doc, &resolved, componentPath, visited); err != nil {
|
|
if err == errMUSTSchema {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
component.Value = resolved.Value
|
|
component.setRefPath(resolved.RefPath())
|
|
}
|
|
defer loader.unvisitRef(ref, component.Value)
|
|
|
|
// OAS 3.1 / JSON Schema 2020-12: apply sibling keywords from the original schema
|
|
// object on top of the resolved $ref value. In 3.1, siblings are not ignored —
|
|
// they augment the referenced schema (e.g. deprecated:true alongside $ref).
|
|
// Only apply for OAS 3.1+ — in 3.0 $ref replaces its entire object and siblings
|
|
// are (validly) ignored.
|
|
if doc.IsOpenAPI31OrLater() && component.sibling != nil && component.Value != nil {
|
|
// Work on a copy so we don't mutate a schema shared by other references.
|
|
schemaCopy := *component.Value
|
|
applySiblingSchemaFields(&schemaCopy, component.sibling, component.extra)
|
|
component.Value = &schemaCopy
|
|
}
|
|
}
|
|
value := component.Value
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
// ResolveRefs referred schemas
|
|
if v := value.Items; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, name := range componentNames(value.Properties) {
|
|
v := value.Properties[name]
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := value.AdditionalProperties.Schema; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := value.Not; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, v := range value.AllOf {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, v := range value.AnyOf {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, v := range value.OneOf {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Discriminator mapping refs are a special case since they are not full
|
|
// ref objects but are plain strings that reference schema objects.
|
|
// Only resolve refs that look like external references (contain a path).
|
|
// Plain schema names like "Dog" or internal refs like "#/components/schemas/Dog"
|
|
// don't need to be resolved by the loader.
|
|
if value.Discriminator != nil {
|
|
for _, k := range componentNames(value.Discriminator.Mapping) {
|
|
v := value.Discriminator.Mapping[k]
|
|
// Only resolve if it looks like an external ref (contains path separator)
|
|
if strings.Contains(v.Ref, "/") && !strings.HasPrefix(v.Ref, "#") {
|
|
if err := loader.resolveSchemaRef(doc, (*SchemaRef)(&v), documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
value.Discriminator.Mapping[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
// OpenAPI 3.1 / JSON Schema 2020-12 fields
|
|
for _, v := range value.PrefixItems {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := value.Contains; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, name := range componentNames(value.PatternProperties) {
|
|
v := value.PatternProperties[name]
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, name := range componentNames(value.DependentSchemas) {
|
|
v := value.DependentSchemas[name]
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, name := range componentNames(value.Defs) {
|
|
v := value.Defs[name]
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := value.PropertyNames; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := value.UnevaluatedItems.Schema; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := value.UnevaluatedProperties.Schema; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := value.If; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := value.Then; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := value.Else; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if v := value.ContentSchema; v != nil {
|
|
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (loader *Loader) resolveSecuritySchemeRef(doc *T, component *SecuritySchemeRef, documentPath *url.URL) (err error) {
|
|
if component.isEmpty() {
|
|
return errMUSTSecurityScheme
|
|
}
|
|
|
|
if ref := component.Ref; ref != "" {
|
|
if component.Value != nil {
|
|
return nil
|
|
}
|
|
if !loader.shouldVisitRef(ref, func(value any) {
|
|
component.Value = value.(*SecurityScheme)
|
|
refPath, _ := loader.resolveRefPath(ref, documentPath)
|
|
component.setRefPath(refPath)
|
|
}) {
|
|
return nil
|
|
}
|
|
loader.visitRef(ref)
|
|
if isSingleRefElement(ref) {
|
|
var scheme SecurityScheme
|
|
if _, err = loader.loadSingleElementFromURI(ref, documentPath, &scheme); err != nil {
|
|
return err
|
|
}
|
|
component.Value = &scheme
|
|
component.setRefPath(documentPath)
|
|
} else {
|
|
var resolved SecuritySchemeRef
|
|
doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := loader.resolveSecuritySchemeRef(doc, &resolved, componentPath); err != nil {
|
|
if err == errMUSTSecurityScheme {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
component.Value = resolved.Value
|
|
component.setRefPath(resolved.RefPath())
|
|
}
|
|
defer loader.unvisitRef(ref, component.Value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (loader *Loader) resolveExampleRef(doc *T, component *ExampleRef, documentPath *url.URL) (err error) {
|
|
if ref := component.Ref; ref != "" {
|
|
if component.Value != nil {
|
|
return nil
|
|
}
|
|
if !loader.shouldVisitRef(ref, func(value any) {
|
|
component.Value = value.(*Example)
|
|
refPath, _ := loader.resolveRefPath(ref, documentPath)
|
|
component.setRefPath(refPath)
|
|
}) {
|
|
return nil
|
|
}
|
|
loader.visitRef(ref)
|
|
if isSingleRefElement(ref) {
|
|
var example Example
|
|
if _, err = loader.loadSingleElementFromURI(ref, documentPath, &example); err != nil {
|
|
return err
|
|
}
|
|
component.Value = &example
|
|
component.setRefPath(documentPath)
|
|
} else {
|
|
var resolved ExampleRef
|
|
doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := loader.resolveExampleRef(doc, &resolved, componentPath); err != nil {
|
|
if err == errMUSTExample {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
component.Value = resolved.Value
|
|
component.setRefPath(resolved.RefPath())
|
|
}
|
|
defer loader.unvisitRef(ref, component.Value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (loader *Loader) resolveCallbackRef(doc *T, component *CallbackRef, documentPath *url.URL) (err error) {
|
|
if component.isEmpty() {
|
|
return errMUSTCallback
|
|
}
|
|
|
|
if ref := component.Ref; ref != "" {
|
|
if component.Value != nil {
|
|
return nil
|
|
}
|
|
if !loader.shouldVisitRef(ref, func(value any) {
|
|
component.Value = value.(*Callback)
|
|
refPath, _ := loader.resolveRefPath(ref, documentPath)
|
|
component.setRefPath(refPath)
|
|
}) {
|
|
return nil
|
|
}
|
|
loader.visitRef(ref)
|
|
if isSingleRefElement(ref) {
|
|
var resolved Callback
|
|
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &resolved); err != nil {
|
|
return err
|
|
}
|
|
component.Value = &resolved
|
|
component.setRefPath(documentPath)
|
|
} else {
|
|
var resolved CallbackRef
|
|
doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = loader.resolveCallbackRef(doc, &resolved, componentPath); err != nil {
|
|
if err == errMUSTCallback {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
component.Value = resolved.Value
|
|
component.setRefPath(resolved.RefPath())
|
|
}
|
|
defer loader.unvisitRef(ref, component.Value)
|
|
}
|
|
value := component.Value
|
|
if value == nil {
|
|
return nil
|
|
}
|
|
|
|
pathItems := value.Map()
|
|
for _, name := range componentNames(pathItems) {
|
|
pathItem := pathItems[name]
|
|
if err = loader.resolvePathItemRef(doc, pathItem, documentPath); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (loader *Loader) resolveLinkRef(doc *T, component *LinkRef, documentPath *url.URL) (err error) {
|
|
if component.isEmpty() {
|
|
return errMUSTLink
|
|
}
|
|
|
|
if ref := component.Ref; ref != "" {
|
|
if component.Value != nil {
|
|
return nil
|
|
}
|
|
if !loader.shouldVisitRef(ref, func(value any) {
|
|
component.Value = value.(*Link)
|
|
refPath, _ := loader.resolveRefPath(ref, documentPath)
|
|
component.setRefPath(refPath)
|
|
}) {
|
|
return nil
|
|
}
|
|
loader.visitRef(ref)
|
|
if isSingleRefElement(ref) {
|
|
var link Link
|
|
if _, err = loader.loadSingleElementFromURI(ref, documentPath, &link); err != nil {
|
|
return err
|
|
}
|
|
component.Value = &link
|
|
component.setRefPath(documentPath)
|
|
} else {
|
|
var resolved LinkRef
|
|
doc, componentPath, err := loader.resolveComponent(doc, ref, documentPath, &resolved)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := loader.resolveLinkRef(doc, &resolved, componentPath); err != nil {
|
|
if err == errMUSTLink {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
component.Value = resolved.Value
|
|
component.setRefPath(resolved.RefPath())
|
|
}
|
|
defer loader.unvisitRef(ref, component.Value)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (loader *Loader) resolvePathItemRef(doc *T, pathItem *PathItem, documentPath *url.URL) (err error) {
|
|
if pathItem == nil {
|
|
err = errMUSTPathItem
|
|
return
|
|
}
|
|
|
|
if ref := pathItem.Ref; ref != "" {
|
|
if !pathItem.isEmpty() {
|
|
return
|
|
}
|
|
if !loader.shouldVisitRef(ref, func(value any) {
|
|
*pathItem = *value.(*PathItem)
|
|
}) {
|
|
return nil
|
|
}
|
|
loader.visitRef(ref)
|
|
if isSingleRefElement(ref) {
|
|
var p PathItem
|
|
if documentPath, err = loader.loadSingleElementFromURI(ref, documentPath, &p); err != nil {
|
|
return
|
|
}
|
|
*pathItem = p
|
|
} else {
|
|
var resolved PathItem
|
|
if doc, documentPath, err = loader.resolveComponent(doc, ref, documentPath, &resolved); err != nil {
|
|
if err == errMUSTPathItem {
|
|
return nil
|
|
}
|
|
return
|
|
}
|
|
*pathItem = resolved
|
|
}
|
|
pathItem.Ref = ref
|
|
defer loader.unvisitRef(ref, pathItem)
|
|
}
|
|
|
|
for _, parameter := range pathItem.Parameters {
|
|
if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil {
|
|
return
|
|
}
|
|
}
|
|
operations := pathItem.Operations()
|
|
for _, name := range componentNames(operations) {
|
|
operation := operations[name]
|
|
for _, parameter := range operation.Parameters {
|
|
if err = loader.resolveParameterRef(doc, parameter, documentPath); err != nil {
|
|
return
|
|
}
|
|
}
|
|
if requestBody := operation.RequestBody; requestBody != nil {
|
|
if err = loader.resolveRequestBodyRef(doc, requestBody, documentPath); err != nil {
|
|
return
|
|
}
|
|
}
|
|
responses := operation.Responses.Map()
|
|
for _, name := range componentNames(responses) {
|
|
response := responses[name]
|
|
if err = loader.resolveResponseRef(doc, response, documentPath); err != nil {
|
|
return
|
|
}
|
|
}
|
|
for _, name := range componentNames(operation.Callbacks) {
|
|
callback := operation.Callbacks[name]
|
|
if err = loader.resolveCallbackRef(doc, callback, documentPath); err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func unescapeRefString(ref string) string {
|
|
return strings.ReplaceAll(strings.ReplaceAll(ref, "~1", "/"), "~0", "~")
|
|
}
|
|
|
|
// applySiblingSchemaFields overlays the fields listed in presentFields from sibling onto dst.
|
|
// It is used to honour keyword siblings of $ref in OpenAPI 3.1 / JSON Schema 2020-12, where
|
|
// sibling keywords are applied in addition to (not instead of) the referenced schema.
|
|
// Only fields that were explicitly present in the original YAML/JSON are applied; the presentFields
|
|
// slice (derived from SchemaRef.extra) carries this information.
|
|
func applySiblingSchemaFields(dst, sibling *Schema, presentFields []string) {
|
|
for _, field := range presentFields {
|
|
switch field {
|
|
case "oneOf":
|
|
dst.OneOf = sibling.OneOf
|
|
case "anyOf":
|
|
dst.AnyOf = sibling.AnyOf
|
|
case "allOf":
|
|
dst.AllOf = sibling.AllOf
|
|
case "not":
|
|
dst.Not = sibling.Not
|
|
case "type":
|
|
dst.Type = sibling.Type
|
|
case "title":
|
|
dst.Title = sibling.Title
|
|
case "format":
|
|
dst.Format = sibling.Format
|
|
case "description":
|
|
dst.Description = sibling.Description
|
|
case "enum":
|
|
dst.Enum = sibling.Enum
|
|
case "default":
|
|
dst.Default = sibling.Default
|
|
case "example":
|
|
dst.Example = sibling.Example
|
|
case "externalDocs":
|
|
dst.ExternalDocs = sibling.ExternalDocs
|
|
case "uniqueItems":
|
|
dst.UniqueItems = sibling.UniqueItems
|
|
case "exclusiveMinimum":
|
|
dst.ExclusiveMin = sibling.ExclusiveMin
|
|
case "exclusiveMaximum":
|
|
dst.ExclusiveMax = sibling.ExclusiveMax
|
|
case "nullable":
|
|
dst.Nullable = sibling.Nullable
|
|
case "readOnly":
|
|
dst.ReadOnly = sibling.ReadOnly
|
|
case "writeOnly":
|
|
dst.WriteOnly = sibling.WriteOnly
|
|
case "allowEmptyValue":
|
|
dst.AllowEmptyValue = sibling.AllowEmptyValue
|
|
case "deprecated":
|
|
dst.Deprecated = sibling.Deprecated
|
|
case "xml":
|
|
dst.XML = sibling.XML
|
|
case "minimum":
|
|
dst.Min = sibling.Min
|
|
case "maximum":
|
|
dst.Max = sibling.Max
|
|
case "multipleOf":
|
|
dst.MultipleOf = sibling.MultipleOf
|
|
case "minLength":
|
|
dst.MinLength = sibling.MinLength
|
|
case "maxLength":
|
|
dst.MaxLength = sibling.MaxLength
|
|
case "pattern":
|
|
dst.Pattern = sibling.Pattern
|
|
case "minItems":
|
|
dst.MinItems = sibling.MinItems
|
|
case "maxItems":
|
|
dst.MaxItems = sibling.MaxItems
|
|
case "items":
|
|
dst.Items = sibling.Items
|
|
case "required":
|
|
dst.Required = sibling.Required
|
|
case "properties":
|
|
dst.Properties = sibling.Properties
|
|
case "minProperties":
|
|
dst.MinProps = sibling.MinProps
|
|
case "maxProperties":
|
|
dst.MaxProps = sibling.MaxProps
|
|
case "additionalProperties":
|
|
dst.AdditionalProperties = sibling.AdditionalProperties
|
|
case "discriminator":
|
|
dst.Discriminator = sibling.Discriminator
|
|
case "const":
|
|
dst.Const = sibling.Const
|
|
case "examples":
|
|
dst.Examples = sibling.Examples
|
|
case "prefixItems":
|
|
dst.PrefixItems = sibling.PrefixItems
|
|
case "contains":
|
|
dst.Contains = sibling.Contains
|
|
case "minContains":
|
|
dst.MinContains = sibling.MinContains
|
|
case "maxContains":
|
|
dst.MaxContains = sibling.MaxContains
|
|
case "patternProperties":
|
|
dst.PatternProperties = sibling.PatternProperties
|
|
case "dependentSchemas":
|
|
dst.DependentSchemas = sibling.DependentSchemas
|
|
case "propertyNames":
|
|
dst.PropertyNames = sibling.PropertyNames
|
|
case "unevaluatedItems":
|
|
dst.UnevaluatedItems = sibling.UnevaluatedItems
|
|
case "unevaluatedProperties":
|
|
dst.UnevaluatedProperties = sibling.UnevaluatedProperties
|
|
case "if":
|
|
dst.If = sibling.If
|
|
case "then":
|
|
dst.Then = sibling.Then
|
|
case "else":
|
|
dst.Else = sibling.Else
|
|
case "dependentRequired":
|
|
dst.DependentRequired = sibling.DependentRequired
|
|
case "$defs":
|
|
dst.Defs = sibling.Defs
|
|
case "$schema":
|
|
dst.SchemaDialect = sibling.SchemaDialect
|
|
case "$comment":
|
|
dst.Comment = sibling.Comment
|
|
case "$id":
|
|
dst.SchemaID = sibling.SchemaID
|
|
case "$anchor":
|
|
dst.Anchor = sibling.Anchor
|
|
case "$dynamicRef":
|
|
dst.DynamicRef = sibling.DynamicRef
|
|
case "$dynamicAnchor":
|
|
dst.DynamicAnchor = sibling.DynamicAnchor
|
|
case "contentMediaType":
|
|
dst.ContentMediaType = sibling.ContentMediaType
|
|
case "contentEncoding":
|
|
dst.ContentEncoding = sibling.ContentEncoding
|
|
case "contentSchema":
|
|
dst.ContentSchema = sibling.ContentSchema
|
|
}
|
|
}
|
|
}
|