1// Copyright 2015 go-swagger maintainers 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package analysis 16 17import ( 18 "fmt" 19 "reflect" 20 21 "github.com/go-openapi/spec" 22) 23 24// Mixin modifies the primary swagger spec by adding the paths and 25// definitions from the mixin specs. Top level parameters and 26// responses from the mixins are also carried over. Operation id 27// collisions are avoided by appending "Mixin<N>" but only if 28// needed. 29// 30// The following parts of primary are subject to merge, filling empty details 31// - Info 32// - BasePath 33// - Host 34// - ExternalDocs 35// 36// Consider calling FixEmptyResponseDescriptions() on the modified primary 37// if you read them from storage and they are valid to start with. 38// 39// Entries in "paths", "definitions", "parameters" and "responses" are 40// added to the primary in the order of the given mixins. If the entry 41// already exists in primary it is skipped with a warning message. 42// 43// The count of skipped entries (from collisions) is returned so any 44// deviation from the number expected can flag a warning in your build 45// scripts. Carefully review the collisions before accepting them; 46// consider renaming things if possible. 47// 48// No key normalization takes place (paths, type defs, 49// etc). Ensure they are canonical if your downstream tools do 50// key normalization of any form. 51// 52// Merging schemes (http, https), and consumers/producers do not account for 53// collisions. 54func Mixin(primary *spec.Swagger, mixins ...*spec.Swagger) []string { 55 skipped := make([]string, 0, len(mixins)) 56 opIds := getOpIds(primary) 57 initPrimary(primary) 58 59 for i, m := range mixins { 60 skipped = append(skipped, mergeSwaggerProps(primary, m)...) 61 62 skipped = append(skipped, mergeConsumes(primary, m)...) 63 64 skipped = append(skipped, mergeProduces(primary, m)...) 65 66 skipped = append(skipped, mergeTags(primary, m)...) 67 68 skipped = append(skipped, mergeSchemes(primary, m)...) 69 70 skipped = append(skipped, mergeSecurityDefinitions(primary, m)...) 71 72 skipped = append(skipped, mergeSecurityRequirements(primary, m)...) 73 74 skipped = append(skipped, mergeDefinitions(primary, m)...) 75 76 // merging paths requires a map of operationIDs to work with 77 skipped = append(skipped, mergePaths(primary, m, opIds, i)...) 78 79 skipped = append(skipped, mergeParameters(primary, m)...) 80 81 skipped = append(skipped, mergeResponses(primary, m)...) 82 } 83 84 return skipped 85} 86 87// getOpIds extracts all the paths.<path>.operationIds from the given 88// spec and returns them as the keys in a map with 'true' values. 89func getOpIds(s *spec.Swagger) map[string]bool { 90 rv := make(map[string]bool) 91 if s.Paths == nil { 92 return rv 93 } 94 95 for _, v := range s.Paths.Paths { 96 piops := pathItemOps(v) 97 98 for _, op := range piops { 99 rv[op.ID] = true 100 } 101 } 102 103 return rv 104} 105 106func pathItemOps(p spec.PathItem) []*spec.Operation { 107 var rv []*spec.Operation 108 rv = appendOp(rv, p.Get) 109 rv = appendOp(rv, p.Put) 110 rv = appendOp(rv, p.Post) 111 rv = appendOp(rv, p.Delete) 112 rv = appendOp(rv, p.Head) 113 rv = appendOp(rv, p.Patch) 114 115 return rv 116} 117 118func appendOp(ops []*spec.Operation, op *spec.Operation) []*spec.Operation { 119 if op == nil { 120 return ops 121 } 122 123 return append(ops, op) 124} 125 126func mergeSecurityDefinitions(primary *spec.Swagger, m *spec.Swagger) (skipped []string) { 127 for k, v := range m.SecurityDefinitions { 128 if _, exists := primary.SecurityDefinitions[k]; exists { 129 warn := fmt.Sprintf( 130 "SecurityDefinitions entry '%v' already exists in primary or higher priority mixin, skipping\n", k) 131 skipped = append(skipped, warn) 132 133 continue 134 } 135 136 primary.SecurityDefinitions[k] = v 137 } 138 139 return 140} 141 142func mergeSecurityRequirements(primary *spec.Swagger, m *spec.Swagger) (skipped []string) { 143 for _, v := range m.Security { 144 found := false 145 for _, vv := range primary.Security { 146 if reflect.DeepEqual(v, vv) { 147 found = true 148 149 break 150 } 151 } 152 153 if found { 154 warn := fmt.Sprintf( 155 "Security requirement: '%v' already exists in primary or higher priority mixin, skipping\n", v) 156 skipped = append(skipped, warn) 157 158 continue 159 } 160 primary.Security = append(primary.Security, v) 161 } 162 163 return 164} 165 166func mergeDefinitions(primary *spec.Swagger, m *spec.Swagger) (skipped []string) { 167 for k, v := range m.Definitions { 168 // assume name collisions represent IDENTICAL type. careful. 169 if _, exists := primary.Definitions[k]; exists { 170 warn := fmt.Sprintf( 171 "definitions entry '%v' already exists in primary or higher priority mixin, skipping\n", k) 172 skipped = append(skipped, warn) 173 174 continue 175 } 176 primary.Definitions[k] = v 177 } 178 179 return 180} 181 182func mergePaths(primary *spec.Swagger, m *spec.Swagger, opIds map[string]bool, mixIndex int) (skipped []string) { 183 if m.Paths != nil { 184 for k, v := range m.Paths.Paths { 185 if _, exists := primary.Paths.Paths[k]; exists { 186 warn := fmt.Sprintf( 187 "paths entry '%v' already exists in primary or higher priority mixin, skipping\n", k) 188 skipped = append(skipped, warn) 189 190 continue 191 } 192 193 // Swagger requires that operationIds be 194 // unique within a spec. If we find a 195 // collision we append "Mixin0" to the 196 // operatoinId we are adding, where 0 is mixin 197 // index. We assume that operationIds with 198 // all the proivded specs are already unique. 199 piops := pathItemOps(v) 200 for _, piop := range piops { 201 if opIds[piop.ID] { 202 piop.ID = fmt.Sprintf("%v%v%v", piop.ID, "Mixin", mixIndex) 203 } 204 opIds[piop.ID] = true 205 } 206 primary.Paths.Paths[k] = v 207 } 208 } 209 210 return 211} 212 213func mergeParameters(primary *spec.Swagger, m *spec.Swagger) (skipped []string) { 214 for k, v := range m.Parameters { 215 // could try to rename on conflict but would 216 // have to fix $refs in the mixin. Complain 217 // for now 218 if _, exists := primary.Parameters[k]; exists { 219 warn := fmt.Sprintf( 220 "top level parameters entry '%v' already exists in primary or higher priority mixin, skipping\n", k) 221 skipped = append(skipped, warn) 222 223 continue 224 } 225 primary.Parameters[k] = v 226 } 227 228 return 229} 230 231func mergeResponses(primary *spec.Swagger, m *spec.Swagger) (skipped []string) { 232 for k, v := range m.Responses { 233 // could try to rename on conflict but would 234 // have to fix $refs in the mixin. Complain 235 // for now 236 if _, exists := primary.Responses[k]; exists { 237 warn := fmt.Sprintf( 238 "top level responses entry '%v' already exists in primary or higher priority mixin, skipping\n", k) 239 skipped = append(skipped, warn) 240 241 continue 242 } 243 primary.Responses[k] = v 244 } 245 246 return skipped 247} 248 249func mergeConsumes(primary *spec.Swagger, m *spec.Swagger) []string { 250 for _, v := range m.Consumes { 251 found := false 252 for _, vv := range primary.Consumes { 253 if v == vv { 254 found = true 255 256 break 257 } 258 } 259 260 if found { 261 // no warning here: we just skip it 262 continue 263 } 264 primary.Consumes = append(primary.Consumes, v) 265 } 266 267 return []string{} 268} 269 270func mergeProduces(primary *spec.Swagger, m *spec.Swagger) []string { 271 for _, v := range m.Produces { 272 found := false 273 for _, vv := range primary.Produces { 274 if v == vv { 275 found = true 276 277 break 278 } 279 } 280 281 if found { 282 // no warning here: we just skip it 283 continue 284 } 285 primary.Produces = append(primary.Produces, v) 286 } 287 288 return []string{} 289} 290 291func mergeTags(primary *spec.Swagger, m *spec.Swagger) (skipped []string) { 292 for _, v := range m.Tags { 293 found := false 294 for _, vv := range primary.Tags { 295 if v.Name == vv.Name { 296 found = true 297 298 break 299 } 300 } 301 302 if found { 303 warn := fmt.Sprintf( 304 "top level tags entry with name '%v' already exists in primary or higher priority mixin, skipping\n", 305 v.Name, 306 ) 307 skipped = append(skipped, warn) 308 309 continue 310 } 311 312 primary.Tags = append(primary.Tags, v) 313 } 314 315 return 316} 317 318func mergeSchemes(primary *spec.Swagger, m *spec.Swagger) []string { 319 for _, v := range m.Schemes { 320 found := false 321 for _, vv := range primary.Schemes { 322 if v == vv { 323 found = true 324 325 break 326 } 327 } 328 329 if found { 330 // no warning here: we just skip it 331 continue 332 } 333 primary.Schemes = append(primary.Schemes, v) 334 } 335 336 return []string{} 337} 338 339func mergeSwaggerProps(primary *spec.Swagger, m *spec.Swagger) []string { 340 var skipped, skippedInfo, skippedDocs []string 341 342 primary.Extensions, skipped = mergeExtensions(primary.Extensions, m.Extensions) 343 344 // merging details in swagger top properties 345 if primary.Host == "" { 346 primary.Host = m.Host 347 } 348 349 if primary.BasePath == "" { 350 primary.BasePath = m.BasePath 351 } 352 353 if primary.Info == nil { 354 primary.Info = m.Info 355 } else if m.Info != nil { 356 skippedInfo = mergeInfo(primary.Info, m.Info) 357 skipped = append(skipped, skippedInfo...) 358 } 359 360 if primary.ExternalDocs == nil { 361 primary.ExternalDocs = m.ExternalDocs 362 } else if m != nil { 363 skippedDocs = mergeExternalDocs(primary.ExternalDocs, m.ExternalDocs) 364 skipped = append(skipped, skippedDocs...) 365 } 366 367 return skipped 368} 369 370// nolint: unparam 371func mergeExternalDocs(primary *spec.ExternalDocumentation, m *spec.ExternalDocumentation) []string { 372 if primary.Description == "" { 373 primary.Description = m.Description 374 } 375 376 if primary.URL == "" { 377 primary.URL = m.URL 378 } 379 380 return nil 381} 382 383func mergeInfo(primary *spec.Info, m *spec.Info) []string { 384 var sk, skipped []string 385 386 primary.Extensions, sk = mergeExtensions(primary.Extensions, m.Extensions) 387 skipped = append(skipped, sk...) 388 389 if primary.Description == "" { 390 primary.Description = m.Description 391 } 392 393 if primary.Title == "" { 394 primary.Description = m.Description 395 } 396 397 if primary.TermsOfService == "" { 398 primary.TermsOfService = m.TermsOfService 399 } 400 401 if primary.Version == "" { 402 primary.Version = m.Version 403 } 404 405 if primary.Contact == nil { 406 primary.Contact = m.Contact 407 } else if m.Contact != nil { 408 var csk []string 409 primary.Contact.Extensions, csk = mergeExtensions(primary.Contact.Extensions, m.Contact.Extensions) 410 skipped = append(skipped, csk...) 411 412 if primary.Contact.Name == "" { 413 primary.Contact.Name = m.Contact.Name 414 } 415 416 if primary.Contact.URL == "" { 417 primary.Contact.URL = m.Contact.URL 418 } 419 420 if primary.Contact.Email == "" { 421 primary.Contact.Email = m.Contact.Email 422 } 423 } 424 425 if primary.License == nil { 426 primary.License = m.License 427 } else if m.License != nil { 428 var lsk []string 429 primary.License.Extensions, lsk = mergeExtensions(primary.License.Extensions, m.License.Extensions) 430 skipped = append(skipped, lsk...) 431 432 if primary.License.Name == "" { 433 primary.License.Name = m.License.Name 434 } 435 436 if primary.License.URL == "" { 437 primary.License.URL = m.License.URL 438 } 439 } 440 441 return skipped 442} 443 444func mergeExtensions(primary spec.Extensions, m spec.Extensions) (result spec.Extensions, skipped []string) { 445 if primary == nil { 446 result = m 447 448 return 449 } 450 451 if m == nil { 452 result = primary 453 454 return 455 } 456 457 result = primary 458 for k, v := range m { 459 if _, found := primary[k]; found { 460 skipped = append(skipped, k) 461 462 continue 463 } 464 465 primary[k] = v 466 } 467 468 return 469} 470 471func initPrimary(primary *spec.Swagger) { 472 if primary.SecurityDefinitions == nil { 473 primary.SecurityDefinitions = make(map[string]*spec.SecurityScheme) 474 } 475 476 if primary.Security == nil { 477 primary.Security = make([]map[string][]string, 0, 10) 478 } 479 480 if primary.Produces == nil { 481 primary.Produces = make([]string, 0, 10) 482 } 483 484 if primary.Consumes == nil { 485 primary.Consumes = make([]string, 0, 10) 486 } 487 488 if primary.Tags == nil { 489 primary.Tags = make([]spec.Tag, 0, 10) 490 } 491 492 if primary.Schemes == nil { 493 primary.Schemes = make([]string, 0, 10) 494 } 495 496 if primary.Paths == nil { 497 primary.Paths = &spec.Paths{Paths: make(map[string]spec.PathItem)} 498 } 499 500 if primary.Paths.Paths == nil { 501 primary.Paths.Paths = make(map[string]spec.PathItem) 502 } 503 504 if primary.Definitions == nil { 505 primary.Definitions = make(spec.Definitions) 506 } 507 508 if primary.Parameters == nil { 509 primary.Parameters = make(map[string]spec.Parameter) 510 } 511 512 if primary.Responses == nil { 513 primary.Responses = make(map[string]spec.Response) 514 } 515} 516