...

Source file src/github.com/go-openapi/spec/schema_loader.go

Documentation: github.com/go-openapi/spec

     1  // Copyright 2015 go-swagger maintainers
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package spec
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"log"
    21  	"net/url"
    22  	"reflect"
    23  	"strings"
    24  
    25  	"github.com/go-openapi/swag"
    26  )
    27  
    28  // PathLoader is a function to use when loading remote refs.
    29  //
    30  // This is a package level default. It may be overridden or bypassed by
    31  // specifying the loader in ExpandOptions.
    32  //
    33  // NOTE: if you are using the go-openapi/loads package, it will override
    34  // this value with its own default (a loader to retrieve YAML documents as
    35  // well as JSON ones).
    36  var PathLoader = func(pth string) (json.RawMessage, error) {
    37  	data, err := swag.LoadFromFileOrHTTP(pth)
    38  	if err != nil {
    39  		return nil, err
    40  	}
    41  	return json.RawMessage(data), nil
    42  }
    43  
    44  // resolverContext allows to share a context during spec processing.
    45  // At the moment, it just holds the index of circular references found.
    46  type resolverContext struct {
    47  	// circulars holds all visited circular references, to shortcircuit $ref resolution.
    48  	//
    49  	// This structure is privately instantiated and needs not be locked against
    50  	// concurrent access, unless we chose to implement a parallel spec walking.
    51  	circulars map[string]bool
    52  	basePath  string
    53  	loadDoc   func(string) (json.RawMessage, error)
    54  	rootID    string
    55  }
    56  
    57  func newResolverContext(options *ExpandOptions) *resolverContext {
    58  	expandOptions := optionsOrDefault(options)
    59  
    60  	// path loader may be overridden by options
    61  	var loader func(string) (json.RawMessage, error)
    62  	if expandOptions.PathLoader == nil {
    63  		loader = PathLoader
    64  	} else {
    65  		loader = expandOptions.PathLoader
    66  	}
    67  
    68  	return &resolverContext{
    69  		circulars: make(map[string]bool),
    70  		basePath:  expandOptions.RelativeBase, // keep the root base path in context
    71  		loadDoc:   loader,
    72  	}
    73  }
    74  
    75  type schemaLoader struct {
    76  	root    interface{}
    77  	options *ExpandOptions
    78  	cache   ResolutionCache
    79  	context *resolverContext
    80  }
    81  
    82  func (r *schemaLoader) transitiveResolver(basePath string, ref Ref) *schemaLoader {
    83  	if ref.IsRoot() || ref.HasFragmentOnly {
    84  		return r
    85  	}
    86  
    87  	baseRef := MustCreateRef(basePath)
    88  	currentRef := normalizeRef(&ref, basePath)
    89  	if strings.HasPrefix(currentRef.String(), baseRef.String()) {
    90  		return r
    91  	}
    92  
    93  	// set a new root against which to resolve
    94  	rootURL := currentRef.GetURL()
    95  	rootURL.Fragment = ""
    96  	root, _ := r.cache.Get(rootURL.String())
    97  
    98  	// shallow copy of resolver options to set a new RelativeBase when
    99  	// traversing multiple documents
   100  	newOptions := r.options
   101  	newOptions.RelativeBase = rootURL.String()
   102  
   103  	return defaultSchemaLoader(root, newOptions, r.cache, r.context)
   104  }
   105  
   106  func (r *schemaLoader) updateBasePath(transitive *schemaLoader, basePath string) string {
   107  	if transitive != r {
   108  		if transitive.options != nil && transitive.options.RelativeBase != "" {
   109  			return normalizeBase(transitive.options.RelativeBase)
   110  		}
   111  	}
   112  
   113  	return basePath
   114  }
   115  
   116  func (r *schemaLoader) resolveRef(ref *Ref, target interface{}, basePath string) error {
   117  	tgt := reflect.ValueOf(target)
   118  	if tgt.Kind() != reflect.Ptr {
   119  		return ErrResolveRefNeedsAPointer
   120  	}
   121  
   122  	if ref.GetURL() == nil {
   123  		return nil
   124  	}
   125  
   126  	var (
   127  		res  interface{}
   128  		data interface{}
   129  		err  error
   130  	)
   131  
   132  	// Resolve against the root if it isn't nil, and if ref is pointing at the root, or has a fragment only which means
   133  	// it is pointing somewhere in the root.
   134  	root := r.root
   135  	if (ref.IsRoot() || ref.HasFragmentOnly) && root == nil && basePath != "" {
   136  		if baseRef, erb := NewRef(basePath); erb == nil {
   137  			root, _, _, _ = r.load(baseRef.GetURL())
   138  		}
   139  	}
   140  
   141  	if (ref.IsRoot() || ref.HasFragmentOnly) && root != nil {
   142  		data = root
   143  	} else {
   144  		baseRef := normalizeRef(ref, basePath)
   145  		data, _, _, err = r.load(baseRef.GetURL())
   146  		if err != nil {
   147  			return err
   148  		}
   149  	}
   150  
   151  	res = data
   152  	if ref.String() != "" {
   153  		res, _, err = ref.GetPointer().Get(data)
   154  		if err != nil {
   155  			return err
   156  		}
   157  	}
   158  	return swag.DynamicJSONToStruct(res, target)
   159  }
   160  
   161  func (r *schemaLoader) load(refURL *url.URL) (interface{}, url.URL, bool, error) {
   162  	debugLog("loading schema from url: %s", refURL)
   163  	toFetch := *refURL
   164  	toFetch.Fragment = ""
   165  
   166  	var err error
   167  	pth := toFetch.String()
   168  	normalized := normalizeBase(pth)
   169  	debugLog("loading doc from: %s", normalized)
   170  
   171  	data, fromCache := r.cache.Get(normalized)
   172  	if fromCache {
   173  		return data, toFetch, fromCache, nil
   174  	}
   175  
   176  	b, err := r.context.loadDoc(normalized)
   177  	if err != nil {
   178  		return nil, url.URL{}, false, err
   179  	}
   180  
   181  	var doc interface{}
   182  	if err := json.Unmarshal(b, &doc); err != nil {
   183  		return nil, url.URL{}, false, err
   184  	}
   185  	r.cache.Set(normalized, doc)
   186  
   187  	return doc, toFetch, fromCache, nil
   188  }
   189  
   190  // isCircular detects cycles in sequences of $ref.
   191  //
   192  // It relies on a private context (which needs not be locked).
   193  func (r *schemaLoader) isCircular(ref *Ref, basePath string, parentRefs ...string) (foundCycle bool) {
   194  	normalizedRef := normalizeURI(ref.String(), basePath)
   195  	if _, ok := r.context.circulars[normalizedRef]; ok {
   196  		// circular $ref has been already detected in another explored cycle
   197  		foundCycle = true
   198  		return
   199  	}
   200  	foundCycle = swag.ContainsStrings(parentRefs, normalizedRef) // normalized windows url's are lower cased
   201  	if foundCycle {
   202  		r.context.circulars[normalizedRef] = true
   203  	}
   204  	return
   205  }
   206  
   207  // Resolve resolves a reference against basePath and stores the result in target.
   208  //
   209  // Resolve is not in charge of following references: it only resolves ref by following its URL.
   210  //
   211  // If the schema the ref is referring to holds nested refs, Resolve doesn't resolve them.
   212  //
   213  // If basePath is an empty string, ref is resolved against the root schema stored in the schemaLoader struct
   214  func (r *schemaLoader) Resolve(ref *Ref, target interface{}, basePath string) error {
   215  	return r.resolveRef(ref, target, basePath)
   216  }
   217  
   218  func (r *schemaLoader) deref(input interface{}, parentRefs []string, basePath string) error {
   219  	var ref *Ref
   220  	switch refable := input.(type) {
   221  	case *Schema:
   222  		ref = &refable.Ref
   223  	case *Parameter:
   224  		ref = &refable.Ref
   225  	case *Response:
   226  		ref = &refable.Ref
   227  	case *PathItem:
   228  		ref = &refable.Ref
   229  	default:
   230  		return fmt.Errorf("unsupported type: %T: %w", input, ErrDerefUnsupportedType)
   231  	}
   232  
   233  	curRef := ref.String()
   234  	if curRef == "" {
   235  		return nil
   236  	}
   237  
   238  	normalizedRef := normalizeRef(ref, basePath)
   239  	normalizedBasePath := normalizedRef.RemoteURI()
   240  
   241  	if r.isCircular(normalizedRef, basePath, parentRefs...) {
   242  		return nil
   243  	}
   244  
   245  	if err := r.resolveRef(ref, input, basePath); r.shouldStopOnError(err) {
   246  		return err
   247  	}
   248  
   249  	if ref.String() == "" || ref.String() == curRef {
   250  		// done with rereferencing
   251  		return nil
   252  	}
   253  
   254  	parentRefs = append(parentRefs, normalizedRef.String())
   255  	return r.deref(input, parentRefs, normalizedBasePath)
   256  }
   257  
   258  func (r *schemaLoader) shouldStopOnError(err error) bool {
   259  	if err != nil && !r.options.ContinueOnError {
   260  		return true
   261  	}
   262  
   263  	if err != nil {
   264  		log.Println(err)
   265  	}
   266  
   267  	return false
   268  }
   269  
   270  func (r *schemaLoader) setSchemaID(target interface{}, id, basePath string) (string, string) {
   271  	debugLog("schema has ID: %s", id)
   272  
   273  	// handling the case when id is a folder
   274  	// remember that basePath has to point to a file
   275  	var refPath string
   276  	if strings.HasSuffix(id, "/") {
   277  		// ensure this is detected as a file, not a folder
   278  		refPath = fmt.Sprintf("%s%s", id, "placeholder.json")
   279  	} else {
   280  		refPath = id
   281  	}
   282  
   283  	// updates the current base path
   284  	// * important: ID can be a relative path
   285  	// * registers target to be fetchable from the new base proposed by this id
   286  	newBasePath := normalizeURI(refPath, basePath)
   287  
   288  	// store found IDs for possible future reuse in $ref
   289  	r.cache.Set(newBasePath, target)
   290  
   291  	// the root document has an ID: all $ref relative to that ID may
   292  	// be rebased relative to the root document
   293  	if basePath == r.context.basePath {
   294  		debugLog("root document is a schema with ID: %s (normalized as:%s)", id, newBasePath)
   295  		r.context.rootID = newBasePath
   296  	}
   297  
   298  	return newBasePath, refPath
   299  }
   300  
   301  func defaultSchemaLoader(
   302  	root interface{},
   303  	expandOptions *ExpandOptions,
   304  	cache ResolutionCache,
   305  	context *resolverContext) *schemaLoader {
   306  
   307  	if expandOptions == nil {
   308  		expandOptions = &ExpandOptions{}
   309  	}
   310  
   311  	cache = cacheOrDefault(cache)
   312  
   313  	if expandOptions.RelativeBase == "" {
   314  		// if no relative base is provided, assume the root document
   315  		// contains all $ref, or at least, that the relative documents
   316  		// may be resolved from the current working directory.
   317  		expandOptions.RelativeBase = baseForRoot(root, cache)
   318  	}
   319  	debugLog("effective expander options: %#v", expandOptions)
   320  
   321  	if context == nil {
   322  		context = newResolverContext(expandOptions)
   323  	}
   324  
   325  	return &schemaLoader{
   326  		root:    root,
   327  		options: expandOptions,
   328  		cache:   cache,
   329  		context: context,
   330  	}
   331  }
   332  

View as plain text