1package diff
2
3import (
4	"fmt"
5	"strings"
6
7	"github.com/go-openapi/spec"
8)
9
10// CompareEnums returns added, deleted enum values
11func CompareEnums(left, right []interface{}) []TypeDiff {
12	diffs := []TypeDiff{}
13
14	leftStrs := []string{}
15	rightStrs := []string{}
16	for _, eachLeft := range left {
17		leftStrs = append(leftStrs, fmt.Sprintf("%v", eachLeft))
18	}
19	for _, eachRight := range right {
20		rightStrs = append(rightStrs, fmt.Sprintf("%v", eachRight))
21	}
22	added, deleted, _ := fromStringArray(leftStrs).DiffsTo(rightStrs)
23	if len(added) > 0 {
24		typeChange := strings.Join(added, ",")
25		diffs = append(diffs, TypeDiff{Change: AddedEnumValue, Description: typeChange})
26	}
27	if len(deleted) > 0 {
28		typeChange := strings.Join(deleted, ",")
29		diffs = append(diffs, TypeDiff{Change: DeletedEnumValue, Description: typeChange})
30	}
31
32	return diffs
33}
34
35// CompareProperties recursive property comparison
36func CompareProperties(location DifferenceLocation, schema1 *spec.Schema, schema2 *spec.Schema, getRefFn1 SchemaFromRefFn, getRefFn2 SchemaFromRefFn, cmp CompareSchemaFn) []SpecDifference {
37	propDiffs := []SpecDifference{}
38
39	if schema1.Properties == nil && schema2.Properties == nil {
40		return propDiffs
41	}
42
43	schema1Props := propertiesFor(schema1, getRefFn1)
44	schema2Props := propertiesFor(schema2, getRefFn2)
45	// find deleted and changed properties
46
47	for eachProp1Name, eachProp1 := range schema1Props {
48		eachProp1 := eachProp1
49		childLoc := addChildDiffNode(location, eachProp1Name, eachProp1.Schema)
50
51		if eachProp2, ok := schema2Props[eachProp1Name]; ok {
52			diffs := CheckToFromRequired(eachProp1.Required, eachProp2.Required)
53			if len(diffs) > 0 {
54				for _, diff := range diffs {
55					propDiffs = append(propDiffs, SpecDifference{DifferenceLocation: childLoc, Code: diff.Change})
56				}
57			}
58			cmp(childLoc, eachProp1.Schema, eachProp2.Schema)
59		} else {
60			propDiffs = append(propDiffs, SpecDifference{DifferenceLocation: childLoc, Code: DeletedProperty})
61		}
62	}
63
64	// find added properties
65	for eachProp2Name, eachProp2 := range schema2.Properties {
66		eachProp2 := eachProp2
67		if _, ok := schema1.Properties[eachProp2Name]; !ok {
68			childLoc := addChildDiffNode(location, eachProp2Name, &eachProp2)
69			propDiffs = append(propDiffs, SpecDifference{DifferenceLocation: childLoc, Code: AddedProperty})
70		}
71	}
72	return propDiffs
73
74}
75
76// CompareFloatValues compares a float data item
77func CompareFloatValues(fieldName string, val1 *float64, val2 *float64, ifGreaterCode SpecChangeCode, ifLessCode SpecChangeCode) []TypeDiff {
78	diffs := []TypeDiff{}
79	if val1 != nil && val2 != nil {
80		if *val2 > *val1 {
81			diffs = append(diffs, TypeDiff{Change: ifGreaterCode, Description: fmt.Sprintf("%s %f->%f", fieldName, *val1, *val2)})
82		} else if *val2 < *val1 {
83			diffs = append(diffs, TypeDiff{Change: ifLessCode, Description: fmt.Sprintf("%s %f->%f", fieldName, *val1, *val2)})
84		}
85	} else {
86		if val1 != val2 {
87			if val1 != nil {
88				diffs = append(diffs, TypeDiff{Change: DeletedConstraint, Description: fmt.Sprintf("%s(%f)", fieldName, *val1)})
89			} else {
90				diffs = append(diffs, TypeDiff{Change: AddedConstraint, Description: fmt.Sprintf("%s(%f)", fieldName, *val2)})
91			}
92		}
93	}
94	return diffs
95}
96
97// CompareIntValues compares to int data items
98func CompareIntValues(fieldName string, val1 *int64, val2 *int64, ifGreaterCode SpecChangeCode, ifLessCode SpecChangeCode) []TypeDiff {
99	diffs := []TypeDiff{}
100	if val1 != nil && val2 != nil {
101		if *val2 > *val1 {
102			diffs = append(diffs, TypeDiff{Change: ifGreaterCode, Description: fmt.Sprintf("%s %d->%d", fieldName, *val1, *val2)})
103		} else if *val2 < *val1 {
104			diffs = append(diffs, TypeDiff{Change: ifLessCode, Description: fmt.Sprintf("%s %d->%d", fieldName, *val1, *val2)})
105		}
106	} else {
107		if val1 != val2 {
108			if val1 != nil {
109				diffs = append(diffs, TypeDiff{Change: DeletedConstraint, Description: fmt.Sprintf("%s(%d)", fieldName, *val1)})
110			} else {
111				diffs = append(diffs, TypeDiff{Change: AddedConstraint, Description: fmt.Sprintf("%s(%d)", fieldName, *val2)})
112			}
113		}
114	}
115	return diffs
116}
117
118// CheckToFromPrimitiveType check for diff to or from a primitive
119func CheckToFromPrimitiveType(diffs []TypeDiff, type1, type2 interface{}) []TypeDiff {
120
121	type1IsPrimitive := isPrimitive(type1)
122	type2IsPrimitive := isPrimitive(type2)
123
124	// Primitive to Obj or Obj to Primitive
125	if type1IsPrimitive != type2IsPrimitive {
126		typeStr1, isarray1 := getSchemaType(type1)
127		typeStr2, isarray2 := getSchemaType(type2)
128		return addTypeDiff(diffs, TypeDiff{Change: ChangedType, FromType: formatTypeString(typeStr1, isarray1), ToType: formatTypeString(typeStr2, isarray2)})
129	}
130
131	return diffs
132}
133
134// CheckRefChange has the property ref changed
135func CheckRefChange(diffs []TypeDiff, type1, type2 interface{}) (diffReturn []TypeDiff) {
136
137	diffReturn = diffs
138	if isRefType(type1) && isRefType(type2) {
139		// both refs but to different objects (TODO detect renamed object)
140		ref1 := definitionFromRef(getRef(type1))
141		ref2 := definitionFromRef(getRef(type2))
142		if ref1 != ref2 {
143			diffReturn = addTypeDiff(diffReturn, TypeDiff{Change: RefTargetChanged, FromType: getSchemaTypeStr(type1), ToType: getSchemaTypeStr(type2)})
144		}
145	} else if isRefType(type1) != isRefType(type2) {
146		diffReturn = addTypeDiff(diffReturn, TypeDiff{Change: ChangedType, FromType: getSchemaTypeStr(type1), ToType: getSchemaTypeStr(type2)})
147	}
148	return
149}
150
151// checkNumericTypeChanges checks for changes to or from a numeric type
152func checkNumericTypeChanges(diffs []TypeDiff, type1, type2 *spec.SchemaProps) []TypeDiff {
153	// Number
154	_, type1IsNumeric := numberWideness[type1.Type[0]]
155	_, type2IsNumeric := numberWideness[type2.Type[0]]
156
157	if type1IsNumeric && type2IsNumeric {
158		foundDiff := false
159		if type1.ExclusiveMaximum && !type2.ExclusiveMaximum {
160			diffs = addTypeDiff(diffs, TypeDiff{Change: WidenedType, Description: fmt.Sprintf("Exclusive Maximum Removed:%v->%v", type1.ExclusiveMaximum, type2.ExclusiveMaximum)})
161			foundDiff = true
162		}
163		if !type1.ExclusiveMaximum && type2.ExclusiveMaximum {
164			diffs = addTypeDiff(diffs, TypeDiff{Change: NarrowedType, Description: fmt.Sprintf("Exclusive Maximum Added:%v->%v", type1.ExclusiveMaximum, type2.ExclusiveMaximum)})
165			foundDiff = true
166		}
167		if type1.ExclusiveMinimum && !type2.ExclusiveMinimum {
168			diffs = addTypeDiff(diffs, TypeDiff{Change: WidenedType, Description: fmt.Sprintf("Exclusive Minimum Removed:%v->%v", type1.ExclusiveMaximum, type2.ExclusiveMaximum)})
169			foundDiff = true
170		}
171		if !type1.ExclusiveMinimum && type2.ExclusiveMinimum {
172			diffs = addTypeDiff(diffs, TypeDiff{Change: NarrowedType, Description: fmt.Sprintf("Exclusive Minimum Added:%v->%v", type1.ExclusiveMinimum, type2.ExclusiveMinimum)})
173			foundDiff = true
174		}
175		if !foundDiff {
176			maxDiffs := CompareFloatValues("Maximum", type1.Maximum, type2.Maximum, WidenedType, NarrowedType)
177			diffs = append(diffs, maxDiffs...)
178			minDiffs := CompareFloatValues("Minimum", type1.Minimum, type2.Minimum, NarrowedType, WidenedType)
179			diffs = append(diffs, minDiffs...)
180		}
181	}
182	return diffs
183}
184
185// CheckStringTypeChanges checks for changes to or from a string type
186func CheckStringTypeChanges(diffs []TypeDiff, type1, type2 *spec.SchemaProps) []TypeDiff {
187	// string changes
188	if type1.Type[0] == StringType &&
189		type2.Type[0] == StringType {
190		minLengthDiffs := CompareIntValues("MinLength", type1.MinLength, type2.MinLength, NarrowedType, WidenedType)
191		diffs = append(diffs, minLengthDiffs...)
192		maxLengthDiffs := CompareIntValues("MaxLength", type1.MinLength, type2.MinLength, WidenedType, NarrowedType)
193		diffs = append(diffs, maxLengthDiffs...)
194		if type1.Pattern != type2.Pattern {
195			diffs = addTypeDiff(diffs, TypeDiff{Change: ChangedType, Description: fmt.Sprintf("Pattern Changed:%s->%s", type1.Pattern, type2.Pattern)})
196		}
197		if type1.Type[0] == StringType {
198			if len(type1.Enum) > 0 {
199				enumDiffs := CompareEnums(type1.Enum, type2.Enum)
200				diffs = append(diffs, enumDiffs...)
201			}
202		}
203	}
204	return diffs
205}
206
207// CheckToFromRequired checks for changes to or from a required property
208func CheckToFromRequired(required1, required2 bool) (diffs []TypeDiff) {
209	if required1 != required2 {
210		code := ChangedOptionalToRequired
211		if required1 {
212			code = ChangedRequiredToOptional
213		}
214		diffs = addTypeDiff(diffs, TypeDiff{Change: code})
215	}
216	return diffs
217}
218
219const objType = "object"
220
221func getTypeHierarchyChange(type1, type2 string) TypeDiff {
222	fromType := type1
223	if fromType == "" {
224		fromType = objType
225	}
226	toType := type2
227	if toType == "" {
228		toType = objType
229	}
230	diffDescription := fmt.Sprintf("%s -> %s", fromType, toType)
231	if isStringType(type1) && !isStringType(type2) {
232		return TypeDiff{Change: NarrowedType, Description: diffDescription}
233	}
234	if !isStringType(type1) && isStringType(type2) {
235		return TypeDiff{Change: WidenedType, Description: diffDescription}
236	}
237	type1Wideness, type1IsNumeric := numberWideness[type1]
238	type2Wideness, type2IsNumeric := numberWideness[type2]
239	if type1IsNumeric && type2IsNumeric {
240		if type1Wideness == type2Wideness {
241			return TypeDiff{Change: ChangedToCompatibleType, Description: diffDescription}
242		}
243		if type1Wideness > type2Wideness {
244			return TypeDiff{Change: NarrowedType, Description: diffDescription}
245		}
246		if type1Wideness < type2Wideness {
247			return TypeDiff{Change: WidenedType, Description: diffDescription}
248		}
249	}
250	return TypeDiff{Change: ChangedType, Description: diffDescription}
251}
252
253func isRefType(item interface{}) bool {
254	switch s := item.(type) {
255	case spec.Refable:
256		return s.Ref.String() != ""
257	case *spec.Schema:
258		return s.Ref.String() != ""
259	case *spec.SchemaProps:
260		return s.Ref.String() != ""
261	case *spec.SimpleSchema:
262		return false
263	default:
264		return false
265	}
266}
267