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