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 }