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