diff --git a/data/simple4.yaml b/data/simple4.yaml index 30bf1311..4d4299e2 100644 --- a/data/simple4.yaml +++ b/data/simple4.yaml @@ -8,6 +8,8 @@ paths: parameters: - name: a in: query + schema: + type: integer responses: 200: description: OK diff --git a/data/simple5.yaml b/data/simple5.yaml new file mode 100644 index 00000000..3e3a3324 --- /dev/null +++ b/data/simple5.yaml @@ -0,0 +1,19 @@ +info: + title: Tufin + version: 1.0.0 +openapi: 3.0.3 +paths: + /api/test: + get: + parameters: + - name: a + in: query + schema: + type: string + responses: + 200: + description: OK + post: + responses: + 201: + description: OK diff --git a/delta/delta.go b/delta/delta.go index 63b8e27d..ce948ccc 100644 --- a/delta/delta.go +++ b/delta/delta.go @@ -11,9 +11,7 @@ func Get(asymmetric bool, diffReport *diff.Diff) float64 { return 0 } - deltaEndpoints := getEndpointsDelta(asymmetric, diffReport.EndpointsDiff) - - return weightedAverage(deltaEndpoints) + return getEndpointsDelta(asymmetric, diffReport.EndpointsDiff) } func ratio(asymmetric bool, added int, deleted int, modifiedDelta float64, all int) float64 { @@ -23,18 +21,3 @@ func ratio(asymmetric bool, added int, deleted int, modifiedDelta float64, all i return (float64(added+deleted) + modifiedDelta) / float64(all) } - -type WeightedDelta struct { - delta float64 - weight int -} - -func weightedAverage(weightedDeltas ...WeightedDelta) float64 { - dividend := 0.0 - divisor := 0 - for _, weightedDelta := range weightedDeltas { - dividend += weightedDelta.delta - divisor += weightedDelta.weight - } - return dividend / float64(divisor) -} diff --git a/delta/delta_test.go b/delta/delta_test.go index 317df135..823838d7 100644 --- a/delta/delta_test.go +++ b/delta/delta_test.go @@ -11,56 +11,146 @@ import ( ) func TestEmpty(t *testing.T) { - loader := openapi3.NewLoader() - s1, err := loader.LoadFromFile("../data/empty.yaml") - require.NoError(t, err) - - d, err := diff.Get(diff.NewConfig(), s1, s1) - require.NoError(t, err) + d := &diff.Diff{} require.Equal(t, 0.0, delta.Get(false, d)) } func TestEndpointAdded(t *testing.T) { - loader := openapi3.NewLoader() - s1, err := loader.LoadFromFile("../data/simple1.yaml") - require.NoError(t, err) - - s2, err := loader.LoadFromFile("../data/simple3.yaml") - require.NoError(t, err) + d := &diff.Diff{ + EndpointsDiff: &diff.EndpointsDiff{ + Added: diff.Endpoints{ + diff.Endpoint{ + Method: "GET", + Path: "/test", + }, + }, + Unchanged: diff.Endpoints{ + diff.Endpoint{ + Method: "POST", + Path: "/test", + }, + }, + }, + } - d, err := diff.Get(diff.NewConfig(), s1, s2) - require.NoError(t, err) require.Equal(t, 0.5, delta.Get(false, d)) } func TestEndpointDeletedAsym(t *testing.T) { - loader := openapi3.NewLoader() - s1, err := loader.LoadFromFile("../data/simple3.yaml") - require.NoError(t, err) - - s2, err := loader.LoadFromFile("../data/simple1.yaml") - require.NoError(t, err) + d := &diff.Diff{ + EndpointsDiff: &diff.EndpointsDiff{ + Deleted: diff.Endpoints{ + diff.Endpoint{ + Method: "GET", + Path: "/test", + }, + }, + Unchanged: diff.Endpoints{ + diff.Endpoint{ + Method: "POST", + Path: "/test", + }, + }, + }, + } - d, err := diff.Get(diff.NewConfig(), s1, s2) - require.NoError(t, err) require.Equal(t, 0.5, delta.Get(true, d)) } func TestEndpointAddedAndDeleted(t *testing.T) { - loader := openapi3.NewLoader() - s1, err := loader.LoadFromFile("../data/simple1.yaml") - require.NoError(t, err) - - s2, err := loader.LoadFromFile("../data/simple2.yaml") - require.NoError(t, err) + d := &diff.Diff{ + EndpointsDiff: &diff.EndpointsDiff{ + Added: diff.Endpoints{ + diff.Endpoint{ + Method: "GET", + Path: "/test", + }, + }, + Deleted: diff.Endpoints{ + diff.Endpoint{ + Method: "POST", + Path: "/test", + }, + }, + }, + } - d, err := diff.Get(diff.NewConfig(), s1, s2) - require.NoError(t, err) require.Equal(t, 1.0, delta.Get(false, d)) } +func TestParameters(t *testing.T) { + d := &diff.Diff{ + EndpointsDiff: &diff.EndpointsDiff{ + Modified: diff.ModifiedEndpoints{ + diff.Endpoint{ + Method: "GET", + Path: "/test", + }: &diff.MethodDiff{ + ParametersDiff: &diff.ParametersDiffByLocation{ + Deleted: diff.ParamNamesByLocation{ + "query": utils.StringList{"a"}, + }, + }, + }, + }, + }, + } + + require.Equal(t, 0.5, delta.Get(true, d)) +} + +func TestResponses(t *testing.T) { + d := &diff.Diff{ + EndpointsDiff: &diff.EndpointsDiff{ + Modified: diff.ModifiedEndpoints{ + diff.Endpoint{ + Method: "GET", + Path: "/test", + }: &diff.MethodDiff{ + ResponsesDiff: &diff.ResponsesDiff{ + Added: utils.StringList{"201"}, + Deleted: utils.StringList{"200"}, + }, + }, + }, + }, + } + + require.Equal(t, 0.5, delta.Get(false, d)) +} + +func TestSchema(t *testing.T) { + d := &diff.Diff{ + EndpointsDiff: &diff.EndpointsDiff{ + Modified: diff.ModifiedEndpoints{ + diff.Endpoint{ + Method: "GET", + Path: "/test", + }: &diff.MethodDiff{ + ParametersDiff: &diff.ParametersDiffByLocation{ + Modified: diff.ParamDiffByLocation{ + "query": diff.ParamDiffs{ + "a": &diff.ParameterDiff{ + SchemaDiff: &diff.SchemaDiff{ + TypeDiff: &diff.ValueDiff{ + From: "integer", + To: "string", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + require.Equal(t, 0.25, delta.Get(false, d)) +} + func TestSymmetric(t *testing.T) { - specs := utils.StringList{"../data/simple.yaml", "../data/simple1.yaml", "../data/simple2.yaml", "../data/simple3.yaml", "../data/simple4.yaml"} + specs := utils.StringList{"../data/simple.yaml", "../data/simple1.yaml", "../data/simple2.yaml", "../data/simple3.yaml", "../data/simple4.yaml", "../data/simple5.yaml"} specPairs := specs.CartesianProduct(specs) loader := openapi3.NewLoader() @@ -82,7 +172,7 @@ func TestSymmetric(t *testing.T) { } func TestAsymmetric(t *testing.T) { - specs := utils.StringList{"../data/simple.yaml", "../data/simple1.yaml", "../data/simple2.yaml", "../data/simple3.yaml", "../data/simple4.yaml"} + specs := utils.StringList{"../data/simple.yaml", "../data/simple1.yaml", "../data/simple2.yaml", "../data/simple3.yaml", "../data/simple4.yaml", "../data/simple5.yaml"} specPairs := specs.CartesianProduct(specs) loader := openapi3.NewLoader() @@ -106,29 +196,3 @@ func TestAsymmetric(t *testing.T) { require.Equal(t, asymmetric1+asymmetric2, symmetric, pair) } } - -func TestParameters(t *testing.T) { - loader := openapi3.NewLoader() - s1, err := loader.LoadFromFile("../data/simple4.yaml") - require.NoError(t, err) - - s2, err := loader.LoadFromFile("../data/simple3.yaml") - require.NoError(t, err) - - d, err := diff.Get(diff.NewConfig(), s1, s2) - require.NoError(t, err) - require.Equal(t, 0.25, delta.Get(true, d)) -} - -func TestResponses(t *testing.T) { - loader := openapi3.NewLoader() - s1, err := loader.LoadFromFile("../data/simple2.yaml") - require.NoError(t, err) - - s2, err := loader.LoadFromFile("../data/simple4.yaml") - require.NoError(t, err) - - d, err := diff.Get(diff.NewConfig(), s1, s2) - require.NoError(t, err) - require.Equal(t, 0.0625, delta.Get(true, d)) -} diff --git a/delta/endpoints.go b/delta/endpoints.go index 9e390202..15eeb5a6 100644 --- a/delta/endpoints.go +++ b/delta/endpoints.go @@ -4,9 +4,9 @@ import ( "github.com/tufin/oasdiff/diff" ) -func getEndpointsDelta(asymmetric bool, d *diff.EndpointsDiff) WeightedDelta { +func getEndpointsDelta(asymmetric bool, d *diff.EndpointsDiff) float64 { if d.Empty() { - return WeightedDelta{} + return 0 } added := len(d.Added) @@ -17,18 +17,17 @@ func getEndpointsDelta(asymmetric bool, d *diff.EndpointsDiff) WeightedDelta { modifiedDelta := coefficient * getModifiedEndpointsDelta(asymmetric, d.Modified) - return WeightedDelta{ - delta: ratio(asymmetric, added, deleted, modifiedDelta, all), - weight: 1, - } + return ratio(asymmetric, added, deleted, modifiedDelta, all) } func getModifiedEndpointsDelta(asymmetric bool, d diff.ModifiedEndpoints) float64 { - result := 0.0 + weightedDeltas := make([]*WeightedDelta, len(d)) + i := 0 for _, methodDiff := range d { - result += getModifiedEndpointDelta(asymmetric, methodDiff) + weightedDeltas[i] = NewWeightedDelta(getModifiedEndpointDelta(asymmetric, methodDiff), 1) + i++ } - return result + return weightedAverage(weightedDeltas) } func getModifiedEndpointDelta(asymmetric bool, d *diff.MethodDiff) float64 { @@ -40,5 +39,5 @@ func getModifiedEndpointDelta(asymmetric bool, d *diff.MethodDiff) float64 { paramsDelta := getParametersDelta(asymmetric, d.ParametersDiff) responsesDelta := getResponsesDelta(asymmetric, d.ResponsesDiff) - return weightedAverage(paramsDelta, responsesDelta) + return weightedAverage([]*WeightedDelta{paramsDelta, responsesDelta}) } diff --git a/delta/parameters.go b/delta/parameters.go index 00044909..e581b794 100644 --- a/delta/parameters.go +++ b/delta/parameters.go @@ -4,9 +4,9 @@ import ( "github.com/tufin/oasdiff/diff" ) -func getParametersDelta(asymmetric bool, d *diff.ParametersDiffByLocation) WeightedDelta { +func getParametersDelta(asymmetric bool, d *diff.ParametersDiffByLocation) *WeightedDelta { if d.Empty() { - return WeightedDelta{} + return &WeightedDelta{} } added := d.Added.Len() @@ -15,11 +15,33 @@ func getParametersDelta(asymmetric bool, d *diff.ParametersDiffByLocation) Weigh unchanged := d.Unchanged.Len() all := added + deleted + modified + unchanged - // TODO: drill down into modified - modifiedDelta := coefficient * float64(modified) + modifiedDelta := coefficient * getModifiedParametersDelta(asymmetric, d.Modified) - return WeightedDelta{ - delta: ratio(asymmetric, added, deleted, modifiedDelta, all), - weight: all, + return NewWeightedDelta( + ratio(asymmetric, added, deleted, modifiedDelta, all), + all, + ) +} + +func getModifiedParametersDelta(asymmetric bool, d diff.ParamDiffByLocation) float64 { + weightedDeltas := make([]*WeightedDelta, len(d)) + i := 0 + for _, paramsDiff := range d { + for _, parameterDiff := range paramsDiff { + weightedDeltas[i] = NewWeightedDelta(getModifiedParameterDelta(asymmetric, parameterDiff), 1) + i++ + } + } + return weightedAverage(weightedDeltas) +} + +func getModifiedParameterDelta(asymmetric bool, d *diff.ParameterDiff) float64 { + if d.Empty() { + return 0.0 } + + // TODO: consider additional elements of ParameterDiff + schemaDelta := getSchemaDelta(asymmetric, d.SchemaDiff) + + return schemaDelta } diff --git a/delta/responses.go b/delta/responses.go index 94bdaac8..06ee8de8 100644 --- a/delta/responses.go +++ b/delta/responses.go @@ -4,9 +4,9 @@ import ( "github.com/tufin/oasdiff/diff" ) -func getResponsesDelta(asymmetric bool, d *diff.ResponsesDiff) WeightedDelta { +func getResponsesDelta(asymmetric bool, d *diff.ResponsesDiff) *WeightedDelta { if d.Empty() { - return WeightedDelta{} + return &WeightedDelta{} } added := d.Added.Len() @@ -18,8 +18,5 @@ func getResponsesDelta(asymmetric bool, d *diff.ResponsesDiff) WeightedDelta { // TODO: drill down into modified modifiedDelta := coefficient * float64(modified) - return WeightedDelta{ - delta: ratio(asymmetric, added, deleted, modifiedDelta, all), - weight: all, - } + return NewWeightedDelta(ratio(asymmetric, added, deleted, modifiedDelta, all), all) } diff --git a/delta/schema.go b/delta/schema.go new file mode 100644 index 00000000..c6ab5f3b --- /dev/null +++ b/delta/schema.go @@ -0,0 +1,31 @@ +package delta + +import ( + "github.com/tufin/oasdiff/diff" +) + +func getSchemaDelta(asymmetric bool, d *diff.SchemaDiff) float64 { + if d.Empty() { + return 0 + } + + // consider additional fields of schema + typeDelta := modifiedLeafDelta(asymmetric, boolToFloat64(!d.TypeDiff.Empty())) + + return typeDelta +} + +func modifiedLeafDelta(asymmetric bool, modified float64) float64 { + if asymmetric { + return modified / 2 + } + + return modified +} + +func boolToFloat64(b bool) float64 { + if b { + return 1.0 + } + return 0.0 +} diff --git a/delta/weighted_delta.go b/delta/weighted_delta.go new file mode 100644 index 00000000..fff024c2 --- /dev/null +++ b/delta/weighted_delta.go @@ -0,0 +1,26 @@ +package delta + +type WeightedDelta struct { + delta float64 + weight int +} + +func NewWeightedDelta(delta float64, weight int) *WeightedDelta { + return &WeightedDelta{ + delta: delta, + weight: weight, + } +} + +func weightedAverage(weightedDeltas []*WeightedDelta) float64 { + dividend := 0.0 + divisor := 0 + for _, weightedDelta := range weightedDeltas { + dividend += weightedDelta.delta * float64(weightedDelta.weight) + divisor += weightedDelta.weight + } + if dividend == 0 { + return 0 + } + return dividend / float64(divisor) +}