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, &param1.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, &param2.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, &param2.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, &param1.SimpleSchema, &param2.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