github.com/snyk/vervet/v4@v4.27.2/collator.go (about)

     1  package vervet
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"sort"
     7  
     8  	"github.com/getkin/kin-openapi/openapi3"
     9  	"github.com/google/go-cmp/cmp"
    10  	"github.com/google/go-cmp/cmp/cmpopts"
    11  	"go.uber.org/multierr"
    12  )
    13  
    14  // Collator merges resource versions into a single OpenAPI document.
    15  type Collator struct {
    16  	result           *openapi3.T
    17  	componentSources map[string]string
    18  	pathSources      map[string]string
    19  	tagSources       map[string]string
    20  
    21  	strictTags    bool
    22  	useFirstRoute bool
    23  	seenRoutes    map[string]struct{}
    24  }
    25  
    26  // NewCollator returns a new Collator instance.
    27  func NewCollator(options ...CollatorOption) *Collator {
    28  	coll := &Collator{
    29  		componentSources: map[string]string{},
    30  		pathSources:      map[string]string{},
    31  		tagSources:       map[string]string{},
    32  		strictTags:       true,
    33  		seenRoutes:       map[string]struct{}{},
    34  	}
    35  	for i := range options {
    36  		options[i](coll)
    37  	}
    38  	return coll
    39  }
    40  
    41  // CollatorOption defines an option when creating a Collator.
    42  type CollatorOption func(*Collator)
    43  
    44  // StrictTags defines whether a collator should enforce a strict conflict check
    45  // when merging tags.
    46  func StrictTags(strict bool) CollatorOption {
    47  	return func(coll *Collator) {
    48  		coll.strictTags = strict
    49  	}
    50  }
    51  
    52  // UseFirstRoute determines whether a collator should use the first matching
    53  // path in the result when merging paths. When true, the first matching path
    54  // goes into the collated result, similar to how a routing table matches a
    55  // path. When false, a conflicting path route will result in an error.
    56  //
    57  // Path variable names do not differentiate path routes; /foo/{bar} and
    58  // /foo/{baz} are regarded as the same route.
    59  func UseFirstRoute(useFirstRoute bool) CollatorOption {
    60  	return func(coll *Collator) {
    61  		coll.useFirstRoute = useFirstRoute
    62  	}
    63  }
    64  
    65  // Result returns the merged result. If no versions have been merged, returns
    66  // nil.
    67  func (c *Collator) Result() *openapi3.T {
    68  	return c.result
    69  }
    70  
    71  // Collate merges a resource version into the current result.
    72  func (c *Collator) Collate(rv *ResourceVersion) error {
    73  	var errs error
    74  	if c.result == nil {
    75  		c.result = &openapi3.T{}
    76  	}
    77  
    78  	err := rv.cleanRefs()
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	mergeExtensions(c.result, rv.T, false)
    84  	mergeInfo(c.result, rv.T, false)
    85  	mergeOpenAPIVersion(c.result, rv.T, false)
    86  	mergeSecurityRequirements(c.result, rv.T, false)
    87  	mergeServers(c.result, rv.T, false)
    88  
    89  	if err = c.mergeComponents(rv); err != nil {
    90  		errs = multierr.Append(errs, err)
    91  	}
    92  
    93  	if err = c.mergePaths(rv); err != nil {
    94  		errs = multierr.Append(errs, err)
    95  	}
    96  
    97  	if err = c.mergeTags(rv); err != nil {
    98  		errs = multierr.Append(errs, err)
    99  	}
   100  
   101  	return errs
   102  }
   103  
   104  func (c *Collator) mergeTags(rv *ResourceVersion) error {
   105  	m := map[string]*openapi3.Tag{}
   106  	for _, t := range c.result.Tags {
   107  		m[t.Name] = t
   108  	}
   109  	var errs error
   110  	for _, t := range rv.T.Tags {
   111  		if current, ok := m[t.Name]; ok && !tagsEqual(current, t) && c.strictTags {
   112  			// If there is a conflict and we're collating with strict tags, indicate an error.
   113  			errs = multierr.Append(errs, fmt.Errorf("conflict in #/tags %s: %s and %s differ", t.Name, rv.path, c.tagSources[t.Name]))
   114  		} else {
   115  			// Otherwise last tag with this key wins.
   116  			m[t.Name] = t
   117  			c.tagSources[t.Name] = rv.path
   118  		}
   119  	}
   120  	if errs != nil {
   121  		return errs
   122  	}
   123  	c.result.Tags = openapi3.Tags{}
   124  	var tagNames []string
   125  	for tagName := range m {
   126  		tagNames = append(tagNames, tagName)
   127  	}
   128  	sort.Strings(tagNames)
   129  	for _, tagName := range tagNames {
   130  		c.result.Tags = append(c.result.Tags, m[tagName])
   131  	}
   132  	return nil
   133  }
   134  
   135  func (c *Collator) mergeComponents(rv *ResourceVersion) error {
   136  	initDestinationComponents(c.result, rv.T)
   137  	var errs error
   138  	inliner := NewInliner()
   139  	for k, v := range rv.T.Components.Schemas {
   140  		ref := "#/components/schemas/" + k
   141  		if current, ok := c.result.Components.Schemas[k]; ok && !componentsEqual(current, v) {
   142  			inliner.AddRef(ref)
   143  		} else {
   144  			c.result.Components.Schemas[k] = v
   145  			c.componentSources[ref] = rv.path
   146  		}
   147  	}
   148  	for k, v := range rv.T.Components.Parameters {
   149  		ref := "#/components/parameters/" + k
   150  		if current, ok := c.result.Components.Parameters[k]; ok && !componentsEqual(current, v) {
   151  			inliner.AddRef(ref)
   152  		} else {
   153  			c.result.Components.Parameters[k] = v
   154  			c.componentSources[ref] = rv.path
   155  		}
   156  	}
   157  	for k, v := range rv.T.Components.Headers {
   158  		ref := "#/components/headers/" + k
   159  		if current, ok := c.result.Components.Headers[k]; ok && !componentsEqual(current, v) {
   160  			inliner.AddRef(ref)
   161  		} else {
   162  			c.result.Components.Headers[k] = v
   163  			c.componentSources[ref] = rv.path
   164  		}
   165  	}
   166  	for k, v := range rv.T.Components.RequestBodies {
   167  		ref := "#/components/requestBodies/" + k
   168  		if current, ok := c.result.Components.RequestBodies[k]; ok && !componentsEqual(current, v) {
   169  			inliner.AddRef(ref)
   170  		} else {
   171  			c.result.Components.RequestBodies[k] = v
   172  			c.componentSources[ref] = rv.path
   173  		}
   174  	}
   175  	for k, v := range rv.T.Components.Responses {
   176  		ref := "#/components/responses/" + k
   177  		if current, ok := c.result.Components.Responses[k]; ok && !componentsEqual(current, v) {
   178  			inliner.AddRef(ref)
   179  		} else {
   180  			c.result.Components.Responses[k] = v
   181  			c.componentSources[ref] = rv.path
   182  		}
   183  	}
   184  	for k, v := range rv.T.Components.SecuritySchemes {
   185  		ref := "#/components/securitySchemas/" + k
   186  		if current, ok := c.result.Components.SecuritySchemes[k]; ok && !componentsEqual(current, v) {
   187  			inliner.AddRef(ref)
   188  		} else {
   189  			c.result.Components.SecuritySchemes[k] = v
   190  			c.componentSources[ref] = rv.path
   191  		}
   192  	}
   193  	for k, v := range rv.T.Components.Examples {
   194  		ref := "#/components/examples/" + k
   195  		if current, ok := c.result.Components.Examples[k]; ok && !componentsEqual(current, v) {
   196  			inliner.AddRef(ref)
   197  		} else {
   198  			c.result.Components.Examples[k] = v
   199  			c.componentSources[ref] = rv.path
   200  		}
   201  	}
   202  	for k, v := range rv.T.Components.Links {
   203  		ref := "#/components/links/" + k
   204  		if current, ok := c.result.Components.Links[k]; ok && !componentsEqual(current, v) {
   205  			inliner.AddRef(ref)
   206  		} else {
   207  			c.result.Components.Links[k] = v
   208  			c.componentSources[ref] = rv.path
   209  		}
   210  	}
   211  	for k, v := range rv.T.Components.Callbacks {
   212  		ref := "#/components/callbacks/" + k
   213  		if current, ok := c.result.Components.Callbacks[k]; ok && !componentsEqual(current, v) {
   214  			inliner.AddRef(ref)
   215  		} else {
   216  			c.result.Components.Callbacks[k] = v
   217  			c.componentSources[ref] = rv.path
   218  		}
   219  	}
   220  	if errs == nil {
   221  		err := inliner.Inline(rv.T)
   222  		if err != nil {
   223  			errs = multierr.Append(errs, err)
   224  		}
   225  	}
   226  	return errs
   227  }
   228  
   229  var cmpComponents = cmp.Options{
   230  	// openapi3.Schema has some unexported fields which are ignored for the
   231  	// purposes of content comparison.
   232  	cmpopts.IgnoreUnexported(openapi3.Schema{}),
   233  	// Refs themselves can mutate during relocation, so they are excluded from
   234  	// content comparison.
   235  	cmp.FilterPath(func(p cmp.Path) bool {
   236  		switch p.Last().String() {
   237  		case ".Ref", ".Description", ".Example", ".Summary":
   238  			return true
   239  		}
   240  		return false
   241  	}, cmp.Ignore()),
   242  }
   243  
   244  func componentsEqual(x, y interface{}) bool {
   245  	return cmp.Equal(x, y, cmpComponents)
   246  }
   247  
   248  func tagsEqual(x, y interface{}) bool {
   249  	return cmp.Equal(x, y)
   250  }
   251  
   252  func (c *Collator) mergePaths(rv *ResourceVersion) error {
   253  	if rv.T.Paths != nil && c.result.Paths == nil {
   254  		c.result.Paths = make(openapi3.Paths)
   255  	}
   256  	var errs error
   257  	for k, v := range rv.T.Paths {
   258  		route := routeForPath(k)
   259  		if _, ok := c.seenRoutes[route]; ok {
   260  			if c.useFirstRoute {
   261  				continue
   262  			} else {
   263  				errs = multierr.Append(errs, fmt.Errorf("conflict in #/paths %s: declared in both %s and %s", k, rv.path, c.pathSources[k]))
   264  			}
   265  		} else {
   266  			c.seenRoutes[route] = struct{}{}
   267  			c.result.Paths[k] = v
   268  			c.pathSources[k] = rv.path
   269  		}
   270  	}
   271  	return errs
   272  }
   273  
   274  var routeForPathRE = regexp.MustCompile(`\{[^}]*\}`)
   275  
   276  func routeForPath(path string) string {
   277  	return routeForPathRE.ReplaceAllString(path, "{}")
   278  }