github.com/jamescostian/go-swagger@v0.30.4-0.20221130163922-68364d6b567b/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 ReferencedDefinitions map[string]bool 36 37 schemasCompared map[string]struct{} 38 } 39 40 // NewSpecAnalyser returns an empty SpecDiffs 41 func NewSpecAnalyser() *SpecAnalyser { 42 return &SpecAnalyser{ 43 Diffs: SpecDifferences{}, 44 ReferencedDefinitions: map[string]bool{}, 45 } 46 } 47 48 // Analyse the differences in two specs 49 func (sd *SpecAnalyser) Analyse(spec1, spec2 *spec.Swagger) error { 50 sd.schemasCompared = make(map[string]struct{}) 51 sd.Definitions1 = spec1.Definitions 52 sd.Definitions2 = spec2.Definitions 53 sd.urlMethods1 = getURLMethodsFor(spec1) 54 sd.urlMethods2 = getURLMethodsFor(spec2) 55 56 sd.analyseSpecMetadata(spec1, spec2) 57 sd.analyseEndpoints() 58 sd.analyseRequestParams() 59 sd.analyseEndpointData() 60 sd.analyseResponseParams() 61 sd.AnalyseDefinitions() 62 63 return nil 64 } 65 66 func (sd *SpecAnalyser) analyseSpecMetadata(spec1, spec2 *spec.Swagger) { 67 // breaking if it no longer consumes any formats 68 added, deleted, _ := fromStringArray(spec1.Consumes).DiffsTo(spec2.Consumes) 69 70 node := getNameOnlyDiffNode("Spec") 71 location := DifferenceLocation{Node: node} 72 consumesLoation := location.AddNode(getNameOnlyDiffNode("consumes")) 73 74 for _, eachAdded := range added { 75 sd.Diffs = sd.Diffs.addDiff( 76 SpecDifference{DifferenceLocation: consumesLoation, Code: AddedConsumesFormat, Compatibility: NonBreaking, DiffInfo: eachAdded}) 77 } 78 for _, eachDeleted := range deleted { 79 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: consumesLoation, Code: DeletedConsumesFormat, Compatibility: Breaking, DiffInfo: eachDeleted}) 80 } 81 82 // // breaking if it no longer produces any formats 83 added, deleted, _ = fromStringArray(spec1.Produces).DiffsTo(spec2.Produces) 84 producesLocation := location.AddNode(getNameOnlyDiffNode("produces")) 85 for _, eachAdded := range added { 86 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: producesLocation, Code: AddedProducesFormat, Compatibility: NonBreaking, DiffInfo: eachAdded}) 87 } 88 for _, eachDeleted := range deleted { 89 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: producesLocation, Code: DeletedProducesFormat, Compatibility: Breaking, DiffInfo: eachDeleted}) 90 } 91 92 // // breaking if it no longer supports a scheme 93 added, deleted, _ = fromStringArray(spec1.Schemes).DiffsTo(spec2.Schemes) 94 schemesLocation := location.AddNode(getNameOnlyDiffNode("schemes")) 95 96 for _, eachAdded := range added { 97 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: schemesLocation, Code: AddedSchemes, Compatibility: NonBreaking, DiffInfo: eachAdded}) 98 } 99 for _, eachDeleted := range deleted { 100 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: schemesLocation, Code: DeletedSchemes, Compatibility: Breaking, DiffInfo: eachDeleted}) 101 } 102 103 // host should be able to change without any issues? 104 sd.analyseMetaDataProperty(spec1.Info.Description, spec2.Info.Description, ChangedDescripton, NonBreaking) 105 106 // // host should be able to change without any issues? 107 sd.analyseMetaDataProperty(spec1.Host, spec2.Host, ChangedHostURL, Breaking) 108 // sd.Host = compareStrings(spec1.Host, spec2.Host) 109 110 // // Base Path change will break non generated clients 111 sd.analyseMetaDataProperty(spec1.BasePath, spec2.BasePath, ChangedBasePath, Breaking) 112 113 // TODO: what to do about security? 114 // Missing security scheme will break a client 115 // Security []map[string][]string `json:"security,omitempty"` 116 // Tags []Tag `json:"tags,omitempty"` 117 // ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty"` 118 } 119 120 func (sd *SpecAnalyser) analyseEndpoints() { 121 sd.findDeletedEndpoints() 122 sd.findAddedEndpoints() 123 } 124 125 // AnalyseDefinitions check for changes to definition objects not referenced in any endpoint 126 func (sd *SpecAnalyser) AnalyseDefinitions() { 127 alreadyReferenced := map[string]bool{} 128 for k := range sd.ReferencedDefinitions { 129 alreadyReferenced[k] = true 130 } 131 location := DifferenceLocation{Node: &Node{Field: "Spec Definitions"}} 132 for name1, sch := range sd.Definitions1 { 133 schema1 := sch 134 if _, ok := alreadyReferenced[name1]; !ok { 135 childLocation := location.AddNode(&Node{Field: name1}) 136 if schema2, ok := sd.Definitions2[name1]; ok { 137 sd.compareSchema(childLocation, &schema1, &schema2) 138 } else { 139 sd.addDiffs(childLocation, []TypeDiff{{Change: DeletedDefinition}}) 140 } 141 } 142 } 143 for name2 := range sd.Definitions2 { 144 if _, ok := sd.Definitions1[name2]; !ok { 145 childLocation := location.AddNode(&Node{Field: name2}) 146 sd.addDiffs(childLocation, []TypeDiff{{Change: AddedDefinition}}) 147 } 148 } 149 } 150 151 func (sd *SpecAnalyser) analyseEndpointData() { 152 153 for URLMethod, op2 := range sd.urlMethods2 { 154 if op1, ok := sd.urlMethods1[URLMethod]; ok { 155 addedTags, deletedTags, _ := fromStringArray(op1.Operation.Tags).DiffsTo(op2.Operation.Tags) 156 location := DifferenceLocation{URL: URLMethod.Path, Method: URLMethod.Method} 157 158 for _, eachAddedTag := range addedTags { 159 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: AddedTag, DiffInfo: fmt.Sprintf(`"%s"`, eachAddedTag)}) 160 } 161 for _, eachDeletedTag := range deletedTags { 162 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: DeletedTag, DiffInfo: fmt.Sprintf(`"%s"`, eachDeletedTag)}) 163 } 164 165 sd.compareDescripton(location, op1.Operation.Description, op2.Operation.Description) 166 167 } 168 } 169 } 170 171 func (sd *SpecAnalyser) analyseRequestParams() { 172 locations := []string{"query", "path", "body", "header"} 173 174 for _, paramLocation := range locations { 175 rootNode := getNameOnlyDiffNode(strings.Title(paramLocation)) 176 for URLMethod, op2 := range sd.urlMethods2 { 177 if op1, ok := sd.urlMethods1[URLMethod]; ok { 178 179 params1 := getParams(op1.ParentPathItem.Parameters, op1.Operation.Parameters, paramLocation) 180 params2 := getParams(op2.ParentPathItem.Parameters, op2.Operation.Parameters, paramLocation) 181 182 location := DifferenceLocation{URL: URLMethod.Path, Method: URLMethod.Method, Node: rootNode} 183 184 // detect deleted params 185 for paramName1, param1 := range params1 { 186 if _, ok := params2[paramName1]; !ok { 187 childLocation := location.AddNode(getSchemaDiffNode(paramName1, ¶m1.SimpleSchema)) 188 code := DeletedOptionalParam 189 if param1.Required { 190 code = DeletedRequiredParam 191 } 192 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: childLocation, Code: code}) 193 } 194 } 195 // detect added changed params 196 for paramName2, param2 := range params2 { 197 // changed? 198 if param1, ok := params1[paramName2]; ok { 199 sd.compareParams(URLMethod, paramLocation, paramName2, param1, param2) 200 } else { 201 // Added 202 childLocation := location.AddNode(getSchemaDiffNode(paramName2, ¶m2.SimpleSchema)) 203 code := AddedOptionalParam 204 if param2.Required { 205 code = AddedRequiredParam 206 } 207 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: childLocation, Code: code}) 208 } 209 } 210 } 211 } 212 } 213 } 214 215 func (sd *SpecAnalyser) analyseResponseParams() { 216 // Loop through url+methods in spec 2 - check deleted and changed 217 for eachURLMethodFrom2, op2 := range sd.urlMethods2 { 218 219 // present in both specs? Use key from spec 2 to lookup in spec 1 220 if op1, ok := sd.urlMethods1[eachURLMethodFrom2]; ok { 221 // compare responses for url and method 222 op1Responses := op1.Operation.Responses.StatusCodeResponses 223 op2Responses := op2.Operation.Responses.StatusCodeResponses 224 225 // deleted responses 226 for code1 := range op1Responses { 227 if _, ok := op2Responses[code1]; !ok { 228 location := DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code1, Node: getSchemaDiffNode("Body", op1Responses[code1].Schema)} 229 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: DeletedResponse}) 230 } 231 } 232 // Added updated Response Codes 233 for code2, op2Response := range op2Responses { 234 235 if op1Response, ok := op1Responses[code2]; ok { 236 op1Headers := op1Response.ResponseProps.Headers 237 headerRootNode := getNameOnlyDiffNode("Headers") 238 239 // Iterate Spec2 Headers looking for added and updated 240 location := DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: headerRootNode} 241 for op2HeaderName, op2Header := range op2Response.ResponseProps.Headers { 242 if op1Header, ok := op1Headers[op2HeaderName]; ok { 243 diffs := sd.CompareProps(forHeader(op1Header), forHeader(op2Header)) 244 sd.addDiffs(location, diffs) 245 } else { 246 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 247 DifferenceLocation: location.AddNode(getSchemaDiffNode(op2HeaderName, &op2Header.SimpleSchema)), 248 Code: AddedResponseHeader}) 249 } 250 } 251 for op1HeaderName := range op1Response.ResponseProps.Headers { 252 if _, ok := op2Response.ResponseProps.Headers[op1HeaderName]; !ok { 253 op1Header := op1Response.ResponseProps.Headers[op1HeaderName] 254 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 255 DifferenceLocation: location.AddNode(getSchemaDiffNode(op1HeaderName, &op1Header.SimpleSchema)), 256 Code: DeletedResponseHeader}) 257 } 258 } 259 schem := op1Response.Schema 260 node := getNameOnlyDiffNode("NoContent") 261 if schem != nil { 262 node = getSchemaDiffNode("Body", &schem.SchemaProps) 263 } 264 responseLocation := DifferenceLocation{URL: eachURLMethodFrom2.Path, 265 Method: eachURLMethodFrom2.Method, 266 Response: code2, 267 Node: node} 268 sd.compareDescripton(responseLocation, op1Response.Description, op2Response.Description) 269 270 if op1Response.Schema != nil { 271 sd.compareSchema( 272 DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: getSchemaDiffNode("Body", op1Response.Schema)}, 273 op1Response.Schema, 274 op2Response.Schema) 275 } 276 } else { 277 // op2Response 278 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 279 DifferenceLocation: DifferenceLocation{URL: eachURLMethodFrom2.Path, Method: eachURLMethodFrom2.Method, Response: code2, Node: getSchemaDiffNode("Body", op2Response.Schema)}, 280 Code: AddedResponse}) 281 } 282 } 283 } 284 } 285 } 286 287 func addTypeDiff(diffs []TypeDiff, diff TypeDiff) []TypeDiff { 288 if diff.Change != NoChangeDetected { 289 diffs = append(diffs, diff) 290 } 291 return diffs 292 } 293 294 // CompareProps computes type specific property diffs 295 func (sd *SpecAnalyser) CompareProps(type1, type2 *spec.SchemaProps) []TypeDiff { 296 297 diffs := []TypeDiff{} 298 299 diffs = CheckToFromPrimitiveType(diffs, type1, type2) 300 301 if len(diffs) > 0 { 302 return diffs 303 } 304 305 if isArray(type1) { 306 maxItemDiffs := CompareIntValues("MaxItems", type1.MaxItems, type2.MaxItems, WidenedType, NarrowedType) 307 diffs = append(diffs, maxItemDiffs...) 308 minItemsDiff := CompareIntValues("MinItems", type1.MinItems, type2.MinItems, NarrowedType, WidenedType) 309 diffs = append(diffs, minItemsDiff...) 310 } 311 312 if len(diffs) > 0 { 313 return diffs 314 } 315 316 diffs = CheckRefChange(diffs, type1, type2) 317 if len(diffs) > 0 { 318 return diffs 319 } 320 321 if !(isPrimitiveType(type1.Type) && isPrimitiveType(type2.Type)) { 322 return diffs 323 } 324 325 // check primitive type hierarchy change eg string -> integer = NarrowedChange 326 if type1.Type[0] != type2.Type[0] || 327 type1.Format != type2.Format { 328 diff := getTypeHierarchyChange(primitiveTypeString(type1.Type[0], type1.Format), primitiveTypeString(type2.Type[0], type2.Format)) 329 diffs = addTypeDiff(diffs, diff) 330 } 331 332 diffs = CheckStringTypeChanges(diffs, type1, type2) 333 334 if len(diffs) > 0 { 335 return diffs 336 } 337 338 diffs = checkNumericTypeChanges(diffs, type1, type2) 339 340 if len(diffs) > 0 { 341 return diffs 342 } 343 344 return diffs 345 } 346 347 func (sd *SpecAnalyser) compareParams(urlMethod URLMethod, location string, name string, param1, param2 spec.Parameter) { 348 diffLocation := DifferenceLocation{URL: urlMethod.Path, Method: urlMethod.Method} 349 350 childLocation := diffLocation.AddNode(getNameOnlyDiffNode(strings.Title(location))) 351 paramLocation := diffLocation.AddNode(getNameOnlyDiffNode(name)) 352 sd.compareDescripton(paramLocation, param1.Description, param2.Description) 353 354 if param1.Schema != nil && param2.Schema != nil { 355 if len(name) > 0 { 356 childLocation = childLocation.AddNode(getSchemaDiffNode(name, param2.Schema)) 357 } 358 359 sd.compareSchema(childLocation, param1.Schema, param2.Schema) 360 } 361 diffs := sd.CompareProps(forParam(param1), forParam(param2)) 362 363 childLocation = childLocation.AddNode(getSchemaDiffNode(name, ¶m2.SimpleSchema)) 364 if len(diffs) > 0 { 365 sd.addDiffs(childLocation, diffs) 366 } 367 368 diffs = CheckToFromRequired(param1.Required, param2.Required) 369 if len(diffs) > 0 { 370 sd.addDiffs(childLocation, diffs) 371 } 372 } 373 374 func (sd *SpecAnalyser) addTypeDiff(location DifferenceLocation, diff *TypeDiff) { 375 diffCopy := diff 376 desc := diffCopy.Description 377 if len(desc) == 0 { 378 if diffCopy.FromType != diffCopy.ToType { 379 desc = fmt.Sprintf("%s -> %s", diffCopy.FromType, diffCopy.ToType) 380 } 381 } 382 sd.Diffs = sd.Diffs.addDiff(SpecDifference{ 383 DifferenceLocation: location, 384 Code: diffCopy.Change, 385 DiffInfo: desc}) 386 } 387 388 func (sd *SpecAnalyser) compareDescripton(location DifferenceLocation, desc1, desc2 string) { 389 if desc1 != desc2 { 390 code := ChangedDescripton 391 if len(desc1) > 0 { 392 code = DeletedDescripton 393 } else if len(desc2) > 0 { 394 code = AddedDescripton 395 } 396 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: location, Code: code}) 397 } 398 } 399 400 func isPrimitiveType(item spec.StringOrArray) bool { 401 return len(item) > 0 && item[0] != ArrayType && item[0] != ObjectType 402 } 403 404 func isArrayType(item spec.StringOrArray) bool { 405 return len(item) > 0 && item[0] == ArrayType 406 } 407 func (sd *SpecAnalyser) getRefSchemaFromSpec1(ref spec.Ref) (*spec.Schema, string) { 408 return sd.schemaFromRef(ref, &sd.Definitions1) 409 } 410 411 func (sd *SpecAnalyser) getRefSchemaFromSpec2(ref spec.Ref) (*spec.Schema, string) { 412 return sd.schemaFromRef(ref, &sd.Definitions2) 413 } 414 415 // CompareSchemaFn Fn spec for comparing schemas 416 type CompareSchemaFn func(location DifferenceLocation, schema1, schema2 *spec.Schema) 417 418 func (sd *SpecAnalyser) compareSchema(location DifferenceLocation, schema1, schema2 *spec.Schema) { 419 420 refDiffs := []TypeDiff{} 421 refDiffs = CheckRefChange(refDiffs, schema1, schema2) 422 if len(refDiffs) > 0 { 423 for _, d := range refDiffs { 424 diff := d 425 sd.addTypeDiff(location, &diff) 426 } 427 return 428 } 429 430 if isRefType(schema1) { 431 key := schemaLocationKey(location) 432 if _, ok := sd.schemasCompared[key]; ok { 433 return 434 } 435 sd.schemasCompared[key] = struct{}{} 436 schema1, _ = sd.schemaFromRef(getRef(schema1), &sd.Definitions1) 437 } 438 439 if isRefType(schema2) { 440 schema2, _ = sd.schemaFromRef(getRef(schema2), &sd.Definitions2) 441 } 442 443 sd.compareDescripton(location, schema1.Description, schema2.Description) 444 445 typeDiffs := sd.CompareProps(&schema1.SchemaProps, &schema2.SchemaProps) 446 if len(typeDiffs) > 0 { 447 sd.addDiffs(location, typeDiffs) 448 return 449 } 450 451 if isArray(schema1) { 452 sd.compareSchema(location, schema1.Items.Schema, schema2.Items.Schema) 453 } 454 455 diffs := CompareProperties(location, schema1, schema2, sd.getRefSchemaFromSpec1, sd.getRefSchemaFromSpec2, sd.compareSchema) 456 for _, diff := range diffs { 457 sd.Diffs = sd.Diffs.addDiff(diff) 458 } 459 } 460 461 func (sd *SpecAnalyser) addDiffs(location DifferenceLocation, diffs []TypeDiff) { 462 for _, e := range diffs { 463 eachTypeDiff := e 464 if eachTypeDiff.Change != NoChangeDetected { 465 sd.addTypeDiff(location, &eachTypeDiff) 466 } 467 } 468 } 469 470 func addChildDiffNode(location DifferenceLocation, propName string, propSchema *spec.Schema) DifferenceLocation { 471 newNode := location.Node 472 childNode := fromSchemaProps(propName, &propSchema.SchemaProps) 473 if newNode != nil { 474 newNode = newNode.Copy() 475 newNode.AddLeafNode(&childNode) 476 } else { 477 newNode = &childNode 478 } 479 return DifferenceLocation{ 480 URL: location.URL, 481 Method: location.Method, 482 Response: location.Response, 483 Node: newNode, 484 } 485 } 486 487 func fromSchemaProps(fieldName string, props *spec.SchemaProps) Node { 488 node := Node{} 489 node.TypeName, node.IsArray = getSchemaType(props) 490 node.Field = fieldName 491 return node 492 } 493 494 func (sd *SpecAnalyser) findAddedEndpoints() { 495 for URLMethod := range sd.urlMethods2 { 496 if _, ok := sd.urlMethods1[URLMethod]; !ok { 497 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{URL: URLMethod.Path, Method: URLMethod.Method}, Code: AddedEndpoint}) 498 } 499 } 500 } 501 502 func (sd *SpecAnalyser) findDeletedEndpoints() { 503 for eachURLMethod, operation1 := range sd.urlMethods1 { 504 code := DeletedEndpoint 505 if (operation1.ParentPathItem.Options != nil && operation1.ParentPathItem.Options.Deprecated) || 506 (operation1.Operation.Deprecated) { 507 code = DeletedDeprecatedEndpoint 508 } 509 if _, ok := sd.urlMethods2[eachURLMethod]; !ok { 510 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{URL: eachURLMethod.Path, Method: eachURLMethod.Method}, Code: code}) 511 } 512 } 513 } 514 515 func (sd *SpecAnalyser) analyseMetaDataProperty(item1, item2 string, codeIfDiff SpecChangeCode, compatIfDiff Compatibility) { 516 if item1 != item2 { 517 diffSpec := fmt.Sprintf("%s -> %s", item1, item2) 518 sd.Diffs = sd.Diffs.addDiff(SpecDifference{DifferenceLocation: DifferenceLocation{Node: &Node{Field: "Spec Metadata"}}, Code: codeIfDiff, Compatibility: compatIfDiff, DiffInfo: diffSpec}) 519 } 520 } 521 522 func (sd *SpecAnalyser) schemaFromRef(ref spec.Ref, defns *spec.Definitions) (actualSchema *spec.Schema, definitionName string) { 523 definitionName = definitionFromRef(ref) 524 foundSchema, ok := (*defns)[definitionName] 525 if !ok { 526 return nil, definitionName 527 } 528 sd.ReferencedDefinitions[definitionName] = true 529 actualSchema = &foundSchema 530 return 531 532 } 533 534 func schemaLocationKey(location DifferenceLocation) string { 535 return location.Method + location.URL + location.Node.Field + location.Node.TypeName 536 } 537 538 // PropertyDefn combines a property with its required-ness 539 type PropertyDefn struct { 540 Schema *spec.Schema 541 Required bool 542 } 543 544 // PropertyMap a unified map including all AllOf fields 545 type PropertyMap map[string]PropertyDefn