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 }