Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

delta #473

Merged
merged 20 commits into from
Jan 26, 2024
14 changes: 14 additions & 0 deletions data/simple3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
info:
title: Tufin
version: 1.0.0
openapi: 3.0.3
paths:
/api/test:
get:
responses:
200:
description: OK
post:
responses:
201:
description: OK
17 changes: 17 additions & 0 deletions data/simple4.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
info:
title: Tufin
version: 1.0.0
openapi: 3.0.3
paths:
/api/test:
get:
parameters:
- name: a
in: query
responses:
200:
description: OK
post:
responses:
201:
description: OK
25 changes: 25 additions & 0 deletions delta/delta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package delta

import (
"github.com/tufin/oasdiff/diff"
)

const coefficient = 0.5

func Get(asymmetric bool, diffReport *diff.Diff) float64 {
if diffReport.Empty() {
return 0
}

delta := getEndpointsDelta(asymmetric, diffReport.EndpointsDiff)

return delta
}

func ratio(asymmetric bool, added int, deleted int, modifiedDelta float64, all int) float64 {
if asymmetric {
added = 0
}

return (float64(added+deleted) + modifiedDelta) / float64(all)
}
111 changes: 111 additions & 0 deletions delta/delta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package delta_test

import (
"testing"

"github.com/getkin/kin-openapi/openapi3"
"github.com/stretchr/testify/require"
"github.com/tufin/oasdiff/delta"
"github.com/tufin/oasdiff/diff"
"github.com/tufin/oasdiff/utils"
)

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, 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, 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, err := diff.Get(diff.NewConfig(), s1, s2)
require.NoError(t, err)
require.Equal(t, 1.0, 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"}
specPairs := specs.CartesianProduct(specs)

loader := openapi3.NewLoader()
for _, pair := range specPairs {
s1, err := loader.LoadFromFile(pair.X)
require.NoError(t, err)

s2, err := loader.LoadFromFile(pair.Y)
require.NoError(t, err)

d1, err := diff.Get(diff.NewConfig(), s1, s2)
require.NoError(t, err)

d2, err := diff.Get(diff.NewConfig(), s2, s1)
require.NoError(t, err)

require.Equal(t, delta.Get(false, d1), delta.Get(false, d2), pair)
}
}

func TestAsymmetric(t *testing.T) {
specs := utils.StringList{"../data/simple.yaml", "../data/simple1.yaml", "../data/simple2.yaml", "../data/simple3.yaml", "../data/simple4.yaml"}
specPairs := specs.CartesianProduct(specs)

loader := openapi3.NewLoader()
for _, pair := range specPairs {
s1, err := loader.LoadFromFile(pair.X)
require.NoError(t, err)

s2, err := loader.LoadFromFile(pair.Y)
require.NoError(t, err)

d1, err := diff.Get(diff.NewConfig(), s1, s2)
require.NoError(t, err)
asymmetric1 := delta.Get(true, d1)

d2, err := diff.Get(diff.NewConfig(), s2, s1)
require.NoError(t, err)
asymmetric2 := delta.Get(true, d2)

symmetric := delta.Get(false, d2)

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))
}
5 changes: 5 additions & 0 deletions delta/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/*
Package delta provides a distance function for OpenAPI Spec 3.
The delta is a numeric value representing the distance between base and revision specs.
*/
package delta
40 changes: 40 additions & 0 deletions delta/endpoints.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package delta

import (
"github.com/tufin/oasdiff/diff"
)

func getEndpointsDelta(asymmetric bool, d *diff.EndpointsDiff) float64 {
if d.Empty() {
return 0
}

added := len(d.Added)
deleted := len(d.Deleted)
modified := len(d.Modified)
unchanged := len(d.Unchanged)
all := added + deleted + modified + unchanged

modifiedDelta := coefficient * getModifiedEndpointsDelta(asymmetric, d.Modified)

return ratio(asymmetric, added, deleted, modifiedDelta, all)
}

func getModifiedEndpointsDelta(asymmetric bool, d diff.ModifiedEndpoints) float64 {
result := 0.0
for _, methodDiff := range d {
result += getModifiedEndpointDelta(asymmetric, methodDiff)
}
return result
}

func getModifiedEndpointDelta(asymmetric bool, d *diff.MethodDiff) float64 {
if d.Empty() {
return 0
}

// TODO: consider additional elements of MethodDiff
delta := getParametersDelta(asymmetric, d.ParametersDiff)

return delta
}
22 changes: 22 additions & 0 deletions delta/parameters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package delta

import (
"github.com/tufin/oasdiff/diff"
)

