github.com/snyk/vervet/v6@v6.2.4/collator.go (about)

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