github.com/go-swagger/go-swagger@v0.31.0/cmd/swagger/commands/diff/spec_analyser.go (about) 1 package diff 2 3 import ( 4 "fmt" 5 "reflect" 6 "strings" 7 8 "github.com/go-openapi/spec" 9 ) 10 11 // StringType For identifying string types 12 const StringType = "string" 13 14 // URLMethodResponse encapsulates these three elements to act as a map key 15 type URLMethodResponse struct { 16 Path string `json:"path"` 17 Method string `json:"method"` 18 Response string `json:"response"` 19 } 20 21 // MarshalText - for serializing as a map key 22 func (p URLMethod) MarshalText() (text []byte, err error) { 23 return []byte(fmt.Sprintf("%s %s", p.Path, p.Method)), nil 24 } 25 26 // URLMethods allows iteration of endpoints based on url and method 27 type URLMethods map[URLMethod]*PathItemOp 28 29 // SpecAnalyser contains all the differences for a Spec 30 type SpecAnalyser struct { 31 Diffs SpecDifferences 32 urlMethods1 URLMethods 33 urlMethods2 URLMethods 34 Definitions1 spec.Definitions 35 Definitions2 spec.Definitions 36 Info1 *spec.Info 37 Info2 *spec.Info 38 ReferencedDefinitions map[string]bool 39 40 schemasCompared map[string]struct{} 41 } 42 43 // NewSpecAnalyser returns an empty SpecDiffs 44 func NewSpecAnalyser() *SpecAnalyser { 45 return &SpecAnalyser{ 46 Diffs: SpecDifferences{}, 47 ReferencedDefinitions: map[string]bool{}, 48 } 49 } 50 51 // Analyse the differences in two specs 52 func (sd *SpecAnalyser) Analyse(spec1, spec2 *spec.Swagger) error { 53 sd.schemasCompared = make(map[string]struct{}) 54 sd.Definitions1 = spec1.Definitions 55 sd.Definitions2 = spec2.Definitions 56 sd.Info1 = spec1.Info 57 sd.Info2 = spec2.Info 58 sd.urlMethods1 = getURLMethodsFor(spec1) 59 sd.urlMethods2 = getURLMethodsFor(spec2) 60 61 sd.analyseSpecMetadata(spec1, spec2) 62 sd.analyseEndpoints() 63 sd.analyseRequestParams() 64 sd.analyseEndpointData() 65 sd.analyseResponseParams() 66 sd.analyseExtensions(spec1, spec2) 67 sd.AnalyseDefinitions() 68 69 return nil 70 } 71 72 func (sd *SpecAnalyser) analyseSpecMetadata(spec1, spec2 *spec.Swagger) { 73 // breaking if it no longer consumes any formats 74 added, deleted, _ := fromStringArray(spec1.Consumes).DiffsTo(spec2.Consumes) 75 76 node := getNameOnlyDiffNode("Spec") 77 location := DifferenceLocation{Node: node} 78 consumesLoation := location.AddNode(getNameOnlyDiffNode("consumes")) 79 80 for _, eachAdded := range added { 81 sd.Diffs = sd.Diffs.addDiff( 82 SpecDifference{DifferenceLocation: consumesLoation, Code: AddedConsumesFormat, Compatibility: NonBreaking, DiffInfo: eachAdded}) 83 } 84 for _, eachDeleted := range deleted { 85 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: consumesLoation, Code: DeletedConsumesFormat, Compatibility: Breaking, DiffInfo: eachDeleted}) 86 } 87 88 // // breaking if it no longer produces any formats 89 added, deleted, _ = fromStringArray(spec1.Produces).DiffsTo(spec2.Produces) 90 producesLocation := location.AddNode(getNameOnlyDiffNode("produces")) 91 for _, eachAdded := range added { 92 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: producesLocation, Code: AddedProducesFormat, Compatibility: NonBreaking, DiffInfo: eachAdded}) 93 } 94 for _, eachDeleted := range deleted { 95 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: producesLocation, Code: DeletedProducesFormat, Compatibility: Breaking, DiffInfo: eachDeleted}) 96 } 97 98 // // breaking if it no longer supports a scheme 99 added, deleted, _ = fromStringArray(spec1.Schemes).DiffsTo(spec2.Schemes) 100 schemesLocation := location.AddNode(getNameOnlyDiffNode("schemes")) 101 102 for _, eachAdded := range added { 103 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: schemesLocation, Code: AddedSchemes, Compatibility: NonBreaking, DiffInfo: eachAdded}) 104 } 105 for _, eachDeleted := range deleted { 106 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: schemesLocation, Code: DeletedSchemes, Compatibility: Breaking, DiffInfo: eachDeleted}) 107 } 108 109 // host should be able to change without any issues? 110 sd.analyseMetaDataProperty(spec1.Info.Description, spec2.Info.Description, ChangedDescripton, NonBreaking) 111 112 // // host should be able to change without any issues? 113 sd.analyseMetaDataProperty(spec1.Host, spec2.Host, ChangedHostURL, Breaking) 114 // sd.Host = compareStrings(spec1.Host, spec2.Host) 115 116 // // Base Path change will break non generated clients 117 sd.analyseMetaDataProperty(spec1.BasePath, spec2.BasePath, ChangedBasePath, Breaking) 118 119 // TODO: what to do about security? 120 // Missing security scheme will break a client 121 // Security []map[string][]string `json:"security,omitempty"` 122 // Tags []Tag `json:"tags,omitempty"` 123 // ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty"` 124 } 125 126 func (sd *SpecAnalyser) analyseEndpoints() { 127 sd.findDeletedEndpoints() 128 sd.findAddedEndpoints() 129 } 130 131 // AnalyseDefinitions check for changes to definition objects not referenced in any endpoint 132 func (sd *SpecAnalyser) AnalyseDefinitions() { 133 alreadyReferenced := map[string]bool{} 134 for k := range sd.ReferencedDefinitions { 135 alreadyReferenced[k] = true 136 } 137 location := DifferenceLocation{Node: &Node{Field: "Spec Definitions"}} 138 for name1, sch := range sd.Definitions1 { 139 schema1 := sch 140 if _, ok := alreadyReferenced[name1]; !ok { 141 childLocation := location.AddNode(&Node{Field: name1}) 142 if schema2, ok := sd.Definitions2[name1]; ok { 143 sd.compareSchema(childLocation, &schema1, &schema2) 144 } else { 145 sd.addDiffs(childLocation, []TypeDiff{{Change: DeletedDefinition}}) 146 } 147 } 148 } 149 for name2 := range sd.Definitions2 { 150 if _, ok := sd.Definitions1[name2]; !ok { 151 childLocation := location.AddNode(&Node{Field: name2}) 152 sd.addDiffs(childLocation, []TypeDiff{{Change: AddedDefinition}}) 153 } 154 } 155 } 156 157 func (sd *SpecAnalyser) analyseEndpointData() { 158 159 for URLMethod, op2 := range sd.urlMethods2 { 160 if op1, ok := sd.urlMethods1[URLMethod]; ok { 161 addedTags, deletedTags, _ := fromStringArray(op1.Operation.Tags).DiffsTo(op2.Operation.Tags) 162 location := DifferenceLocation{URL: URLMethod.Path, Method: URLMethod.Method} 163 164 for _, eachAddedTag := range addedTags { 165 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: AddedTag, DiffInfo: fmt.Sprintf(`"%s"`, eachAddedTag)}) 166 } 167 for _, eachDeletedTag := range deletedTags { 168 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: DeletedTag, DiffInfo: fmt.Sprintf(`"%s"`, eachDeletedTag)}) 169 } 170 171 sd.compareDescripton(location, op1.Operation.Description, op2.Operation.Description) 172 173 } 174 } 175 } 176 177 func (sd *SpecAnalyser) analyseRequestParams() { 178 locations := []string{"query", "path", "body", "header", "formData"} 179 180 for _, paramLocation := range locations { 181 rootNode := getNameOnlyDiffNode(strings.Title(paramLocation)) 182 for URLMethod, op2 := range sd.urlMethods2 { 183 if op1, ok := sd.urlMethods1[URLMethod]; ok { 184 185 params1 := getParams(op1.ParentPathItem.Parameters, op1.Operation.Parameters, paramLocation) 186 params2 := getParams(op2.ParentPathItem.Parameters, op2.Operation.Parameters, paramLocation) 187 188 location := DifferenceLocation{URL: URLMethod.Path, Method: URLMethod.Method, Node: rootNode} 189 190 // detect deleted params 191 for paramName1, param1 := range params1 { 192 if _, ok := params2[paramName1]; !ok { 193 childLocation := location.AddNode(getSchemaDiffNode(paramName1, ¶m1.SimpleSchema)) 194 code := DeletedOptionalParam 195 if param1.Required { 196 code = DeletedRequiredParam 197 } 198 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: childLocation, Code: code}) 199 } 200 } 201 // detect added changed params 202 for paramName2, param2 := range params2 { 203 // changed? 204 if param1, ok := params1[paramName2]; ok { 205 sd.compareParams(URLMethod, paramLocation, paramName2, param1, param2) 206 } else { 207 // Added 208 childLocation := location.AddNode(getSchemaDiffNode(paramName2, ¶m2.SimpleSchema)) 209 code := AddedOptionalParam 210 if param2.Required { 211 code = AddedRequiredParam 212 } 213 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: childLocation, Code: code}) 214 } 215 } 216 } 217 } 218 } 219 } 220 221 func (sd *SpecAnalyser) analyseResponseParams() { 222 // Loop through url+methods in spec 2 - check deleted and changed 223 for eachURLMethodFrom2, op2 := range sd.urlMethods2 { 224 225 // present in both specs? Use key from spec 2 to lookup in spec 1 226 if op1, ok := sd.urlMethods1[eachURLMethodFrom2]; ok { 227 // compare responses for url and method 228 op1Responses := op1.Operation.Responses.StatusCodeResponses 229 op2Responses := op2.Operation.Responses.StatusCodeResponses 230 231 // deleted responses 232 for code1 := range op1Responses { 233 if _, ok := op2Responses[code1]; !ok { 234 location := DifferenceLocation{ 235 URL: eachURLMethodFrom2.Path, 236 Method: eachURLMethodFrom2.Method, 237 Response: code1, 238 Node: getNameOnlyDiffNode("NoContent"), 239 } 240 if op1Responses[code1].Schema != nil { 241 location.Node = getSchemaDiffNode("Body", op1Responses[code1].Schema) 242 } 243 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: DeletedResponse}) 244 } 245 } 246 // Added updated Response Codes 247 for code2, op2Response := range op2Responses { 248 if op1Response, ok := op1Responses[code2]; ok { 249 op1Headers := op1Response.ResponseProps.Headers 250 headerRootNode := getNameOnlyDiffNode("Headers") 251 252 // Iterate Spec2 Headers looking for added and updated 253 location := DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: headerRootNode} 254 for op2HeaderName, op2Header := range op2Response.ResponseProps.Headers { 255 if op1Header, ok := op1Headers[op2HeaderName]; ok { 256 diffs := sd.CompareProps(forHeader(op1Header), forHeader(op2Header)) 257 sd.addDiffs(location, diffs) 258 } else { 259 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 260 DifferenceLocation: location.AddNode(getSchemaDiffNode(op2HeaderName, &op2Header.SimpleSchema)), 261 Code: AddedResponseHeader}) 262 } 263 } 264 for op1HeaderName := range op1Response.ResponseProps.Headers { 265 if _, ok := op2Response.ResponseProps.Headers[op1HeaderName]; !ok { 266 op1Header := op1Response.ResponseProps.Headers[op1HeaderName] 267 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 268 DifferenceLocation: location.AddNode(getSchemaDiffNode(op1HeaderName, &op1Header.SimpleSchema)), 269 Code: DeletedResponseHeader}) 270 } 271 } 272 schem := op1Response.Schema 273 node := getNameOnlyDiffNode("NoContent") 274 if schem != nil { 275 node = getSchemaDiffNode("Body", &schem.SchemaProps) 276 } 277 responseLocation := DifferenceLocation{URL: eachURLMethodFrom2.Path, 278 Method: eachURLMethodFrom2.Method, 279 Response: code2, 280 Node: node} 281 sd.compareDescripton(responseLocation, op1Response.Description, op2Response.Description) 282 283 if op1Response.Schema != nil { 284 if op2Response.Schema == nil { 285 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 286 DifferenceLocation: DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: getSchemaDiffNode("Body", op1Response.Schema)}, 287 Code: DeletedProperty}) 288 } else { 289 sd.compareSchema( 290 DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: getSchemaDiffNode("Body", op1Response.Schema)}, 291 op1Response.Schema, 292 op2Response.Schema) 293 } 294 } else if op2Response.Schema != nil { 295 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 296 DifferenceLocation: DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: getSchemaDiffNode("Body", op2Response.Schema)}, 297 Code: AddedProperty}) 298 } 299 300 } else { 301 // op2Response 302 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 303 DifferenceLocation: DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: getSchemaDiffNode("Body", op2Response.Schema)}, 304 Code: AddedResponse}) 305 } 306 } 307 } 308 } 309 } 310 311 func (sd *SpecAnalyser) analyseExtensions(spec1, spec2 *spec.Swagger) { 312 // root 313 specLoc := DifferenceLocation{Node: &Node{Field: "Spec"}} 314 sd.checkAddedExtensions(spec1.Extensions, spec2.Extensions, specLoc, "") 315 sd.checkDeletedExtensions(spec1.Extensions, spec2.Extensions, specLoc, "") 316 sd.checkChangedExtensions(spec1.Extensions, spec2.Extensions, specLoc, "") 317 318 sd.analyzeInfoExtensions() 319 sd.analyzeTagExtensions(spec1, spec2) 320 sd.analyzeSecurityDefinitionExtensions(spec1, spec2) 321 322 sd.analyzeOperationExtensions() 323 } 324 325 func (sd *SpecAnalyser) analyzeOperationExtensions() { 326 pathsIterated := make(map[string]struct{}) 327 for urlMethod, op2 := range sd.urlMethods2 { 328 pathAndMethodLoc := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method} 329 if op1, ok := sd.urlMethods1[urlMethod]; ok { 330 if _, ok := pathsIterated[urlMethod.Path]; !ok { 331 sd.checkAddedExtensions(op1.Extensions, op2.Extensions, DifferenceLocation{URL: urlMethod.Path}, "") 332 sd.checkChangedExtensions(op1.Extensions, op2.Extensions, DifferenceLocation{URL: urlMethod.Path}, "") 333 pathsIterated[urlMethod.Path] = struct{}{} 334 } 335 sd.checkAddedExtensions(op1.Operation.Responses.Extensions, op2.Operation.Responses.Extensions, pathAndMethodLoc, "Responses") 336 sd.checkChangedExtensions(op1.Operation.Responses.Extensions, op2.Operation.Responses.Extensions, pathAndMethodLoc, "Responses") 337 sd.checkAddedExtensions(op1.Operation.Extensions, op2.Operation.Extensions, pathAndMethodLoc, "") 338 sd.checkChangedExtensions(op1.Operation.Extensions, op2.Operation.Extensions, pathAndMethodLoc, "") 339 sd.checkParamExtensions(op1, op2, urlMethod) 340 for code, resp := range op1.Operation.Responses.StatusCodeResponses { 341 for hdr, h := range resp.Headers { 342 op2StatusCode, ok := op2.Operation.Responses.StatusCodeResponses[code] 343 if ok { 344 if _, ok = op2StatusCode.Headers[hdr]; ok { 345 sd.checkAddedExtensions(h.Extensions, op2StatusCode.Headers[hdr].Extensions, DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method, Node: getNameOnlyDiffNode("Headers")}, hdr) 346 sd.checkChangedExtensions(h.Extensions, op2StatusCode.Headers[hdr].Extensions, DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method, Node: getNameOnlyDiffNode("Headers")}, hdr) 347 } 348 } 349 } 350 351 resp2 := op2.Operation.Responses.StatusCodeResponses[code] 352 sd.analyzeSchemaExtensions(resp.Schema, resp2.Schema, code, urlMethod) 353 } 354 355 } 356 } 357 358 pathsIterated = make(map[string]struct{}) 359 for urlMethod, op1 := range sd.urlMethods1 { 360 pathAndMethodLoc := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method} 361 if op2, ok := sd.urlMethods2[urlMethod]; ok { 362 if _, ok := pathsIterated[urlMethod.Path]; !ok { 363 sd.checkDeletedExtensions(op1.Extensions, op2.Extensions, DifferenceLocation{URL: urlMethod.Path}, "") 364 pathsIterated[urlMethod.Path] = struct{}{} 365 } 366 sd.checkDeletedExtensions(op1.Operation.Responses.Extensions, op2.Operation.Responses.Extensions, pathAndMethodLoc, "Responses") 367 sd.checkDeletedExtensions(op1.Operation.Extensions, op2.Operation.Extensions, pathAndMethodLoc, "") 368 for code, resp := range op1.Operation.Responses.StatusCodeResponses { 369 for hdr, h := range resp.Headers { 370 op2StatusCode, ok := op2.Operation.Responses.StatusCodeResponses[code] 371 if ok { 372 if _, ok = op2StatusCode.Headers[hdr]; ok { 373 sd.checkDeletedExtensions(h.Extensions, op2StatusCode.Headers[hdr].Extensions, DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method, Node: getNameOnlyDiffNode("Headers")}, hdr) 374 } 375 } 376 } 377 } 378 } 379 } 380 } 381 382 func (sd *SpecAnalyser) checkParamExtensions(op1 *PathItemOp, op2 *PathItemOp, urlMethod URLMethod) { 383 locations := []string{"query", "path", "body", "header", "formData"} 384 titles := []string{"Query", "Path", "Body", "Header", "FormData"} 385 386 for i, paramLocation := range locations { 387 rootNode := getNameOnlyDiffNode(titles[i]) 388 params1 := getParams(op1.ParentPathItem.Parameters, op1.Operation.Parameters, paramLocation) 389 params2 := getParams(op2.ParentPathItem.Parameters, op2.Operation.Parameters, paramLocation) 390 391 location := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method, Node: rootNode} 392 // detect deleted param extensions 393 for paramName1, param1 := range params1 { 394 if param2, ok := params2[paramName1]; ok { 395 childLocation := location.AddNode(getSchemaDiffNode(paramName1, ¶m1.SimpleSchema)) 396 sd.checkDeletedExtensions(param1.Extensions, param2.Extensions, childLocation, "") 397 } 398 } 399 400 // detect added changed params 401 for paramName2, param2 := range params2 { 402 // changed? 403 if param1, ok := params1[paramName2]; ok { 404 childLocation := location.AddNode(getSchemaDiffNode(paramName2, ¶m1.SimpleSchema)) 405 sd.checkAddedExtensions(param1.Extensions, param2.Extensions, childLocation, "") 406 sd.checkChangedExtensions(param1.Extensions, param2.Extensions, childLocation, "") 407 } 408 } 409 } 410 } 411 412 func (sd *SpecAnalyser) analyzeSecurityDefinitionExtensions(spec1 *spec.Swagger, spec2 *spec.Swagger) { 413 securityDefLoc := DifferenceLocation{Node: &Node{Field: "Security Definitions"}} 414 for key, securityDef1 := range spec1.SecurityDefinitions { 415 if securityDef2, ok := spec2.SecurityDefinitions[key]; ok { 416 sd.checkAddedExtensions(securityDef1.Extensions, securityDef2.Extensions, securityDefLoc, "") 417 sd.checkChangedExtensions(securityDef1.Extensions, securityDef2.Extensions, securityDefLoc, "") 418 } 419 } 420 421 for key, securityDef := range spec2.SecurityDefinitions { 422 if securityDef1, ok := spec1.SecurityDefinitions[key]; ok { 423 sd.checkDeletedExtensions(securityDef1.Extensions, securityDef.Extensions, securityDefLoc, "") 424 } 425 } 426 } 427 428 func (sd *SpecAnalyser) analyzeSchemaExtensions(schema1, schema2 *spec.Schema, code int, urlMethod URLMethod) { 429 if schema1 != nil && schema2 != nil { 430 diffLoc := DifferenceLocation{Response: code, URL: urlMethod.Path, Method: urlMethod.Method, Node: getSchemaDiffNode("Body", schema2)} 431 sd.checkAddedExtensions(schema1.Extensions, schema2.Extensions, diffLoc, "") 432 sd.checkChangedExtensions(schema1.Extensions, schema2.Extensions, diffLoc, "") 433 sd.checkDeletedExtensions(schema1.Extensions, schema2.Extensions, diffLoc, "") 434 if schema1.Items != nil && schema2.Items != nil { 435 sd.analyzeSchemaExtensions(schema1.Items.Schema, schema2.Items.Schema, code, urlMethod) 436 for i := range schema1.Items.Schemas { 437 s1 := schema1.Items.Schemas[i] 438 for j := range schema2.Items.Schemas { 439 s2 := schema2.Items.Schemas[j] 440 sd.analyzeSchemaExtensions(&s1, &s2, code, urlMethod) 441 } 442 } 443 } 444 } 445 } 446 447 func (sd *SpecAnalyser) analyzeInfoExtensions() { 448 if sd.Info1 != nil && sd.Info2 != nil { 449 diffLocation := DifferenceLocation{Node: &Node{Field: "Spec Info"}} 450 sd.checkAddedExtensions(sd.Info1.Extensions, sd.Info2.Extensions, diffLocation, "") 451 sd.checkDeletedExtensions(sd.Info1.Extensions, sd.Info2.Extensions, diffLocation, "") 452 sd.checkChangedExtensions(sd.Info1.Extensions, sd.Info2.Extensions, diffLocation, "") 453 if sd.Info1.Contact != nil && sd.Info2.Contact != nil { 454 diffLocation = DifferenceLocation{Node: &Node{Field: "Spec Info.Contact"}} 455 sd.checkAddedExtensions(sd.Info1.Contact.Extensions, sd.Info2.Contact.Extensions, diffLocation, "") 456 sd.checkDeletedExtensions(sd.Info1.Contact.Extensions, sd.Info2.Contact.Extensions, diffLocation, "") 457 sd.checkChangedExtensions(sd.Info1.Contact.Extensions, sd.Info2.Contact.Extensions, diffLocation, "") 458 } 459 if sd.Info1.License != nil && sd.Info2.License != nil { 460 diffLocation = DifferenceLocation{Node: &Node{Field: "Spec Info.License"}} 461 sd.checkAddedExtensions(sd.Info1.License.Extensions, sd.Info2.License.Extensions, diffLocation, "") 462 sd.checkDeletedExtensions(sd.Info1.License.Extensions, sd.Info2.License.Extensions, diffLocation, "") 463 sd.checkChangedExtensions(sd.Info1.License.Extensions, sd.Info2.License.Extensions, diffLocation, "") 464 } 465 } 466 } 467 468 func (sd *SpecAnalyser) analyzeTagExtensions(spec1 *spec.Swagger, spec2 *spec.Swagger) { 469 diffLocation := DifferenceLocation{Node: &Node{Field: "Spec Tags"}} 470 for _, spec2Tag := range spec2.Tags { 471 for _, spec1Tag := range spec1.Tags { 472 if spec2Tag.Name == spec1Tag.Name { 473 sd.checkAddedExtensions(spec1Tag.Extensions, spec2Tag.Extensions, diffLocation, "") 474 sd.checkChangedExtensions(spec1Tag.Extensions, spec2Tag.Extensions, diffLocation, "") 475 } 476 } 477 } 478 for _, spec1Tag := range spec1.Tags { 479 for _, spec2Tag := range spec2.Tags { 480 if spec1Tag.Name == spec2Tag.Name { 481 sd.checkDeletedExtensions(spec1Tag.Extensions, spec2Tag.Extensions, diffLocation, "") 482 } 483 } 484 } 485 } 486 487 func (sd *SpecAnalyser) checkAddedExtensions(extensions1 spec.Extensions, extensions2 spec.Extensions, diffLocation DifferenceLocation, fieldPrefix string) { 488 for extKey := range extensions2 { 489 if _, ok := extensions1[extKey]; !ok { 490 if fieldPrefix != "" { 491 extKey = fmt.Sprintf("%s.%s", fieldPrefix, extKey) 492 } 493 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 494 DifferenceLocation: diffLocation.AddNode(&Node{Field: extKey}), 495 Code: AddedExtension, 496 Compatibility: Warning, // this could potentially be a breaking change 497 }) 498 } 499 } 500 } 501 502 func (sd *SpecAnalyser) checkChangedExtensions(extensions1 spec.Extensions, extensions2 spec.Extensions, diffLocation DifferenceLocation, fieldPrefix string) { 503 for extKey, ext2Val := range extensions2 { 504 if ext1Val, ok := extensions1[extKey]; ok && !reflect.DeepEqual(ext1Val, ext2Val) { 505 if fieldPrefix != "" { 506 extKey = fmt.Sprintf("%s.%s", fieldPrefix, extKey) 507 } 508 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 509 DifferenceLocation: diffLocation.AddNode(&Node{Field: extKey}), 510 Code: ChangedExtensionValue, 511 Compatibility: Warning, // this could potentially be a breaking change 512 }) 513 } 514 } 515 } 516 517 func (sd *SpecAnalyser) checkDeletedExtensions(extensions1 spec.Extensions, extensions2 spec.Extensions, diffLocation DifferenceLocation, fieldPrefix string) { 518 for extKey := range extensions1 { 519 if _, ok := extensions2[extKey]; !ok { 520 if fieldPrefix != "" { 521 extKey = fmt.Sprintf("%s.%s", fieldPrefix, extKey) 522 } 523 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 524 DifferenceLocation: diffLocation.AddNode(&Node{Field: extKey}), 525 Code: DeletedExtension, 526 Compatibility: Warning, // this could potentially be a breaking change 527 }) 528 } 529 } 530 } 531 532 func addTypeDiff(diffs []TypeDiff, diff TypeDiff) []TypeDiff { 533 if diff.Change != NoChangeDetected { 534 diffs = append(diffs, diff) 535 } 536 return diffs 537 } 538 539 // CompareProps computes type specific property diffs 540 func (sd *SpecAnalyser) CompareProps(type1, type2 *spec.SchemaProps) []TypeDiff { 541 542 diffs := []TypeDiff{} 543 544 diffs = CheckToFromPrimitiveType(diffs, type1, type2) 545 546 if len(diffs) > 0 { 547 return diffs 548 } 549 550 if isArray(type1) { 551 maxItemDiffs := CompareIntValues("MaxItems", type1.MaxItems, type2.MaxItems, WidenedType, NarrowedType) 552 diffs = append(diffs, maxItemDiffs...) 553 minItemsDiff := CompareIntValues("MinItems", type1.MinItems, type2.MinItems, NarrowedType, WidenedType) 554 diffs = append(diffs, minItemsDiff...) 555 } 556 557 if len(diffs) > 0 { 558 return diffs 559 } 560 561 diffs = CheckRefChange(diffs, type1, type2) 562 if len(diffs) > 0 { 563 return diffs 564 } 565 566 if !(isPrimitiveType(type1.Type) && isPrimitiveType(type2.Type)) { 567 return diffs 568 } 569 570 // check primitive type hierarchy change eg string -> integer = NarrowedChange 571 if type1.Type[0] != type2.Type[0] || 572 type1.Format != type2.Format { 573 diff := getTypeHierarchyChange(primitiveTypeString(type1.Type[0], type1.Format), primitiveTypeString(type2.Type[0], type2.Format)) 574 diffs = addTypeDiff(diffs, diff) 575 } 576 577 diffs = CheckStringTypeChanges(diffs, type1, type2) 578 579 if len(diffs) > 0 { 580 return diffs 581 } 582 583 diffs = checkNumericTypeChanges(diffs, type1, type2) 584 585 if len(diffs) > 0 { 586 return diffs 587 } 588 589 return diffs 590 } 591 592 func (sd *SpecAnalyser) compareParams(urlMethod URLMethod, location string, name string, param1, param2 spec.Parameter) { 593 diffLocation := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method} 594 595 childLocation := diffLocation.AddNode(getNameOnlyDiffNode(strings.Title(location))) 596 paramLocation := diffLocation.AddNode(getNameOnlyDiffNode(name)) 597 sd.compareDescripton(paramLocation, param1.Description, param2.Description) 598 599 if param1.Schema != nil && param2.Schema != nil { 600 if len(name) > 0 { 601 childLocation = childLocation.AddNode(getSchemaDiffNode(name, param2.Schema)) 602 } 603 sd.compareSchema(childLocation, param1.Schema, param2.Schema) 604 } 605 606 diffs := sd.CompareProps(forParam(param1), forParam(param2)) 607 608 childLocation = childLocation.AddNode(getSchemaDiffNode(name, ¶m2.SimpleSchema)) 609 if len(diffs) > 0 { 610 sd.addDiffs(childLocation, diffs) 611 } 612 613 diffs = CheckToFromRequired(param1.Required, param2.Required) 614 if len(diffs) > 0 { 615 sd.addDiffs(childLocation, diffs) 616 } 617 618 sd.compareSimpleSchema(childLocation, ¶m1.SimpleSchema, ¶m2.SimpleSchema) 619 } 620 621 func (sd *SpecAnalyser) addTypeDiff(location DifferenceLocation, diff *TypeDiff) { 622 diffCopy := diff 623 desc := diffCopy.Description 624 if len(desc) == 0 { 625 if diffCopy.FromType != diffCopy.ToType { 626 desc = fmt.Sprintf("%s -> %s", diffCopy.FromType, diffCopy.ToType) 627 } 628 } 629 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 630 DifferenceLocation: location, 631 Code: diffCopy.Change, 632 DiffInfo: desc}) 633 } 634 635 func (sd *SpecAnalyser) compareDescripton(location DifferenceLocation, desc1, desc2 string) { 636 if desc1 != desc2 { 637 code := ChangedDescripton 638 if len(desc1) > 0 { 639 code = DeletedDescripton 640 } else if len(desc2) > 0 { 641 code = AddedDescripton 642 } 643 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: code}) 644 } 645 } 646 647 func isPrimitiveType(item spec.StringOrArray) bool { 648 return len(item) > 0 && item[0] != ArrayType && item[0] != ObjectType 649 } 650 651 func isArrayType(item spec.StringOrArray) bool { 652 return len(item) > 0 && item[0] == ArrayType 653 } 654 func (sd *SpecAnalyser) getRefSchemaFromSpec1(ref spec.Ref) (*spec.Schema, string) { 655 return sd.schemaFromRef(ref, &sd.Definitions1) 656 } 657 658 func (sd *SpecAnalyser) getRefSchemaFromSpec2(ref spec.Ref) (*spec.Schema, string) { 659 return sd.schemaFromRef(ref, &sd.Definitions2) 660 } 661 662 // CompareSchemaFn Fn spec for comparing schemas 663 type CompareSchemaFn func(location DifferenceLocation, schema1, schema2 *spec.Schema) 664 665 func (sd *SpecAnalyser) compareSchema(location DifferenceLocation, schema1, schema2 *spec.Schema) { 666 667 refDiffs := []TypeDiff{} 668 refDiffs = CheckRefChange(refDiffs, schema1, schema2) 669 if len(refDiffs) > 0 { 670 for _, d := range refDiffs { 671 diff := d 672 sd.addTypeDiff(location, &diff) 673 } 674 return 675 } 676 677 if isRefType(schema1) { 678 key := schemaLocationKey(location) 679 if _, ok := sd.schemasCompared[key]; ok { 680 return 681 } 682 sd.schemasCompared[key] = struct{}{} 683 schema1, _ = sd.schemaFromRef(getRef(schema1), &sd.Definitions1) 684 } 685 686 if isRefType(schema2) { 687 schema2, _ = sd.schemaFromRef(getRef(schema2), &sd.Definitions2) 688 } 689 690 sd.compareDescripton(location, schema1.Description, schema2.Description) 691 692 typeDiffs := sd.CompareProps(&schema1.SchemaProps, &schema2.SchemaProps) 693 if len(typeDiffs) > 0 { 694 sd.addDiffs(location, typeDiffs) 695 return 696 } 697 698 if isArray(schema1) { 699 if isArray(schema2) { 700 sd.compareSchema(location, schema1.Items.Schema, schema2.Items.Schema) 701 } else { 702 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedType, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 703 } 704 } 705 706 diffs := CompareProperties(location, schema1, schema2, sd.getRefSchemaFromSpec1, sd.getRefSchemaFromSpec2, sd.compareSchema) 707 for _, diff := range diffs { 708 sd.Diffs = sd.Diffs.addDiff(diff) 709 } 710 } 711 712 func (sd *SpecAnalyser) compareSimpleSchema(location DifferenceLocation, schema1, schema2 *spec.SimpleSchema) { 713 // check optional/required 714 if schema1.Nullable != schema2.Nullable { 715 // If optional is made required 716 if schema1.Nullable && !schema2.Nullable { 717 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedOptionalToRequired, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 718 } else if !schema1.Nullable && schema2.Nullable { 719 // If required is made optional 720 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedRequiredToOptional, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 721 } 722 } 723 724 if schema1.CollectionFormat != schema2.CollectionFormat { 725 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedCollectionFormat, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 726 } 727 728 if schema1.Default != schema2.Default { 729 switch { 730 case schema1.Default == nil && schema2.Default != nil: 731 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: AddedDefault, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 732 case schema1.Default != nil && schema2.Default == nil: 733 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: DeletedDefault, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 734 default: 735 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedDefault, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 736 } 737 } 738 739 if schema1.Example != schema2.Example { 740 switch { 741 case schema1.Example == nil && schema2.Example != nil: 742 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: AddedExample, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 743 case schema1.Example != nil && schema2.Example == nil: 744 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: DeletedExample, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 745 default: 746 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedExample, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 747 } 748 } 749 750 if isArray(schema1) { 751 if isArray(schema2) { 752 sd.compareSimpleSchema(location, &schema1.Items.SimpleSchema, &schema2.Items.SimpleSchema) 753 } else { 754 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedType, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 755 } 756 } 757 } 758 759 func (sd *SpecAnalyser) addDiffs(location DifferenceLocation, diffs []TypeDiff) { 760 for _, e := range diffs { 761 eachTypeDiff := e 762 if eachTypeDiff.Change != NoChangeDetected { 763 sd.addTypeDiff(location, &eachTypeDiff) 764 } 765 } 766 } 767 768 func addChildDiffNode(location DifferenceLocation, propName string, propSchema *spec.Schema) DifferenceLocation { 769 newNode := location.Node 770 childNode := fromSchemaProps(propName, &propSchema.SchemaProps) 771 if newNode != nil { 772 newNode = newNode.Copy() 773 newNode.AddLeafNode(&childNode) 774 } else { 775 newNode = &childNode 776 } 777 return DifferenceLocation{ 778 URL: location.URL, 779 Method: location.Method, 780 Response: location.Response, 781 Node: newNode, 782 } 783 } 784 785 func fromSchemaProps(fieldName string, props *spec.SchemaProps) Node { 786 node := Node{} 787 node.TypeName, node.IsArray = getSchemaType(props) 788 node.Field = fieldName 789 return node 790 } 791 792 func (sd *SpecAnalyser) findAddedEndpoints() { 793 for URLMethod := range sd.urlMethods2 { 794 if _, ok := sd.urlMethods1[URLMethod]; !ok { 795 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{URL: URLMethod.Path, Method: URLMethod.Method}, Code: AddedEndpoint}) 796 } 797 } 798 } 799 800 func (sd *SpecAnalyser) findDeletedEndpoints() { 801 for eachURLMethod, operation1 := range sd.urlMethods1 { 802 code := DeletedEndpoint 803 if (operation1.ParentPathItem.Options != nil && operation1.ParentPathItem.Options.Deprecated) || 804 (operation1.Operation.Deprecated) { 805 code = DeletedDeprecatedEndpoint 806 } 807 if _, ok := sd.urlMethods2[eachURLMethod]; !ok { 808 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{URL: eachURLMethod.Path, Method: eachURLMethod.Method}, Code: code}) 809 } 810 } 811 } 812 813 func (sd *SpecAnalyser) analyseMetaDataProperty(item1, item2 string, codeIfDiff SpecChangeCode, compatIfDiff Compatibility) { 814 if item1 != item2 { 815 diffSpec := fmt.Sprintf("%s -> %s", item1, item2) 816 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{Node: &Node{Field: "Spec Metadata"}}, Code: codeIfDiff, Compatibility: compatIfDiff, DiffInfo: diffSpec}) 817 } 818 } 819 820 func (sd *SpecAnalyser) schemaFromRef(ref spec.Ref, defns *spec.Definitions) (actualSchema *spec.Schema, definitionName string) { 821 definitionName = definitionFromRef(ref) 822 foundSchema, ok := (*defns)[definitionName] 823 if !ok { 824 return nil, definitionName 825 } 826 sd.ReferencedDefinitions[definitionName] = true 827 actualSchema = &foundSchema 828 return 829 830 } 831 832 func schemaLocationKey(location DifferenceLocation) string { 833 k := location.Method + location.URL + location.Node.Field + location.Node.TypeName 834 if location.Node.ChildNode != nil && location.Node.ChildNode.IsArray { 835 k += location.Node.ChildNode.Field + location.Node.ChildNode.TypeName 836 } 837 return k 838 } 839 840 // PropertyDefn combines a property with its required-ness 841 type PropertyDefn struct { 842 Schema *spec.Schema 843 Required bool 844 } 845 846 // PropertyMap a unified map including all AllOf fields 847 type PropertyMap map[string]PropertyDefn