...

Source file src/github.com/go-openapi/spec/expander.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  )
    21  
    22  // ExpandOptions provides options for the spec expander.
    23  //
    24  // RelativeBase is the path to the root document. This can be a remote URL or a path to a local file.
    25  //
    26  // If left empty, the root document is assumed to be located in the current working directory:
    27  // all relative $ref's will be resolved from there.
    28  //
    29  // PathLoader injects a document loading method. By default, this resolves to the function provided by the SpecLoader package variable.
    30  type ExpandOptions struct {
    31  	RelativeBase        string                                // the path to the root document to expand. This is a file, not a directory
    32  	SkipSchemas         bool                                  // do not expand schemas, just paths, parameters and responses
    33  	ContinueOnError     bool                                  // continue expanding even after and error is found
    34  	PathLoader          func(string) (json.RawMessage, error) `json:"-"` // the document loading method that takes a path as input and yields a json document
    35  	AbsoluteCircularRef bool                                  // circular $ref remaining after expansion remain absolute URLs
    36  }
    37  
    38  func optionsOrDefault(opts *ExpandOptions) *ExpandOptions {
    39  	if opts != nil {
    40  		clone := *opts // shallow clone to avoid internal changes to be propagated to the caller
    41  		if clone.RelativeBase != "" {
    42  			clone.RelativeBase = normalizeBase(clone.RelativeBase)
    43  		}
    44  		// if the relative base is empty, let the schema loader choose a pseudo root document
    45  		return &clone
    46  	}
    47  	return &ExpandOptions{}
    48  }
    49  
    50  // ExpandSpec expands the references in a swagger spec
    51  func ExpandSpec(spec *Swagger, options *ExpandOptions) error {
    52  	options = optionsOrDefault(options)
    53  	resolver := defaultSchemaLoader(spec, options, nil, nil)
    54  
    55  	specBasePath := options.RelativeBase
    56  
    57  	if !options.SkipSchemas {
    58  		for key, definition := range spec.Definitions {
    59  			parentRefs := make([]string, 0, 10)
    60  			parentRefs = append(parentRefs, "#/definitions/"+key)
    61  
    62  			def, err := expandSchema(definition, parentRefs, resolver, specBasePath)
    63  			if resolver.shouldStopOnError(err) {
    64  				return err
    65  			}
    66  			if def != nil {
    67  				spec.Definitions[key] = *def
    68  			}
    69  		}
    70  	}
    71  
    72  	for key := range spec.Parameters {
    73  		parameter := spec.Parameters[key]
    74  		if err := expandParameterOrResponse(&parameter, resolver, specBasePath); resolver.shouldStopOnError(err) {
    75  			return err
    76  		}
    77  		spec.Parameters[key] = parameter
    78  	}
    79  
    80  	for key := range spec.Responses {
    81  		response := spec.Responses[key]
    82  		if err := expandParameterOrResponse(&response, resolver, specBasePath); resolver.shouldStopOnError(err) {
    83  			return err
    84  		}
    85  		spec.Responses[key] = response
    86  	}
    87  
    88  	if spec.Paths != nil {
    89  		for key := range spec.Paths.Paths {
    90  			pth := spec.Paths.Paths[key]
    91  			if err := expandPathItem(&pth, resolver, specBasePath); resolver.shouldStopOnError(err) {
    92  				return err
    93  			}
    94  			spec.Paths.Paths[key] = pth
    95  		}
    96  	}
    97  
    98  	return nil
    99  }
   100  
   101  const rootBase = ".root"
   102  
   103  // baseForRoot loads in the cache the root document and produces a fake ".root" base path entry
   104  // for further $ref resolution
   105  func baseForRoot(root interface{}, cache ResolutionCache) string {
   106  	// cache the root document to resolve $ref's
   107  	normalizedBase := normalizeBase(rootBase)
   108  
   109  	if root == nil {
   110  		// ensure that we never leave a nil root: always cache the root base pseudo-document
   111  		cachedRoot, found := cache.Get(normalizedBase)
   112  		if found && cachedRoot != nil {
   113  			// the cache is already preloaded with a root
   114  			return normalizedBase
   115  		}
   116  
   117  		root = map[string]interface{}{}
   118  	}
   119  
   120  	cache.Set(normalizedBase, root)
   121  
   122  	return normalizedBase
   123  }
   124  
   125  // ExpandSchema expands the refs in the schema object with reference to the root object.
   126  //
   127  // go-openapi/validate uses this function.
   128  //
   129  // Notice that it is impossible to reference a json schema in a different document other than root
   130  // (use ExpandSchemaWithBasePath to resolve external references).
   131  //
   132  // Setting the cache is optional and this parameter may safely be left to nil.
   133  func ExpandSchema(schema *Schema, root interface{}, cache ResolutionCache) error {
   134  	cache = cacheOrDefault(cache)
   135  	if root == nil {
   136  		root = schema
   137  	}
   138  
   139  	opts := &ExpandOptions{
   140  		// when a root is specified, cache the root as an in-memory document for $ref retrieval
   141  		RelativeBase:    baseForRoot(root, cache),
   142  		SkipSchemas:     false,
   143  		ContinueOnError: false,
   144  	}
   145  
   146  	return ExpandSchemaWithBasePath(schema, cache, opts)
   147  }
   148  
   149  // ExpandSchemaWithBasePath expands the refs in the schema object, base path configured through expand options.
   150  //
   151  // Setting the cache is optional and this parameter may safely be left to nil.
   152  func ExpandSchemaWithBasePath(schema *Schema, cache ResolutionCache, opts *ExpandOptions) error {
   153  	if schema == nil {
   154  		return nil
   155  	}
   156  
   157  	cache = cacheOrDefault(cache)
   158  
   159  	opts = optionsOrDefault(opts)
   160  
   161  	resolver := defaultSchemaLoader(nil, opts, cache, nil)
   162  
   163  	parentRefs := make([]string, 0, 10)
   164  	s, err := expandSchema(*schema, parentRefs, resolver, opts.RelativeBase)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	if s != nil {
   169  		// guard for when continuing on error
   170  		*schema = *s
   171  	}
   172  
   173  	return nil
   174  }
   175  
   176  func expandItems(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) {
   177  	if target.Items == nil {
   178  		return &target, nil
   179  	}
   180  
   181  	// array
   182  	if target.Items.Schema != nil {
   183  		t, err := expandSchema(*target.Items.Schema, parentRefs, resolver, basePath)
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  		*target.Items.Schema = *t
   188  	}
   189  
   190  	// tuple
   191  	for i := range target.Items.Schemas {
   192  		t, err := expandSchema(target.Items.Schemas[i], parentRefs, resolver, basePath)
   193  		if err != nil {
   194  			return nil, err
   195  		}
   196  		target.Items.Schemas[i] = *t
   197  	}
   198  
   199  	return &target, nil
   200  }
   201  
   202  func expandSchema(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) {
   203  	if target.Ref.String() == "" && target.Ref.IsRoot() {
   204  		newRef := normalizeRef(&target.Ref, basePath)
   205  		target.Ref = *newRef
   206  		return &target, nil
   207  	}
   208  
   209  	// change the base path of resolution when an ID is encountered
   210  	// otherwise the basePath should inherit the parent's
   211  	if target.ID != "" {
   212  		basePath, _ = resolver.setSchemaID(target, target.ID, basePath)
   213  	}
   214  
   215  	if target.Ref.String() != "" {
   216  		if !resolver.options.SkipSchemas {
   217  			return expandSchemaRef(target, parentRefs, resolver, basePath)
   218  		}
   219  
   220  		// when "expand" with SkipSchema, we just rebase the existing $ref without replacing
   221  		// the full schema.
   222  		rebasedRef, err := NewRef(normalizeURI(target.Ref.String(), basePath))
   223  		if err != nil {
   224  			return nil, err
   225  		}
   226  		target.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID)
   227  
   228  		return &target, nil
   229  	}
   230  
   231  	for k := range target.Definitions {
   232  		tt, err := expandSchema(target.Definitions[k], parentRefs, resolver, basePath)
   233  		if resolver.shouldStopOnError(err) {
   234  			return &target, err
   235  		}
   236  		if tt != nil {
   237  			target.Definitions[k] = *tt
   238  		}
   239  	}
   240  
   241  	t, err := expandItems(target, parentRefs, resolver, basePath)
   242  	if resolver.shouldStopOnError(err) {
   243  		return &target, err
   244  	}
   245  	if t != nil {
   246  		target = *t
   247  	}
   248  
   249  	for i := range target.AllOf {
   250  		t, err := expandSchema(target.AllOf[i], parentRefs, resolver, basePath)
   251  		if resolver.shouldStopOnError(err) {
   252  			return &target, err
   253  		}
   254  		if t != nil {
   255  			target.AllOf[i] = *t
   256  		}
   257  	}
   258  
   259  	for i := range target.AnyOf {
   260  		t, err := expandSchema(target.AnyOf[i], parentRefs, resolver, basePath)
   261  		if resolver.shouldStopOnError(err) {
   262  			return &target, err
   263  		}
   264  		if t != nil {
   265  			target.AnyOf[i] = *t
   266  		}
   267  	}
   268  
   269  	for i := range target.OneOf {
   270  		t, err := expandSchema(target.OneOf[i], parentRefs, resolver, basePath)
   271  		if resolver.shouldStopOnError(err) {
   272  			return &target, err
   273  		}
   274  		if t != nil {
   275  			target.OneOf[i] = *t
   276  		}
   277  	}
   278  
   279  	if target.Not != nil {
   280  		t, err := expandSchema(*target.Not, parentRefs, resolver, basePath)
   281  		if resolver.shouldStopOnError(err) {
   282  			return &target, err
   283  		}
   284  		if t != nil {
   285  			*target.Not = *t
   286  		}
   287  	}
   288  
   289  	for k := range target.Properties {
   290  		t, err := expandSchema(target.Properties[k], parentRefs, resolver, basePath)
   291  		if resolver.shouldStopOnError(err) {
   292  			return &target, err
   293  		}
   294  		if t != nil {
   295  			target.Properties[k] = *t
   296  		}
   297  	}
   298  
   299  	if target.AdditionalProperties != nil && target.AdditionalProperties.Schema != nil {
   300  		t, err := expandSchema(*target.AdditionalProperties.Schema, parentRefs, resolver, basePath)
   301  		if resolver.shouldStopOnError(err) {
   302  			return &target, err
   303  		}
   304  		if t != nil {
   305  			*target.AdditionalProperties.Schema = *t
   306  		}
   307  	}
   308  
   309  	for k := range target.PatternProperties {
   310  		t, err := expandSchema(target.PatternProperties[k], parentRefs, resolver, basePath)
   311  		if resolver.shouldStopOnError(err) {
   312  			return &target, err
   313  		}
   314  		if t != nil {
   315  			target.PatternProperties[k] = *t
   316  		}
   317  	}
   318  
   319  	for k := range target.Dependencies {
   320  		if target.Dependencies[k].Schema != nil {
   321  			t, err := expandSchema(*target.Dependencies[k].Schema, parentRefs, resolver, basePath)
   322  			if resolver.shouldStopOnError(err) {
   323  				return &target, err
   324  			}
   325  			if t != nil {
   326  				*target.Dependencies[k].Schema = *t
   327  			}
   328  		}
   329  	}
   330  
   331  	if target.AdditionalItems != nil && target.AdditionalItems.Schema != nil {
   332  		t, err := expandSchema(*target.AdditionalItems.Schema, parentRefs, resolver, basePath)
   333  		if resolver.shouldStopOnError(err) {
   334  			return &target, err
   335  		}
   336  		if t != nil {
   337  			*target.AdditionalItems.Schema = *t
   338  		}
   339  	}
   340  	return &target, nil
   341  }
   342  
   343  func expandSchemaRef(target Schema, parentRefs []string, resolver *schemaLoader, basePath string) (*Schema, error) {
   344  	// if a Ref is found, all sibling fields are skipped
   345  	// Ref also changes the resolution scope of children expandSchema
   346  
   347  	// here the resolution scope is changed because a $ref was encountered
   348  	normalizedRef := normalizeRef(&target.Ref, basePath)
   349  	normalizedBasePath := normalizedRef.RemoteURI()
   350  
   351  	if resolver.isCircular(normalizedRef, basePath, parentRefs...) {
   352  		// this means there is a cycle in the recursion tree: return the Ref
   353  		// - circular refs cannot be expanded. We leave them as ref.
   354  		// - denormalization means that a new local file ref is set relative to the original basePath
   355  		debugLog("short circuit circular ref: basePath: %s, normalizedPath: %s, normalized ref: %s",
   356  			basePath, normalizedBasePath, normalizedRef.String())
   357  		if !resolver.options.AbsoluteCircularRef {
   358  			target.Ref = denormalizeRef(normalizedRef, resolver.context.basePath, resolver.context.rootID)
   359  		} else {
   360  			target.Ref = *normalizedRef
   361  		}
   362  		return &target, nil
   363  	}
   364  
   365  	var t *Schema
   366  	err := resolver.Resolve(&target.Ref, &t, basePath)
   367  	if resolver.shouldStopOnError(err) {
   368  		return nil, err
   369  	}
   370  
   371  	if t == nil {
   372  		// guard for when continuing on error
   373  		return &target, nil
   374  	}
   375  
   376  	parentRefs = append(parentRefs, normalizedRef.String())
   377  	transitiveResolver := resolver.transitiveResolver(basePath, target.Ref)
   378  
   379  	basePath = resolver.updateBasePath(transitiveResolver, normalizedBasePath)
   380  
   381  	return expandSchema(*t, parentRefs, transitiveResolver, basePath)
   382  }
   383  
   384  func expandPathItem(pathItem *PathItem, resolver *schemaLoader, basePath string) error {
   385  	if pathItem == nil {
   386  		return nil
   387  	}
   388  
   389  	parentRefs := make([]string, 0, 10)
   390  	if err := resolver.deref(pathItem, parentRefs, basePath); resolver.shouldStopOnError(err) {
   391  		return err
   392  	}
   393  
   394  	if pathItem.Ref.String() != "" {
   395  		transitiveResolver := resolver.transitiveResolver(basePath, pathItem.Ref)
   396  		basePath = transitiveResolver.updateBasePath(resolver, basePath)
   397  		resolver = transitiveResolver
   398  	}
   399  
   400  	pathItem.Ref = Ref{}
   401  	for i := range pathItem.Parameters {
   402  		if err := expandParameterOrResponse(&(pathItem.Parameters[i]), resolver, basePath); resolver.shouldStopOnError(err) {
   403  			return err
   404  		}
   405  	}
   406  
   407  	ops := []*Operation{
   408  		pathItem.Get,
   409  		pathItem.Head,
   410  		pathItem.Options,
   411  		pathItem.Put,
   412  		pathItem.Post,
   413  		pathItem.Patch,
   414  		pathItem.Delete,
   415  	}
   416  	for _, op := range ops {
   417  		if err := expandOperation(op, resolver, basePath); resolver.shouldStopOnError(err) {
   418  			return err
   419  		}
   420  	}
   421  
   422  	return nil
   423  }
   424  
   425  func expandOperation(op *Operation, resolver *schemaLoader, basePath string) error {
   426  	if op == nil {
   427  		return nil
   428  	}
   429  
   430  	for i := range op.Parameters {
   431  		param := op.Parameters[i]
   432  		if err := expandParameterOrResponse(&param, resolver, basePath); resolver.shouldStopOnError(err) {
   433  			return err
   434  		}
   435  		op.Parameters[i] = param
   436  	}
   437  
   438  	if op.Responses == nil {
   439  		return nil
   440  	}
   441  
   442  	responses := op.Responses
   443  	if err := expandParameterOrResponse(responses.Default, resolver, basePath); resolver.shouldStopOnError(err) {
   444  		return err
   445  	}
   446  
   447  	for code := range responses.StatusCodeResponses {
   448  		response := responses.StatusCodeResponses[code]
   449  		if err := expandParameterOrResponse(&response, resolver, basePath); resolver.shouldStopOnError(err) {
   450  			return err
   451  		}
   452  		responses.StatusCodeResponses[code] = response
   453  	}
   454  
   455  	return nil
   456  }
   457  
   458  // ExpandResponseWithRoot expands a response based on a root document, not a fetchable document
   459  //
   460  // Notice that it is impossible to reference a json schema in a different document other than root
   461  // (use ExpandResponse to resolve external references).
   462  //
   463  // Setting the cache is optional and this parameter may safely be left to nil.
   464  func ExpandResponseWithRoot(response *Response, root interface{}, cache ResolutionCache) error {
   465  	cache = cacheOrDefault(cache)
   466  	opts := &ExpandOptions{
   467  		RelativeBase: baseForRoot(root, cache),
   468  	}
   469  	resolver := defaultSchemaLoader(root, opts, cache, nil)
   470  
   471  	return expandParameterOrResponse(response, resolver, opts.RelativeBase)
   472  }
   473  
   474  // ExpandResponse expands a response based on a basepath
   475  //
   476  // All refs inside response will be resolved relative to basePath
   477  func ExpandResponse(response *Response, basePath string) error {
   478  	opts := optionsOrDefault(&ExpandOptions{
   479  		RelativeBase: basePath,
   480  	})
   481  	resolver := defaultSchemaLoader(nil, opts, nil, nil)
   482  
   483  	return expandParameterOrResponse(response, resolver, opts.RelativeBase)
   484  }
   485  
   486  // ExpandParameterWithRoot expands a parameter based on a root document, not a fetchable document.
   487  //
   488  // Notice that it is impossible to reference a json schema in a different document other than root
   489  // (use ExpandParameter to resolve external references).
   490  func ExpandParameterWithRoot(parameter *Parameter, root interface{}, cache ResolutionCache) error {
   491  	cache = cacheOrDefault(cache)
   492  
   493  	opts := &ExpandOptions{
   494  		RelativeBase: baseForRoot(root, cache),
   495  	}
   496  	resolver := defaultSchemaLoader(root, opts, cache, nil)
   497  
   498  	return expandParameterOrResponse(parameter, resolver, opts.RelativeBase)
   499  }
   500  
   501  // ExpandParameter expands a parameter based on a basepath.
   502  // This is the exported version of expandParameter
   503  // all refs inside parameter will be resolved relative to basePath
   504  func ExpandParameter(parameter *Parameter, basePath string) error {
   505  	opts := optionsOrDefault(&ExpandOptions{
   506  		RelativeBase: basePath,
   507  	})
   508  	resolver := defaultSchemaLoader(nil, opts, nil, nil)
   509  
   510  	return expandParameterOrResponse(parameter, resolver, opts.RelativeBase)
   511  }
   512  
   513  func getRefAndSchema(input interface{}) (*Ref, *Schema, error) {
   514  	var (
   515  		ref *Ref
   516  		sch *Schema
   517  	)
   518  
   519  	switch refable := input.(type) {
   520  	case *Parameter:
   521  		if refable == nil {
   522  			return nil, nil, nil
   523  		}
   524  		ref = &refable.Ref
   525  		sch = refable.Schema
   526  	case *Response:
   527  		if refable == nil {
   528  			return nil, nil, nil
   529  		}
   530  		ref = &refable.Ref
   531  		sch = refable.Schema
   532  	default:
   533  		return nil, nil, fmt.Errorf("unsupported type: %T: %w", input, ErrExpandUnsupportedType)
   534  	}
   535  
   536  	return ref, sch, nil
   537  }
   538  
   539  func expandParameterOrResponse(input interface{}, resolver *schemaLoader, basePath string) error {
   540  	ref, sch, err := getRefAndSchema(input)
   541  	if err != nil {
   542  		return err
   543  	}
   544  
   545  	if ref == nil && sch == nil { // nothing to do
   546  		return nil
   547  	}
   548  
   549  	parentRefs := make([]string, 0, 10)
   550  	if ref != nil {
   551  		// dereference this $ref
   552  		if err = resolver.deref(input, parentRefs, basePath); resolver.shouldStopOnError(err) {
   553  			return err
   554  		}
   555  
   556  		ref, sch, _ = getRefAndSchema(input)
   557  	}
   558  
   559  	if ref.String() != "" {
   560  		transitiveResolver := resolver.transitiveResolver(basePath, *ref)
   561  		basePath = resolver.updateBasePath(transitiveResolver, basePath)
   562  		resolver = transitiveResolver
   563  	}
   564  
   565  	if sch == nil {
   566  		// nothing to be expanded
   567  		if ref != nil {
   568  			*ref = Ref{}
   569  		}
   570  
   571  		return nil
   572  	}
   573  
   574  	if sch.Ref.String() != "" {
   575  		rebasedRef, ern := NewRef(normalizeURI(sch.Ref.String(), basePath))
   576  		if ern != nil {
   577  			return ern
   578  		}
   579  
   580  		if resolver.isCircular(&rebasedRef, basePath, parentRefs...) {
   581  			// this is a circular $ref: stop expansion
   582  			if !resolver.options.AbsoluteCircularRef {
   583  				sch.Ref = denormalizeRef(&rebasedRef, resolver.context.basePath, resolver.context.rootID)
   584  			} else {
   585  				sch.Ref = rebasedRef
   586  			}
   587  		}
   588  	}
   589  
   590  	// $ref expansion or rebasing is performed by expandSchema below
   591  	if ref != nil {
   592  		*ref = Ref{}
   593  	}
   594  
   595  	// expand schema
   596  	// yes, we do it even if options.SkipSchema is true: we have to go down that rabbit hole and rebase nested $ref)
   597  	s, err := expandSchema(*sch, parentRefs, resolver, basePath)
   598  	if resolver.shouldStopOnError(err) {
   599  		return err
   600  	}
   601  
   602  	if s != nil { // guard for when continuing on error
   603  		*sch = *s
   604  	}
   605  
   606  	return nil
   607  }
   608  

View as plain text