github.com/thetreep/go-swagger@v0.0.0-20240223100711-35af64f14f01/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 sd.compareSchema( 285 DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: getSchemaDiffNode("Body", op1Response.Schema)}, 286 op1Response.Schema, 287 op2Response.Schema) 288 } 289 } else { 290 // op2Response 291 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 292 DifferenceLocation: DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: getSchemaDiffNode("Body", op2Response.Schema)}, 293 Code: AddedResponse}) 294 } 295 } 296 } 297 } 298 } 299 300 func (sd *SpecAnalyser) analyseExtensions(spec1, spec2 *spec.Swagger) { 301 // root 302 specLoc := DifferenceLocation{Node: &Node{Field: "Spec"}} 303 sd.checkAddedExtensions(spec1.Extensions, spec2.Extensions, specLoc, "") 304 sd.checkDeletedExtensions(spec1.Extensions, spec2.Extensions, specLoc, "") 305 sd.checkChangedExtensions(spec1.Extensions, spec2.Extensions, specLoc, "") 306 307 sd.analyzeInfoExtensions() 308 sd.analyzeTagExtensions(spec1, spec2) 309 sd.analyzeSecurityDefinitionExtensions(spec1, spec2) 310 311 sd.analyzeOperationExtensions() 312 } 313 314 func (sd *SpecAnalyser) analyzeOperationExtensions() { 315 pathsIterated := make(map[string]struct{}) 316 for urlMethod, op2 := range sd.urlMethods2 { 317 pathAndMethodLoc := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method} 318 if op1, ok := sd.urlMethods1[urlMethod]; ok { 319 if _, ok := pathsIterated[urlMethod.Path]; !ok { 320 sd.checkAddedExtensions(op1.Extensions, op2.Extensions, DifferenceLocation{URL: urlMethod.Path}, "") 321 sd.checkChangedExtensions(op1.Extensions, op2.Extensions, DifferenceLocation{URL: urlMethod.Path}, "") 322 pathsIterated[urlMethod.Path] = struct{}{} 323 } 324 sd.checkAddedExtensions(op1.Operation.Responses.Extensions, op2.Operation.Responses.Extensions, pathAndMethodLoc, "Responses") 325 sd.checkChangedExtensions(op1.Operation.Responses.Extensions, op2.Operation.Responses.Extensions, pathAndMethodLoc, "Responses") 326 sd.checkAddedExtensions(op1.Operation.Extensions, op2.Operation.Extensions, pathAndMethodLoc, "") 327 sd.checkChangedExtensions(op1.Operation.Extensions, op2.Operation.Extensions, pathAndMethodLoc, "") 328 sd.checkParamExtensions(op1, op2, urlMethod) 329 for code, resp := range op1.Operation.Responses.StatusCodeResponses { 330 for hdr, h := range resp.Headers { 331 op2StatusCode, ok := op2.Operation.Responses.StatusCodeResponses[code] 332 if ok { 333 if _, ok = op2StatusCode.Headers[hdr]; ok { 334 sd.checkAddedExtensions(h.Extensions, op2StatusCode.Headers[hdr].Extensions, DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method, Node: getNameOnlyDiffNode("Headers")}, hdr) 335 sd.checkChangedExtensions(h.Extensions, op2StatusCode.Headers[hdr].Extensions, DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method, Node: getNameOnlyDiffNode("Headers")}, hdr) 336 } 337 } 338 } 339 340 resp2 := op2.Operation.Responses.StatusCodeResponses[code] 341 sd.analyzeSchemaExtensions(resp.Schema, resp2.Schema, code, urlMethod) 342 } 343 344 } 345 } 346 347 pathsIterated = make(map[string]struct{}) 348 for urlMethod, op1 := range sd.urlMethods1 { 349 pathAndMethodLoc := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method} 350 if op2, ok := sd.urlMethods2[urlMethod]; ok { 351 if _, ok := pathsIterated[urlMethod.Path]; !ok { 352 sd.checkDeletedExtensions(op1.Extensions, op2.Extensions, DifferenceLocation{URL: urlMethod.Path}, "") 353 pathsIterated[urlMethod.Path] = struct{}{} 354 } 355 sd.checkDeletedExtensions(op1.Operation.Responses.Extensions, op2.Operation.Responses.Extensions, pathAndMethodLoc, "Responses") 356 sd.checkDeletedExtensions(op1.Operation.Extensions, op2.Operation.Extensions, pathAndMethodLoc, "") 357 for code, resp := range op1.Operation.Responses.StatusCodeResponses { 358 for hdr, h := range resp.Headers { 359 op2StatusCode, ok := op2.Operation.Responses.StatusCodeResponses[code] 360 if ok { 361 if _, ok = op2StatusCode.Headers[hdr]; ok { 362 sd.checkDeletedExtensions(h.Extensions, op2StatusCode.Headers[hdr].Extensions, DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method, Node: getNameOnlyDiffNode("Headers")}, hdr) 363 } 364 } 365 } 366 } 367 } 368 } 369 } 370 371 func (sd *SpecAnalyser) checkParamExtensions(op1 *PathItemOp, op2 *PathItemOp, urlMethod URLMethod) { 372 locations := []string{"query", "path", "body", "header", "formData"} 373 titles := []string{"Query", "Path", "Body", "Header", "FormData"} 374 375 for i, paramLocation := range locations { 376 rootNode := getNameOnlyDiffNode(titles[i]) 377 params1 := getParams(op1.ParentPathItem.Parameters, op1.Operation.Parameters, paramLocation) 378 params2 := getParams(op2.ParentPathItem.Parameters, op2.Operation.Parameters, paramLocation) 379 380 location := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method, Node: rootNode} 381 // detect deleted param extensions 382 for paramName1, param1 := range params1 { 383 if param2, ok := params2[paramName1]; ok { 384 childLocation := location.AddNode(getSchemaDiffNode(paramName1, ¶m1.SimpleSchema)) 385 sd.checkDeletedExtensions(param1.Extensions, param2.Extensions, childLocation, "") 386 } 387 } 388 389 // detect added changed params 390 for paramName2, param2 := range params2 { 391 // changed? 392 if param1, ok := params1[paramName2]; ok { 393 childLocation := location.AddNode(getSchemaDiffNode(paramName2, ¶m1.SimpleSchema)) 394 sd.checkAddedExtensions(param1.Extensions, param2.Extensions, childLocation, "") 395 sd.checkChangedExtensions(param1.Extensions, param2.Extensions, childLocation, "") 396 } 397 } 398 } 399 } 400 401 func (sd *SpecAnalyser) analyzeSecurityDefinitionExtensions(spec1 *spec.Swagger, spec2 *spec.Swagger) { 402 securityDefLoc := DifferenceLocation{Node: &Node{Field: "Security Definitions"}} 403 for key, securityDef1 := range spec1.SecurityDefinitions { 404 if securityDef2, ok := spec2.SecurityDefinitions[key]; ok { 405 sd.checkAddedExtensions(securityDef1.Extensions, securityDef2.Extensions, securityDefLoc, "") 406 sd.checkChangedExtensions(securityDef1.Extensions, securityDef2.Extensions, securityDefLoc, "") 407 } 408 } 409 410 for key, securityDef := range spec2.SecurityDefinitions { 411 if securityDef1, ok := spec1.SecurityDefinitions[key]; ok { 412 sd.checkDeletedExtensions(securityDef1.Extensions, securityDef.Extensions, securityDefLoc, "") 413 } 414 } 415 } 416 417 func (sd *SpecAnalyser) analyzeSchemaExtensions(schema1, schema2 *spec.Schema, code int, urlMethod URLMethod) { 418 if schema1 != nil && schema2 != nil { 419 diffLoc := DifferenceLocation{Response: code, URL: urlMethod.Path, Method: urlMethod.Method, Node: getSchemaDiffNode("Body", schema2)} 420 sd.checkAddedExtensions(schema1.Extensions, schema2.Extensions, diffLoc, "") 421 sd.checkChangedExtensions(schema1.Extensions, schema2.Extensions, diffLoc, "") 422 sd.checkDeletedExtensions(schema1.Extensions, schema2.Extensions, diffLoc, "") 423 if schema1.Items != nil && schema2.Items != nil { 424 sd.analyzeSchemaExtensions(schema1.Items.Schema, schema2.Items.Schema, code, urlMethod) 425 for i := range schema1.Items.Schemas { 426 s1 := schema1.Items.Schemas[i] 427 for j := range schema2.Items.Schemas { 428 s2 := schema2.Items.Schemas[j] 429 sd.analyzeSchemaExtensions(&s1, &s2, code, urlMethod) 430 } 431 } 432 } 433 } 434 } 435 436 func (sd *SpecAnalyser) analyzeInfoExtensions() { 437 if sd.Info1 != nil && sd.Info2 != nil { 438 diffLocation := DifferenceLocation{Node: &Node{Field: "Spec Info"}} 439 sd.checkAddedExtensions(sd.Info1.Extensions, sd.Info2.Extensions, diffLocation, "") 440 sd.checkDeletedExtensions(sd.Info1.Extensions, sd.Info2.Extensions, diffLocation, "") 441 sd.checkChangedExtensions(sd.Info1.Extensions, sd.Info2.Extensions, diffLocation, "") 442 if sd.Info1.Contact != nil && sd.Info2.Contact != nil { 443 diffLocation = DifferenceLocation{Node: &Node{Field: "Spec Info.Contact"}} 444 sd.checkAddedExtensions(sd.Info1.Contact.Extensions, sd.Info2.Contact.Extensions, diffLocation, "") 445 sd.checkDeletedExtensions(sd.Info1.Contact.Extensions, sd.Info2.Contact.Extensions, diffLocation, "") 446 sd.checkChangedExtensions(sd.Info1.Contact.Extensions, sd.Info2.Contact.Extensions, diffLocation, "") 447 } 448 if sd.Info1.License != nil && sd.Info2.License != nil { 449 diffLocation = DifferenceLocation{Node: &Node{Field: "Spec Info.License"}} 450 sd.checkAddedExtensions(sd.Info1.License.Extensions, sd.Info2.License.Extensions, diffLocation, "") 451 sd.checkDeletedExtensions(sd.Info1.License.Extensions, sd.Info2.License.Extensions, diffLocation, "") 452 sd.checkChangedExtensions(sd.Info1.License.Extensions, sd.Info2.License.Extensions, diffLocation, "") 453 } 454 } 455 } 456 457 func (sd *SpecAnalyser) analyzeTagExtensions(spec1 *spec.Swagger, spec2 *spec.Swagger) { 458 diffLocation := DifferenceLocation{Node: &Node{Field: "Spec Tags"}} 459 for _, spec2Tag := range spec2.Tags { 460 for _, spec1Tag := range spec1.Tags { 461 if spec2Tag.Name == spec1Tag.Name { 462 sd.checkAddedExtensions(spec1Tag.Extensions, spec2Tag.Extensions, diffLocation, "") 463 sd.checkChangedExtensions(spec1Tag.Extensions, spec2Tag.Extensions, diffLocation, "") 464 } 465 } 466 } 467 for _, spec1Tag := range spec1.Tags { 468 for _, spec2Tag := range spec2.Tags { 469 if spec1Tag.Name == spec2Tag.Name { 470 sd.checkDeletedExtensions(spec1Tag.Extensions, spec2Tag.Extensions, diffLocation, "") 471 } 472 } 473 } 474 } 475 476 func (sd *SpecAnalyser) checkAddedExtensions(extensions1 spec.Extensions, extensions2 spec.Extensions, diffLocation DifferenceLocation, fieldPrefix string) { 477 for extKey := range extensions2 { 478 if _, ok := extensions1[extKey]; !ok { 479 if fieldPrefix != "" { 480 extKey = fmt.Sprintf("%s.%s", fieldPrefix, extKey) 481 } 482 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 483 DifferenceLocation: diffLocation.AddNode(&Node{Field: extKey}), 484 Code: AddedExtension, 485 Compatibility: Warning, // this could potentially be a breaking change 486 }) 487 } 488 } 489 } 490 491 func (sd *SpecAnalyser) checkChangedExtensions(extensions1 spec.Extensions, extensions2 spec.Extensions, diffLocation DifferenceLocation, fieldPrefix string) { 492 for extKey, ext2Val := range extensions2 { 493 if ext1Val, ok := extensions1[extKey]; ok && !reflect.DeepEqual(ext1Val, ext2Val) { 494 if fieldPrefix != "" { 495 extKey = fmt.Sprintf("%s.%s", fieldPrefix, extKey) 496 } 497 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 498 DifferenceLocation: diffLocation.AddNode(&Node{Field: extKey}), 499 Code: ChangedExtensionValue, 500 Compatibility: Warning, // this could potentially be a breaking change 501 }) 502 } 503 } 504 } 505 506 func (sd *SpecAnalyser) checkDeletedExtensions(extensions1 spec.Extensions, extensions2 spec.Extensions, diffLocation DifferenceLocation, fieldPrefix string) { 507 for extKey := range extensions1 { 508 if _, ok := extensions2[extKey]; !ok { 509 if fieldPrefix != "" { 510 extKey = fmt.Sprintf("%s.%s", fieldPrefix, extKey) 511 } 512 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 513 DifferenceLocation: diffLocation.AddNode(&Node{Field: extKey}), 514 Code: DeletedExtension, 515 Compatibility: Warning, // this could potentially be a breaking change 516 }) 517 } 518 } 519 } 520 521 func addTypeDiff(diffs []TypeDiff, diff TypeDiff) []TypeDiff { 522 if diff.Change != NoChangeDetected { 523 diffs = append(diffs, diff) 524 } 525 return diffs 526 } 527 528 // CompareProps computes type specific property diffs 529 func (sd *SpecAnalyser) CompareProps(type1, type2 *spec.SchemaProps) []TypeDiff { 530 531 diffs := []TypeDiff{} 532 533 diffs = CheckToFromPrimitiveType(diffs, type1, type2) 534 535 if len(diffs) > 0 { 536 return diffs 537 } 538 539 if isArray(type1) { 540 maxItemDiffs := CompareIntValues("MaxItems", type1.MaxItems, type2.MaxItems, WidenedType, NarrowedType) 541 diffs = append(diffs, maxItemDiffs...) 542 minItemsDiff := CompareIntValues("MinItems", type1.MinItems, type2.MinItems, NarrowedType, WidenedType) 543 diffs = append(diffs, minItemsDiff...) 544 } 545 546 if len(diffs) > 0 { 547 return diffs 548 } 549 550 diffs = CheckRefChange(diffs, type1, type2) 551 if len(diffs) > 0 { 552 return diffs 553 } 554 555 if !(isPrimitiveType(type1.Type) && isPrimitiveType(type2.Type)) { 556 return diffs 557 } 558 559 // check primitive type hierarchy change eg string -> integer = NarrowedChange 560 if type1.Type[0] != type2.Type[0] || 561 type1.Format != type2.Format { 562 diff := getTypeHierarchyChange(primitiveTypeString(type1.Type[0], type1.Format), primitiveTypeString(type2.Type[0], type2.Format)) 563 diffs = addTypeDiff(diffs, diff) 564 } 565 566 diffs = CheckStringTypeChanges(diffs, type1, type2) 567 568 if len(diffs) > 0 { 569 return diffs 570 } 571 572 diffs = checkNumericTypeChanges(diffs, type1, type2) 573 574 if len(diffs) > 0 { 575 return diffs 576 } 577 578 return diffs 579 } 580 581 func (sd *SpecAnalyser) compareParams(urlMethod URLMethod, location string, name string, param1, param2 spec.Parameter) { 582 diffLocation := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method} 583 584 childLocation := diffLocation.AddNode(getNameOnlyDiffNode(strings.Title(location))) 585 paramLocation := diffLocation.AddNode(getNameOnlyDiffNode(name)) 586 sd.compareDescripton(paramLocation, param1.Description, param2.Description) 587 588 if param1.Schema != nil && param2.Schema != nil { 589 if len(name) > 0 { 590 childLocation = childLocation.AddNode(getSchemaDiffNode(name, param2.Schema)) 591 } 592 sd.compareSchema(childLocation, param1.Schema, param2.Schema) 593 } 594 595 diffs := sd.CompareProps(forParam(param1), forParam(param2)) 596 597 childLocation = childLocation.AddNode(getSchemaDiffNode(name, ¶m2.SimpleSchema)) 598 if len(diffs) > 0 { 599 sd.addDiffs(childLocation, diffs) 600 } 601 602 diffs = CheckToFromRequired(param1.Required, param2.Required) 603 if len(diffs) > 0 { 604 sd.addDiffs(childLocation, diffs) 605 } 606 607 sd.compareSimpleSchema(childLocation, ¶m1.SimpleSchema, ¶m2.SimpleSchema) 608 } 609 610 func (sd *SpecAnalyser) addTypeDiff(location DifferenceLocation, diff *TypeDiff) { 611 diffCopy := diff 612 desc := diffCopy.Description 613 if len(desc) == 0 { 614 if diffCopy.FromType != diffCopy.ToType { 615 desc = fmt.Sprintf("%s -> %s", diffCopy.FromType, diffCopy.ToType) 616 } 617 } 618 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 619 DifferenceLocation: location, 620 Code: diffCopy.Change, 621 DiffInfo: desc}) 622 } 623 624 func (sd *SpecAnalyser) compareDescripton(location DifferenceLocation, desc1, desc2 string) { 625 if desc1 != desc2 { 626 code := ChangedDescripton 627 if len(desc1) > 0 { 628 code = DeletedDescripton 629 } else if len(desc2) > 0 { 630 code = AddedDescripton 631 } 632 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: code}) 633 } 634 } 635 636 func isPrimitiveType(item spec.StringOrArray) bool { 637 return len(item) > 0 && item[0] != ArrayType && item[0] != ObjectType 638 } 639 640 func isArrayType(item spec.StringOrArray) bool { 641 return len(item) > 0 && item[0] == ArrayType 642 } 643 func (sd *SpecAnalyser) getRefSchemaFromSpec1(ref spec.Ref) (*spec.Schema, string) { 644 return sd.schemaFromRef(ref, &sd.Definitions1) 645 } 646 647 func (sd *SpecAnalyser) getRefSchemaFromSpec2(ref spec.Ref) (*spec.Schema, string) { 648 return sd.schemaFromRef(ref, &sd.Definitions2) 649 } 650 651 // CompareSchemaFn Fn spec for comparing schemas 652 type CompareSchemaFn func(location DifferenceLocation, schema1, schema2 *spec.Schema) 653 654 func (sd *SpecAnalyser) compareSchema(location DifferenceLocation, schema1, schema2 *spec.Schema) { 655 656 refDiffs := []TypeDiff{} 657 refDiffs = CheckRefChange(refDiffs, schema1, schema2) 658 if len(refDiffs) > 0 { 659 for _, d := range refDiffs { 660 diff := d 661 sd.addTypeDiff(location, &diff) 662 } 663 return 664 } 665 666 if isRefType(schema1) { 667 key := schemaLocationKey(location) 668 if _, ok := sd.schemasCompared[key]; ok { 669 return 670 } 671 sd.schemasCompared[key] = struct{}{} 672 schema1, _ = sd.schemaFromRef(getRef(schema1), &sd.Definitions1) 673 } 674 675 if isRefType(schema2) { 676 schema2, _ = sd.schemaFromRef(getRef(schema2), &sd.Definitions2) 677 } 678 679 sd.compareDescripton(location, schema1.Description, schema2.Description) 680 681 typeDiffs := sd.CompareProps(&schema1.SchemaProps, &schema2.SchemaProps) 682 if len(typeDiffs) > 0 { 683 sd.addDiffs(location, typeDiffs) 684 return 685 } 686 687 if isArray(schema1) { 688 if isArray(schema2) { 689 sd.compareSchema(location, schema1.Items.Schema, schema2.Items.Schema) 690 } else { 691 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedType, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 692 } 693 } 694 695 diffs := CompareProperties(location, schema1, schema2, sd.getRefSchemaFromSpec1, sd.getRefSchemaFromSpec2, sd.compareSchema) 696 for _, diff := range diffs { 697 sd.Diffs = sd.Diffs.addDiff(diff) 698 } 699 } 700 701 func (sd *SpecAnalyser) compareSimpleSchema(location DifferenceLocation, schema1, schema2 *spec.SimpleSchema) { 702 // check optional/required 703 if schema1.Nullable != schema2.Nullable { 704 // If optional is made required 705 if schema1.Nullable && !schema2.Nullable { 706 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedOptionalToRequired, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 707 } else if !schema1.Nullable && schema2.Nullable { 708 // If required is made optional 709 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedRequiredToOptional, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 710 } 711 } 712 713 if schema1.CollectionFormat != schema2.CollectionFormat { 714 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedCollectionFormat, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 715 } 716 717 if schema1.Default != schema2.Default { 718 switch { 719 case schema1.Default == nil && schema2.Default != nil: 720 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: AddedDefault, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 721 case schema1.Default != nil && schema2.Default == nil: 722 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: DeletedDefault, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 723 default: 724 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedDefault, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 725 } 726 } 727 728 if schema1.Example != schema2.Example { 729 switch { 730 case schema1.Example == nil && schema2.Example != nil: 731 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: AddedExample, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 732 case schema1.Example != nil && schema2.Example == nil: 733 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: DeletedExample, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 734 default: 735 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedExample, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 736 } 737 } 738 739 if isArray(schema1) { 740 if isArray(schema2) { 741 sd.compareSimpleSchema(location, &schema1.Items.SimpleSchema, &schema2.Items.SimpleSchema) 742 } else { 743 sd.addDiffs(location, addTypeDiff([]TypeDiff{}, TypeDiff{Change: ChangedType, FromType: getSchemaTypeStr(schema1), ToType: getSchemaTypeStr(schema2)})) 744 } 745 } 746 } 747 748 func (sd *SpecAnalyser) addDiffs(location DifferenceLocation, diffs []TypeDiff) { 749 for _, e := range diffs { 750 eachTypeDiff := e 751 if eachTypeDiff.Change != NoChangeDetected { 752 sd.addTypeDiff(location, &eachTypeDiff) 753 } 754 } 755 } 756 757 func addChildDiffNode(location DifferenceLocation, propName string, propSchema *spec.Schema) DifferenceLocation { 758 newNode := location.Node 759 childNode := fromSchemaProps(propName, &propSchema.SchemaProps) 760 if newNode != nil { 761 newNode = newNode.Copy() 762 newNode.AddLeafNode(&childNode) 763 } else { 764 newNode = &childNode 765 } 766 return DifferenceLocation{ 767 URL: location.URL, 768 Method: location.Method, 769 Response: location.Response, 770 Node: newNode, 771 } 772 } 773 774 func fromSchemaProps(fieldName string, props *spec.SchemaProps) Node { 775 node := Node{} 776 node.TypeName, node.IsArray = getSchemaType(props) 777 node.Field = fieldName 778 return node 779 } 780 781 func (sd *SpecAnalyser) findAddedEndpoints() { 782 for URLMethod := range sd.urlMethods2 { 783 if _, ok := sd.urlMethods1[URLMethod]; !ok { 784 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{URL: URLMethod.Path, Method: URLMethod.Method}, Code: AddedEndpoint}) 785 } 786 } 787 } 788 789 func (sd *SpecAnalyser) findDeletedEndpoints() { 790 for eachURLMethod, operation1 := range sd.urlMethods1 { 791 code := DeletedEndpoint 792 if (operation1.ParentPathItem.Options != nil && operation1.ParentPathItem.Options.Deprecated) || 793 (operation1.Operation.Deprecated) { 794 code = DeletedDeprecatedEndpoint 795 } 796 if _, ok := sd.urlMethods2[eachURLMethod]; !ok { 797 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{URL: eachURLMethod.Path, Method: eachURLMethod.Method}, Code: code}) 798 } 799 } 800 } 801 802 func (sd *SpecAnalyser) analyseMetaDataProperty(item1, item2 string, codeIfDiff SpecChangeCode, compatIfDiff Compatibility) { 803 if item1 != item2 { 804 diffSpec := fmt.Sprintf("%s -> %s", item1, item2) 805 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{Node: &Node{Field: "Spec Metadata"}}, Code: codeIfDiff, Compatibility: compatIfDiff, DiffInfo: diffSpec}) 806 } 807 } 808 809 func (sd *SpecAnalyser) schemaFromRef(ref spec.Ref, defns *spec.Definitions) (actualSchema *spec.Schema, definitionName string) { 810 definitionName = definitionFromRef(ref) 811 foundSchema, ok := (*defns)[definitionName] 812 if !ok { 813 return nil, definitionName 814 } 815 sd.ReferencedDefinitions[definitionName] = true 816 actualSchema = &foundSchema 817 return 818 819 } 820 821 func schemaLocationKey(location DifferenceLocation) string { 822 k := location.Method + location.URL + location.Node.Field + location.Node.TypeName 823 if location.Node.ChildNode != nil && location.Node.ChildNode.IsArray { 824 k += location.Node.ChildNode.Field + location.Node.ChildNode.TypeName 825 } 826 return k 827 } 828 829 // PropertyDefn combines a property with its required-ness 830 type PropertyDefn struct { 831 Schema *spec.Schema 832 Required bool 833 } 834 835 // PropertyMap a unified map including all AllOf fields 836 type PropertyMap map[string]PropertyDefn