github.com/jamescostian/go-swagger@v0.30.4-0.20221130163922-68364d6b567b/cmd/swagger/commands/diff/spec_analyser.go (about)

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