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