func getParametersDelta(asymmetric bool, d *diff.ParametersDiffByLocation) float64 {
if d.Empty() {
return 0
}

added := d.Added.Len()
deleted := d.Deleted.Len()
modified := d.Modified.Len()
unchanged := d.Unchanged.Len()
all := added + deleted + modified + unchanged

// TODO: drill down into modified
modifiedDelta := coefficient * float64(modified)

return ratio(asymmetric, added, deleted, modifiedDelta, all)
}
7 changes: 4 additions & 3 deletions diff/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ func TestDiff_ModifiedOperation(t *testing.T) {
require.NoError(t, err)

require.Equal(t, &diff.OperationsDiff{
Added: utils.StringList{"GET"},
Deleted: utils.StringList{"POST"},
Modified: diff.ModifiedOperations{},
Added: utils.StringList{"GET"},
Deleted: utils.StringList{"POST"},
Modified: diff.ModifiedOperations{},
Unchanged: utils.StringList{},
},
d.PathsDiff.Modified["/api/test"].OperationsDiff)
}
Expand Down
25 changes: 19 additions & 6 deletions diff/endpoints_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ For example, if there's a new path "/test" with method POST then EndpointsDiff w
Or, if path "/test" was modified to include a new methdod, PUT, then EndpointsDiff will describe this as a new endpoint: PUT /test.
*/
type EndpointsDiff struct {
Added Endpoints `json:"added,omitempty" yaml:"added,omitempty"`
Deleted Endpoints `json:"deleted,omitempty" yaml:"deleted,omitempty"`
Modified ModifiedEndpoints `json:"modified,omitempty" yaml:"modified,omitempty"`
Added Endpoints `json:"added,omitempty" yaml:"added,omitempty"`
Deleted Endpoints `json:"deleted,omitempty" yaml:"deleted,omitempty"`
Modified ModifiedEndpoints `json:"modified,omitempty" yaml:"modified,omitempty"`
Unchanged Endpoints `json:"unchanged,omitempty" yaml:"unchanged,omitempty"`
}

// Endpoint is a combination of an HTTP method and a Path
Expand All @@ -37,9 +38,10 @@ func (diff *EndpointsDiff) Empty() bool {

func newEndpointsDiff() *EndpointsDiff {
return &EndpointsDiff{
Added: Endpoints{},
Deleted: Endpoints{},
Modified: ModifiedEndpoints{},
Added: Endpoints{},
Deleted: Endpoints{},
Modified: ModifiedEndpoints{},
Unchanged: Endpoints{},
}
}

Expand Down Expand Up @@ -109,6 +111,13 @@ func (diff *EndpointsDiff) addDeletedPath(path string, method string) {
})
}

func (diff *EndpointsDiff) addUnchangedPath(path string, method string) {
diff.Unchanged = append(diff.Unchanged, Endpoint{
Method: method,
Path: path,
})
}

func (diff *EndpointsDiff) addModifiedPaths(config *Config, state *state, path string, pathItemPair *pathItemPair) error {

pathDiff, err := getPathDiff(config, state, pathItemPair)
Expand All @@ -135,6 +144,10 @@ func (diff *EndpointsDiff) addModifiedPaths(config *Config, state *state, path s
}] = methodDiff
}

for _, method := range pathDiff.OperationsDiff.Unchanged {
diff.addUnchangedPath(path, method)
}

return nil
}

Expand Down
16 changes: 10 additions & 6 deletions diff/operations_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (

// OperationsDiff describes the changes between a pair of operation objects (https://swagger.io/specification/#operation-object) of two path item objects
type OperationsDiff struct {
Added utils.StringList `json:"added,omitempty" yaml:"added,omitempty"`
Deleted utils.StringList `json:"deleted,omitempty" yaml:"deleted,omitempty"`
Modified ModifiedOperations `json:"modified,omitempty" yaml:"modified,omitempty"`
Added utils.StringList `json:"added,omitempty" yaml:"added,omitempty"`
Deleted utils.StringList `json:"deleted,omitempty" yaml:"deleted,omitempty"`
Modified ModifiedOperations `json:"modified,omitempty" yaml:"modified,omitempty"`
Unchanged utils.StringList `json:"unchanged,omitempty" yaml:"unchanged,omitempty"`
}

// Empty indicates whether a change was found in this element
Expand All @@ -29,9 +30,10 @@ func (operationsDiff *OperationsDiff) Empty() bool {

func newOperationsDiff() *OperationsDiff {
return &OperationsDiff{
Added: utils.StringList{},
Deleted: utils.StringList{},
Modified: ModifiedOperations{},
Added: utils.StringList{},
Deleted: utils.StringList{},
Modified: ModifiedOperations{},
Unchanged: utils.StringList{},
}
}

Expand Down Expand Up @@ -105,6 +107,8 @@ func (operationsDiff *OperationsDiff) diffOperation(config *Config, state *state

if !diff.Empty() {
operationsDiff.Modified[method] = diff
} else {
operationsDiff.Unchanged = append(operationsDiff.Unchanged, method)
}

return nil
Expand Down
Loading
Loading