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