github.com/w3security/vervet/v5@v5.3.1-0.20230618081846-5bd9b5d799dc/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(
   114  				errs,
   115  				fmt.Errorf("conflict in #/tags %s: %s and %s differ", t.Name, rv.path, c.tagSources[t.Name]),
   116  			)
   117  		} else {
   118  			// Otherwise last tag with this key wins.
   119  			m[t.Name] = t
   120  			c.tagSources[t.Name] = rv.path
   121  		}
   122  	}
   123  	if errs != nil {
   124  		return errs
   125  	}
   126  	c.result.Tags = openapi3.Tags{}
   127  	tagNames := []string{}
   128  	for tagName := range m {
   129  		tagNames = append(tagNames, tagName)
   130  	}
   131  	sort.Strings(tagNames)
   132  	for _, tagName := range tagNames {
   133  		c.result.Tags = append(c.result.Tags, m[tagName])
   134  	}
   135  	return nil
   136  }
   137  
   138  func (c *Collator) mergeComponents(rv *ResourceVersion) error {
   139  	if rv.Components == nil {
   140  		return nil
   141  	}
   142  
   143  	if c.result.Components == nil {
   144  		c.result.Components = &openapi3.Components{}
   145  	}
   146  
   147  	initDestinationComponents(c.result, rv.T)
   148  
   149  	inliner := NewInliner()
   150  	for k, v := range rv.T.Components.Schemas {
   151  		ref := "#/components/schemas/" + k
   152  		if current, ok := c.result.Components.Schemas[k]; ok && !componentsEqual(current, v) {
   153  			inliner.AddRef(ref)
   154  		} else {
   155  			c.result.Components.Schemas[k] = v
   156  			c.componentSources[ref] = rv.path
   157  		}
   158  	}
   159  	for k, v := range rv.T.Components.Parameters {
   160  		ref := "#/components/parameters/" + k
   161  		if current, ok := c.result.Components.Parameters[k]; ok && !componentsEqual(current, v) {
   162  			inliner.AddRef(ref)
   163  		} else {
   164  			c.result.Components.Parameters[k] = v
   165  			c.componentSources[ref] = rv.path
   166  		}
   167  	}
   168  	for k, v := range rv.T.Components.Headers {
   169  		ref := "#/components/headers/" + k
   170  		if current, ok := c.result.Components.Headers[k]; ok && !componentsEqual(current, v) {
   171  			inliner.AddRef(ref)
   172  		} else {
   173  			c.result.Components.Headers[k] = v
   174  			c.componentSources[ref] = rv.path
   175  		}
   176  	}
   177  	for k, v := range rv.T.Components.RequestBodies {
   178  		ref := "#/components/requestBodies/" + k
   179  		if current, ok := c.result.Components.RequestBodies[k]; ok && !componentsEqual(current, v) {
   180  			inliner.AddRef(ref)
   181  		} else {
   182  			c.result.Components.RequestBodies[k] = v
   183  			c.componentSources[ref] = rv.path
   184  		}
   185  	}
   186  	for k, v := range rv.T.Components.Responses {
   187  		ref := "#/components/responses/" + k
   188  		if current, ok := c.result.Components.Responses[k]; ok && !componentsEqual(current, v) {
   189  			inliner.AddRef(ref)
   190  		} else {
   191  			c.result.Components.Responses[k] = v
   192  			c.componentSources[ref] = rv.path
   193  		}
   194  	}
   195  	for k, v := range rv.T.Components.SecuritySchemes {
   196  		ref := "#/components/securitySchemas/" + k
   197  		if current, ok := c.result.Components.SecuritySchemes[k]; ok && !componentsEqual(current, v) {
   198  			inliner.AddRef(ref)
   199  		} else {
   200  			c.result.Components.SecuritySchemes[k] = v
   201  			c.componentSources[ref] = rv.path
   202  		}
   203  	}
   204  	for k, v := range rv.T.Components.Examples {
   205  		ref := "#/components/examples/" + k
   206  		if current, ok := c.result.Components.Examples[k]; ok && !componentsEqual(current, v) {
   207  			inliner.AddRef(ref)
   208  		} else {
   209  			c.result.Components.Examples[k] = v
   210  			c.componentSources[ref] = rv.path
   211  		}
   212  	}
   213  	for k, v := range rv.T.Components.Links {
   214  		ref := "#/components/links/" + k
   215  		if current, ok := c.result.Components.Links[k]; ok && !componentsEqual(current, v) {
   216  			inliner.AddRef(ref)
   217  		} else {
   218  			c.result.Components.Links[k] = v
   219  			c.componentSources[ref] = rv.path
   220  		}
   221  	}
   222  	for k, v := range rv.T.Components.Callbacks {
   223  		ref := "#/components/callbacks/" + k
   224  		if current, ok := c.result.Components.Callbacks[k]; ok && !componentsEqual(current, v) {
   225  			inliner.AddRef(ref)
   226  		} else {
   227  			c.result.Components.Callbacks[k] = v
   228  			c.componentSources[ref] = rv.path
   229  		}
   230  	}
   231  	return inliner.Inline(rv.T)
   232  }
   233  
   234  var cmpComponents = cmp.Options{
   235  	// openapi3.Schema has some unexported fields which are ignored for the
   236  	// purposes of content comparison.
   237  	cmpopts.IgnoreUnexported(
   238  		openapi3.HeaderRef{},
   239  		openapi3.ParameterRef{},
   240  		openapi3.ResponseRef{},
   241  		openapi3.Schema{},
   242  		openapi3.SchemaRef{},
   243  	),
   244  	// Refs themselves can mutate during relocation, so they are excluded from
   245  	// content comparison.
   246  	cmp.FilterPath(func(p cmp.Path) bool {
   247  		switch p.Last().String() {
   248  		case ".Ref", ".Description", ".Example", ".Summary":
   249  			return true
   250  		}
   251  		return false
   252  	}, cmp.Ignore()),
   253  }
   254  
   255  func componentsEqual(x, y interface{}) bool {
   256  	return cmp.Equal(x, y, cmpComponents)
   257  }
   258  
   259  func tagsEqual(x, y interface{}) bool {
   260  	return cmp.Equal(x, y)
   261  }
   262  
   263  func (c *Collator) mergePaths(rv *ResourceVersion) error {
   264  	if rv.T.Paths != nil && c.result.Paths == nil {
   265  		c.result.Paths = make(openapi3.Paths)
   266  	}
   267  	var errs error
   268  	for k, v := range rv.T.Paths {
   269  		route := routeForPath(k)
   270  		if _, ok := c.seenRoutes[route]; ok {
   271  			if c.useFirstRoute {
   272  				continue
   273  			} else {
   274  				errs = multierr.Append(
   275  					errs,
   276  					fmt.Errorf("conflict in #/paths %s: declared in both %s and %s", k, rv.path, c.pathSources[k]),
   277  				)
   278  			}
   279  		} else {
   280  			c.seenRoutes[route] = struct{}{}
   281  			c.result.Paths[k] = v
   282  			c.pathSources[k] = rv.path
   283  		}
   284  	}
   285  	return errs
   286  }
   287  
   288  var routeForPathRE = regexp.MustCompile(`\{[^}]*\}`)
   289  
   290  func routeForPath(path string) string {
   291  	return routeForPathRE.ReplaceAllString(path, "{}")
   292  }