...

Source file src/github.com/go-openapi/analysis/flatten.go

Documentation: github.com/go-openapi/analysis

     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 analysis
    16  
    17  import (
    18  	"fmt"
    19  	"log"
    20  	"path"
    21  	"sort"
    22  	"strings"
    23  
    24  	"github.com/go-openapi/analysis/internal/flatten/normalize"
    25  	"github.com/go-openapi/analysis/internal/flatten/operations"
    26  	"github.com/go-openapi/analysis/internal/flatten/replace"
    27  	"github.com/go-openapi/analysis/internal/flatten/schutils"
    28  	"github.com/go-openapi/analysis/internal/flatten/sortref"
    29  	"github.com/go-openapi/jsonpointer"
    30  	"github.com/go-openapi/spec"
    31  )
    32  
    33  const definitionsPath = "#/definitions"
    34  
    35  // newRef stores information about refs created during the flattening process
    36  type newRef struct {
    37  	key      string
    38  	newName  string
    39  	path     string
    40  	isOAIGen bool
    41  	resolved bool
    42  	schema   *spec.Schema
    43  	parents  []string
    44  }
    45  
    46  // context stores intermediary results from flatten
    47  type context struct {
    48  	newRefs  map[string]*newRef
    49  	warnings []string
    50  	resolved map[string]string
    51  }
    52  
    53  func newContext() *context {
    54  	return &context{
    55  		newRefs:  make(map[string]*newRef, 150),
    56  		warnings: make([]string, 0),
    57  		resolved: make(map[string]string, 50),
    58  	}
    59  }
    60  
    61  // Flatten an analyzed spec and produce a self-contained spec bundle.
    62  //
    63  // There is a minimal and a full flattening mode.
    64  //
    65  // Minimally flattening a spec means:
    66  //   - Expanding parameters, responses, path items, parameter items and header items (references to schemas are left
    67  //     unscathed)
    68  //   - Importing external (http, file) references so they become internal to the document
    69  //   - Moving every JSON pointer to a $ref to a named definition (i.e. the reworked spec does not contain pointers
    70  //     like "$ref": "#/definitions/myObject/allOfs/1")
    71  //
    72  // A minimally flattened spec thus guarantees the following properties:
    73  //   - all $refs point to a local definition (i.e. '#/definitions/...')
    74  //   - definitions are unique
    75  //
    76  // NOTE: arbitrary JSON pointers (other than $refs to top level definitions) are rewritten as definitions if they
    77  // represent a complex schema or express commonality in the spec.
    78  // Otherwise, they are simply expanded.
    79  // Self-referencing JSON pointers cannot resolve to a type and trigger an error.
    80  //
    81  // Minimal flattening is necessary and sufficient for codegen rendering using go-swagger.
    82  //
    83  // Fully flattening a spec means:
    84  //   - Moving every complex inline schema to be a definition with an auto-generated name in a depth-first fashion.
    85  //
    86  // By complex, we mean every JSON object with some properties.
    87  // Arrays, when they do not define a tuple,
    88  // or empty objects with or without additionalProperties, are not considered complex and remain inline.
    89  //
    90  // NOTE: rewritten schemas get a vendor extension x-go-gen-location so we know from which part of the spec definitions
    91  // have been created.
    92  //
    93  // Available flattening options:
    94  //   - Minimal: stops flattening after minimal $ref processing, leaving schema constructs untouched
    95  //   - Expand: expand all $ref's in the document (inoperant if Minimal set to true)
    96  //   - Verbose: croaks about name conflicts detected
    97  //   - RemoveUnused: removes unused parameters, responses and definitions after expansion/flattening
    98  //
    99  // NOTE: expansion removes all $ref save circular $ref, which remain in place
   100  //
   101  // TODO: additional options
   102  //   - ProgagateNameExtensions: ensure that created entries properly follow naming rules when their parent have set a
   103  //     x-go-name extension
   104  //   - LiftAllOfs:
   105  //   - limit the flattening of allOf members when simple objects
   106  //   - merge allOf with validation only
   107  //   - merge allOf with extensions only
   108  //   - ...
   109  func Flatten(opts FlattenOpts) error {
   110  	debugLog("FlattenOpts: %#v", opts)
   111  
   112  	opts.flattenContext = newContext()
   113  
   114  	// 1. Recursively expand responses, parameters, path items and items in simple schemas.
   115  	//
   116  	// This simplifies the spec and leaves only the $ref's in schema objects.
   117  	if err := expand(&opts); err != nil {
   118  		return err
   119  	}
   120  
   121  	// 2. Strip the current document from absolute $ref's that actually a in the root,
   122  	// so we can recognize them as proper definitions
   123  	//
   124  	// In particular, this works around issue go-openapi/spec#76: leading absolute file in $ref is stripped
   125  	if err := normalizeRef(&opts); err != nil {
   126  		return err
   127  	}
   128  
   129  	// 3. Optionally remove shared parameters and responses already expanded (now unused).
   130  	//
   131  	// Operation parameters (i.e. under paths) remain.
   132  	if opts.RemoveUnused {
   133  		removeUnusedShared(&opts)
   134  	}
   135  
   136  	// 4. Import all remote references.
   137  	if err := importReferences(&opts); err != nil {
   138  		return err
   139  	}
   140  
   141  	// 5. full flattening: rewrite inline schemas (schemas that aren't simple types or arrays or maps)
   142  	if !opts.Minimal && !opts.Expand {
   143  		if err := nameInlinedSchemas(&opts); err != nil {
   144  			return err
   145  		}
   146  	}
   147  
   148  	// 6. Rewrite JSON pointers other than $ref to named definitions
   149  	// and attempt to resolve conflicting names whenever possible.
   150  	if err := stripPointersAndOAIGen(&opts); err != nil {
   151  		return err
   152  	}
   153  
   154  	// 7. Strip the spec from unused definitions
   155  	if opts.RemoveUnused {
   156  		removeUnused(&opts)
   157  	}
   158  
   159  	// 8. Issue warning notifications, if any
   160  	opts.croak()
   161  
   162  	// TODO: simplify known schema patterns to flat objects with properties
   163  	// examples:
   164  	//  - lift simple allOf object,
   165  	//  - empty allOf with validation only or extensions only
   166  	//  - rework allOf arrays
   167  	//  - rework allOf additionalProperties
   168  
   169  	return nil
   170  }
   171  
   172  func expand(opts *FlattenOpts) error {
   173  	if err := spec.ExpandSpec(opts.Swagger(), opts.ExpandOpts(!opts.Expand)); err != nil {
   174  		return err
   175  	}
   176  
   177  	opts.Spec.reload() // re-analyze
   178  
   179  	return nil
   180  }
   181  
   182  // normalizeRef strips the current file from any absolute file $ref. This works around issue go-openapi/spec#76:
   183  // leading absolute file in $ref is stripped
   184  func normalizeRef(opts *FlattenOpts) error {
   185  	debugLog("normalizeRef")
   186  
   187  	altered := false
   188  	for k, w := range opts.Spec.references.allRefs {
   189  		if !strings.HasPrefix(w.String(), opts.BasePath+definitionsPath) { // may be a mix of / and \, depending on OS
   190  			continue
   191  		}
   192  
   193  		altered = true
   194  		debugLog("stripping absolute path for: %s", w.String())
   195  
   196  		// strip the base path from definition
   197  		if err := replace.UpdateRef(opts.Swagger(), k,
   198  			spec.MustCreateRef(path.Join(definitionsPath, path.Base(w.String())))); err != nil {
   199  			return err
   200  		}
   201  	}
   202  
   203  	if altered {
   204  		opts.Spec.reload() // re-analyze
   205  	}
   206  
   207  	return nil
   208  }
   209  
   210  func removeUnusedShared(opts *FlattenOpts) {
   211  	opts.Swagger().Parameters = nil
   212  	opts.Swagger().Responses = nil
   213  
   214  	opts.Spec.reload() // re-analyze
   215  }
   216  
   217  func importReferences(opts *FlattenOpts) error {
   218  	var (
   219  		imported bool
   220  		err      error
   221  	)
   222  
   223  	for !imported && err == nil {
   224  		// iteratively import remote references until none left.
   225  		// This inlining deals with name conflicts by introducing auto-generated names ("OAIGen")
   226  		imported, err = importExternalReferences(opts)
   227  
   228  		opts.Spec.reload() // re-analyze
   229  	}
   230  
   231  	return err
   232  }
   233  
   234  // nameInlinedSchemas replaces every complex inline construct by a named definition.
   235  func nameInlinedSchemas(opts *FlattenOpts) error {
   236  	debugLog("nameInlinedSchemas")
   237  
   238  	namer := &InlineSchemaNamer{
   239  		Spec:           opts.Swagger(),
   240  		Operations:     operations.AllOpRefsByRef(opts.Spec, nil),
   241  		flattenContext: opts.flattenContext,
   242  		opts:           opts,
   243  	}
   244  
   245  	depthFirst := sortref.DepthFirst(opts.Spec.allSchemas)
   246  	for _, key := range depthFirst {
   247  		sch := opts.Spec.allSchemas[key]
   248  		if sch.Schema == nil || sch.Schema.Ref.String() != "" || sch.TopLevel {
   249  			continue
   250  		}
   251  
   252  		asch, err := Schema(SchemaOpts{Schema: sch.Schema, Root: opts.Swagger(), BasePath: opts.BasePath})
   253  		if err != nil {
   254  			return fmt.Errorf("schema analysis [%s]: %w", key, err)
   255  		}
   256  
   257  		if asch.isAnalyzedAsComplex() { // move complex schemas to definitions
   258  			if err := namer.Name(key, sch.Schema, asch); err != nil {
   259  				return err
   260  			}
   261  		}
   262  	}
   263  
   264  	opts.Spec.reload() // re-analyze
   265  
   266  	return nil
   267  }
   268  
   269  func removeUnused(opts *FlattenOpts) {
   270  	for removeUnusedSinglePass(opts) {
   271  		// continue until no unused definition remains
   272  	}
   273  }
   274  
   275  func removeUnusedSinglePass(opts *FlattenOpts) (hasRemoved bool) {
   276  	expected := make(map[string]struct{})
   277  	for k := range opts.Swagger().Definitions {
   278  		expected[path.Join(definitionsPath, jsonpointer.Escape(k))] = struct{}{}
   279  	}
   280  
   281  	for _, k := range opts.Spec.AllDefinitionReferences() {
   282  		delete(expected, k)
   283  	}
   284  
   285  	for k := range expected {
   286  		hasRemoved = true
   287  		debugLog("removing unused definition %s", path.Base(k))
   288  		if opts.Verbose {
   289  			log.Printf("info: removing unused definition: %s", path.Base(k))
   290  		}
   291  		delete(opts.Swagger().Definitions, path.Base(k))
   292  	}
   293  
   294  	opts.Spec.reload() // re-analyze
   295  
   296  	return hasRemoved
   297  }
   298  
   299  func importKnownRef(entry sortref.RefRevIdx, refStr, newName string, opts *FlattenOpts) error {
   300  	// rewrite ref with already resolved external ref (useful for cyclical refs):
   301  	// rewrite external refs to local ones
   302  	debugLog("resolving known ref [%s] to %s", refStr, newName)
   303  
   304  	for _, key := range entry.Keys {
   305  		if err := replace.UpdateRef(opts.Swagger(), key, spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil {
   306  			return err
   307  		}
   308  	}
   309  
   310  	return nil
   311  }
   312  
   313  func importNewRef(entry sortref.RefRevIdx, refStr string, opts *FlattenOpts) error {
   314  	var (
   315  		isOAIGen bool
   316  		newName  string
   317  	)
   318  
   319  	debugLog("resolving schema from remote $ref [%s]", refStr)
   320  
   321  	sch, err := spec.ResolveRefWithBase(opts.Swagger(), &entry.Ref, opts.ExpandOpts(false))
   322  	if err != nil {
   323  		return fmt.Errorf("could not resolve schema: %w", err)
   324  	}
   325  
   326  	// at this stage only $ref analysis matters
   327  	partialAnalyzer := &Spec{
   328  		references: referenceAnalysis{},
   329  		patterns:   patternAnalysis{},
   330  		enums:      enumAnalysis{},
   331  	}
   332  	partialAnalyzer.reset()
   333  	partialAnalyzer.analyzeSchema("", sch, "/")
   334  
   335  	// now rewrite those refs with rebase
   336  	for key, ref := range partialAnalyzer.references.allRefs {
   337  		if err := replace.UpdateRef(sch, key, spec.MustCreateRef(normalize.RebaseRef(entry.Ref.String(), ref.String()))); err != nil {
   338  			return fmt.Errorf("failed to rewrite ref for key %q at %s: %w", key, entry.Ref.String(), err)
   339  		}
   340  	}
   341  
   342  	// generate a unique name - isOAIGen means that a naming conflict was resolved by changing the name
   343  	newName, isOAIGen = uniqifyName(opts.Swagger().Definitions, nameFromRef(entry.Ref, opts))
   344  	debugLog("new name for [%s]: %s - with name conflict:%t", strings.Join(entry.Keys, ", "), newName, isOAIGen)
   345  
   346  	opts.flattenContext.resolved[refStr] = newName
   347  
   348  	// rewrite the external refs to local ones
   349  	for _, key := range entry.Keys {
   350  		if err := replace.UpdateRef(opts.Swagger(), key,
   351  			spec.MustCreateRef(path.Join(definitionsPath, newName))); err != nil {
   352  			return err
   353  		}
   354  
   355  		// keep track of created refs
   356  		resolved := false
   357  		if _, ok := opts.flattenContext.newRefs[key]; ok {
   358  			resolved = opts.flattenContext.newRefs[key].resolved
   359  		}
   360  
   361  		debugLog("keeping track of ref: %s (%s), resolved: %t", key, newName, resolved)
   362  		opts.flattenContext.newRefs[key] = &newRef{
   363  			key:      key,
   364  			newName:  newName,
   365  			path:     path.Join(definitionsPath, newName),
   366  			isOAIGen: isOAIGen,
   367  			resolved: resolved,
   368  			schema:   sch,
   369  		}
   370  	}
   371  
   372  	// add the resolved schema to the definitions
   373  	schutils.Save(opts.Swagger(), newName, sch)
   374  
   375  	return nil
   376  }
   377  
   378  // importExternalReferences iteratively digs remote references and imports them into the main schema.
   379  //
   380  // At every iteration, new remotes may be found when digging deeper: they are rebased to the current schema before being imported.
   381  //
   382  // This returns true when no more remote references can be found.
   383  func importExternalReferences(opts *FlattenOpts) (bool, error) {
   384  	debugLog("importExternalReferences")
   385  
   386  	groupedRefs := sortref.ReverseIndex(opts.Spec.references.schemas, opts.BasePath)
   387  	sortedRefStr := make([]string, 0, len(groupedRefs))
   388  	if opts.flattenContext == nil {
   389  		opts.flattenContext = newContext()
   390  	}
   391  
   392  	// sort $ref resolution to ensure deterministic name conflict resolution
   393  	for refStr := range groupedRefs {
   394  		sortedRefStr = append(sortedRefStr, refStr)
   395  	}
   396  	sort.Strings(sortedRefStr)
   397  
   398  	complete := true
   399  
   400  	for _, refStr := range sortedRefStr {
   401  		entry := groupedRefs[refStr]
   402  		if entry.Ref.HasFragmentOnly {
   403  			continue
   404  		}
   405  
   406  		complete = false
   407  
   408  		newName := opts.flattenContext.resolved[refStr]
   409  		if newName != "" {
   410  			if err := importKnownRef(entry, refStr, newName, opts); err != nil {
   411  				return false, err
   412  			}
   413  
   414  			continue
   415  		}
   416  
   417  		// resolve schemas
   418  		if err := importNewRef(entry, refStr, opts); err != nil {
   419  			return false, err
   420  		}
   421  	}
   422  
   423  	// maintains ref index entries
   424  	for k := range opts.flattenContext.newRefs {
   425  		r := opts.flattenContext.newRefs[k]
   426  
   427  		// update tracking with resolved schemas
   428  		if r.schema.Ref.String() != "" {
   429  			ref := spec.MustCreateRef(r.path)
   430  			sch, err := spec.ResolveRefWithBase(opts.Swagger(), &ref, opts.ExpandOpts(false))
   431  			if err != nil {
   432  				return false, fmt.Errorf("could not resolve schema: %w", err)
   433  			}
   434  
   435  			r.schema = sch
   436  		}
   437  
   438  		if r.path == k {
   439  			continue
   440  		}
   441  
   442  		// update tracking with renamed keys: got a cascade of refs
   443  		renamed := *r
   444  		renamed.key = r.path
   445  		opts.flattenContext.newRefs[renamed.path] = &renamed
   446  
   447  		// indirect ref
   448  		r.newName = path.Base(k)
   449  		r.schema = spec.RefSchema(r.path)
   450  		r.path = k
   451  		r.isOAIGen = strings.Contains(k, "OAIGen")
   452  	}
   453  
   454  	return complete, nil
   455  }
   456  
   457  // stripPointersAndOAIGen removes anonymous JSON pointers from spec and chain with name conflicts handler.
   458  // This loops until the spec has no such pointer and all name conflicts have been reduced as much as possible.
   459  func stripPointersAndOAIGen(opts *FlattenOpts) error {
   460  	// name all JSON pointers to anonymous documents
   461  	if err := namePointers(opts); err != nil {
   462  		return err
   463  	}
   464  
   465  	// remove unnecessary OAIGen ref (created when flattening external refs creates name conflicts)
   466  	hasIntroducedPointerOrInline, ers := stripOAIGen(opts)
   467  	if ers != nil {
   468  		return ers
   469  	}
   470  
   471  	// iterate as pointer or OAIGen resolution may introduce inline schemas or pointers
   472  	for hasIntroducedPointerOrInline {
   473  		if !opts.Minimal {
   474  			opts.Spec.reload() // re-analyze
   475  			if err := nameInlinedSchemas(opts); err != nil {
   476  				return err
   477  			}
   478  		}
   479  
   480  		if err := namePointers(opts); err != nil {
   481  			return err
   482  		}
   483  
   484  		// restrip and re-analyze
   485  		var err error
   486  		if hasIntroducedPointerOrInline, err = stripOAIGen(opts); err != nil {
   487  			return err
   488  		}
   489  	}
   490  
   491  	return nil
   492  }
   493  
   494  // stripOAIGen strips the spec from unnecessary OAIGen constructs, initially created to dedupe flattened definitions.
   495  //
   496  // A dedupe is deemed unnecessary whenever:
   497  //   - the only conflict is with its (single) parent: OAIGen is merged into its parent (reinlining)
   498  //   - there is a conflict with multiple parents: merge OAIGen in first parent, the rewrite other parents to point to
   499  //     the first parent.
   500  //
   501  // This function returns true whenever it re-inlined a complex schema, so the caller may chose to iterate
   502  // pointer and name resolution again.
   503  func stripOAIGen(opts *FlattenOpts) (bool, error) {
   504  	debugLog("stripOAIGen")
   505  	replacedWithComplex := false
   506  
   507  	// figure out referers of OAIGen definitions (doing it before the ref start mutating)
   508  	for _, r := range opts.flattenContext.newRefs {
   509  		updateRefParents(opts.Spec.references.allRefs, r)
   510  	}
   511  
   512  	for k := range opts.flattenContext.newRefs {
   513  		r := opts.flattenContext.newRefs[k]
   514  		debugLog("newRefs[%s]: isOAIGen: %t, resolved: %t, name: %s, path:%s, #parents: %d, parents: %v,  ref: %s",
   515  			k, r.isOAIGen, r.resolved, r.newName, r.path, len(r.parents), r.parents, r.schema.Ref.String())
   516  
   517  		if !r.isOAIGen || len(r.parents) == 0 {
   518  			continue
   519  		}
   520  
   521  		hasReplacedWithComplex, err := stripOAIGenForRef(opts, k, r)
   522  		if err != nil {
   523  			return replacedWithComplex, err
   524  		}
   525  
   526  		replacedWithComplex = replacedWithComplex || hasReplacedWithComplex
   527  	}
   528  
   529  	debugLog("replacedWithComplex: %t", replacedWithComplex)
   530  	opts.Spec.reload() // re-analyze
   531  
   532  	return replacedWithComplex, nil
   533  }
   534  
   535  // updateRefParents updates all parents of an updated $ref
   536  func updateRefParents(allRefs map[string]spec.Ref, r *newRef) {
   537  	if !r.isOAIGen || r.resolved { // bail on already resolved entries (avoid looping)
   538  		return
   539  	}
   540  	for k, v := range allRefs {
   541  		if r.path != v.String() {
   542  			continue
   543  		}
   544  
   545  		found := false
   546  		for _, p := range r.parents {
   547  			if p == k {
   548  				found = true
   549  
   550  				break
   551  			}
   552  		}
   553  		if !found {
   554  			r.parents = append(r.parents, k)
   555  		}
   556  	}
   557  }
   558  
   559  func stripOAIGenForRef(opts *FlattenOpts, k string, r *newRef) (bool, error) {
   560  	replacedWithComplex := false
   561  
   562  	pr := sortref.TopmostFirst(r.parents)
   563  
   564  	// rewrite first parent schema in hierarchical then lexicographical order
   565  	debugLog("rewrite first parent %s with schema", pr[0])
   566  	if err := replace.UpdateRefWithSchema(opts.Swagger(), pr[0], r.schema); err != nil {
   567  		return false, err
   568  	}
   569  
   570  	if pa, ok := opts.flattenContext.newRefs[pr[0]]; ok && pa.isOAIGen {
   571  		// update parent in ref index entry
   572  		debugLog("update parent entry: %s", pr[0])
   573  		pa.schema = r.schema
   574  		pa.resolved = false
   575  		replacedWithComplex = true
   576  	}
   577  
   578  	// rewrite other parents to point to first parent
   579  	if len(pr) > 1 {
   580  		for _, p := range pr[1:] {
   581  			replacingRef := spec.MustCreateRef(pr[0])
   582  
   583  			// set complex when replacing ref is an anonymous jsonpointer: further processing may be required
   584  			replacedWithComplex = replacedWithComplex || path.Dir(replacingRef.String()) != definitionsPath
   585  			debugLog("rewrite parent with ref: %s", replacingRef.String())
   586  
   587  			// NOTE: it is possible at this stage to introduce json pointers (to non-definitions places).
   588  			// Those are stripped later on.
   589  			if err := replace.UpdateRef(opts.Swagger(), p, replacingRef); err != nil {
   590  				return false, err
   591  			}
   592  
   593  			if pa, ok := opts.flattenContext.newRefs[p]; ok && pa.isOAIGen {
   594  				// update parent in ref index
   595  				debugLog("update parent entry: %s", p)
   596  				pa.schema = r.schema
   597  				pa.resolved = false
   598  				replacedWithComplex = true
   599  			}
   600  		}
   601  	}
   602  
   603  	// remove OAIGen definition
   604  	debugLog("removing definition %s", path.Base(r.path))
   605  	delete(opts.Swagger().Definitions, path.Base(r.path))
   606  
   607  	// propagate changes in ref index for keys which have this one as a parent
   608  	for kk, value := range opts.flattenContext.newRefs {
   609  		if kk == k || !value.isOAIGen || value.resolved {
   610  			continue
   611  		}
   612  
   613  		found := false
   614  		newParents := make([]string, 0, len(value.parents))
   615  		for _, parent := range value.parents {
   616  			switch {
   617  			case parent == r.path:
   618  				found = true
   619  				parent = pr[0]
   620  			case strings.HasPrefix(parent, r.path+"/"):
   621  				found = true
   622  				parent = path.Join(pr[0], strings.TrimPrefix(parent, r.path))
   623  			}
   624  
   625  			newParents = append(newParents, parent)
   626  		}
   627  
   628  		if found {
   629  			value.parents = newParents
   630  		}
   631  	}
   632  
   633  	// mark naming conflict as resolved
   634  	debugLog("marking naming conflict resolved for key: %s", r.key)
   635  	opts.flattenContext.newRefs[r.key].isOAIGen = false
   636  	opts.flattenContext.newRefs[r.key].resolved = true
   637  
   638  	// determine if the previous substitution did inline a complex schema
   639  	if r.schema != nil && r.schema.Ref.String() == "" { // inline schema
   640  		asch, err := Schema(SchemaOpts{Schema: r.schema, Root: opts.Swagger(), BasePath: opts.BasePath})
   641  		if err != nil {
   642  			return false, err
   643  		}
   644  
   645  		debugLog("re-inlined schema: parent: %s, %t", pr[0], asch.isAnalyzedAsComplex())
   646  		replacedWithComplex = replacedWithComplex || !(path.Dir(pr[0]) == definitionsPath) && asch.isAnalyzedAsComplex()
   647  	}
   648  
   649  	return replacedWithComplex, nil
   650  }
   651  
   652  // namePointers replaces all JSON pointers to anonymous documents by a $ref to a new named definitions.
   653  //
   654  // This is carried on depth-first. Pointers to $refs which are top level definitions are replaced by the $ref itself.
   655  // Pointers to simple types are expanded, unless they express commonality (i.e. several such $ref are used).
   656  func namePointers(opts *FlattenOpts) error {
   657  	debugLog("name pointers")
   658  
   659  	refsToReplace := make(map[string]SchemaRef, len(opts.Spec.references.schemas))
   660  	for k, ref := range opts.Spec.references.allRefs {
   661  		debugLog("name pointers: %q => %#v", k, ref)
   662  		if path.Dir(ref.String()) == definitionsPath {
   663  			// this a ref to a top-level definition: ok
   664  			continue
   665  		}
   666  
   667  		result, err := replace.DeepestRef(opts.Swagger(), opts.ExpandOpts(false), ref)
   668  		if err != nil {
   669  			return fmt.Errorf("at %s, %w", k, err)
   670  		}
   671  
   672  		replacingRef := result.Ref
   673  		sch := result.Schema
   674  		if opts.flattenContext != nil {
   675  			opts.flattenContext.warnings = append(opts.flattenContext.warnings, result.Warnings...)
   676  		}
   677  
   678  		debugLog("planning pointer to replace at %s: %s, resolved to: %s", k, ref.String(), replacingRef.String())
   679  		refsToReplace[k] = SchemaRef{
   680  			Name:     k,            // caller
   681  			Ref:      replacingRef, // called
   682  			Schema:   sch,
   683  			TopLevel: path.Dir(replacingRef.String()) == definitionsPath,
   684  		}
   685  	}
   686  
   687  	depthFirst := sortref.DepthFirst(refsToReplace)
   688  	namer := &InlineSchemaNamer{
   689  		Spec:           opts.Swagger(),
   690  		Operations:     operations.AllOpRefsByRef(opts.Spec, nil),
   691  		flattenContext: opts.flattenContext,
   692  		opts:           opts,
   693  	}
   694  
   695  	for _, key := range depthFirst {
   696  		v := refsToReplace[key]
   697  		// update current replacement, which may have been updated by previous changes of deeper elements
   698  		result, erd := replace.DeepestRef(opts.Swagger(), opts.ExpandOpts(false), v.Ref)
   699  		if erd != nil {
   700  			return fmt.Errorf("at %s, %w", key, erd)
   701  		}
   702  
   703  		if opts.flattenContext != nil {
   704  			opts.flattenContext.warnings = append(opts.flattenContext.warnings, result.Warnings...)
   705  		}
   706  
   707  		v.Ref = result.Ref
   708  		v.Schema = result.Schema
   709  		v.TopLevel = path.Dir(result.Ref.String()) == definitionsPath
   710  		debugLog("replacing pointer at %s: resolved to: %s", key, v.Ref.String())
   711  
   712  		if v.TopLevel {
   713  			debugLog("replace pointer %s by canonical definition: %s", key, v.Ref.String())
   714  
   715  			// if the schema is a $ref to a top level definition, just rewrite the pointer to this $ref
   716  			if err := replace.UpdateRef(opts.Swagger(), key, v.Ref); err != nil {
   717  				return err
   718  			}
   719  
   720  			continue
   721  		}
   722  
   723  		if err := flattenAnonPointer(key, v, refsToReplace, namer, opts); err != nil {
   724  			return err
   725  		}
   726  	}
   727  
   728  	opts.Spec.reload() // re-analyze
   729  
   730  	return nil
   731  }
   732  
   733  func flattenAnonPointer(key string, v SchemaRef, refsToReplace map[string]SchemaRef, namer *InlineSchemaNamer, opts *FlattenOpts) error {
   734  	// this is a JSON pointer to an anonymous document (internal or external):
   735  	// create a definition for this schema when:
   736  	// - it is a complex schema
   737  	// - or it is pointed by more than one $ref (i.e. expresses commonality)
   738  	// otherwise, expand the pointer (single reference to a simple type)
   739  	//
   740  	// The named definition for this follows the target's key, not the caller's
   741  	debugLog("namePointers at %s for %s", key, v.Ref.String())
   742  
   743  	// qualify the expanded schema
   744  	asch, ers := Schema(SchemaOpts{Schema: v.Schema, Root: opts.Swagger(), BasePath: opts.BasePath})
   745  	if ers != nil {
   746  		return fmt.Errorf("schema analysis [%s]: %w", key, ers)
   747  	}
   748  	callers := make([]string, 0, 64)
   749  
   750  	debugLog("looking for callers")
   751  
   752  	an := New(opts.Swagger())
   753  	for k, w := range an.references.allRefs {
   754  		r, err := replace.DeepestRef(opts.Swagger(), opts.ExpandOpts(false), w)
   755  		if err != nil {
   756  			return fmt.Errorf("at %s, %w", key, err)
   757  		}
   758  
   759  		if opts.flattenContext != nil {
   760  			opts.flattenContext.warnings = append(opts.flattenContext.warnings, r.Warnings...)
   761  		}
   762  
   763  		if r.Ref.String() == v.Ref.String() {
   764  			callers = append(callers, k)
   765  		}
   766  	}
   767  
   768  	debugLog("callers for %s: %d", v.Ref.String(), len(callers))
   769  	if len(callers) == 0 {
   770  		// has already been updated and resolved
   771  		return nil
   772  	}
   773  
   774  	parts := sortref.KeyParts(v.Ref.String())
   775  	debugLog("number of callers for %s: %d", v.Ref.String(), len(callers))
   776  
   777  	// identifying edge case when the namer did nothing because we point to a non-schema object
   778  	// no definition is created and we expand the $ref for all callers
   779  	debugLog("decide what to do with the schema pointed to: asch.IsSimpleSchema=%t, len(callers)=%d, parts.IsSharedParam=%t, parts.IsSharedResponse=%t",
   780  		asch.IsSimpleSchema, len(callers), parts.IsSharedParam(), parts.IsSharedResponse(),
   781  	)
   782  
   783  	if (!asch.IsSimpleSchema || len(callers) > 1) && !parts.IsSharedParam() && !parts.IsSharedResponse() {
   784  		debugLog("replace JSON pointer at [%s] by definition: %s", key, v.Ref.String())
   785  		if err := namer.Name(v.Ref.String(), v.Schema, asch); err != nil {
   786  			return err
   787  		}
   788  
   789  		// regular case: we named the $ref as a definition, and we move all callers to this new $ref
   790  		for _, caller := range callers {
   791  			if caller == key {
   792  				continue
   793  			}
   794  
   795  			// move $ref for next to resolve
   796  			debugLog("identified caller of %s at [%s]", v.Ref.String(), caller)
   797  			c := refsToReplace[caller]
   798  			c.Ref = v.Ref
   799  			refsToReplace[caller] = c
   800  		}
   801  
   802  		return nil
   803  	}
   804  
   805  	// everything that is a simple schema and not factorizable is expanded
   806  	debugLog("expand JSON pointer for key=%s", key)
   807  
   808  	if err := replace.UpdateRefWithSchema(opts.Swagger(), key, v.Schema); err != nil {
   809  		return err
   810  	}
   811  	// NOTE: there is no other caller to update
   812  
   813  	return nil
   814  }
   815  

View as plain text