diff --git a/datamodel/low/v3/components.go b/datamodel/low/v3/components.go index a53534e..31e878b 100644 --- a/datamodel/low/v3/components.go +++ b/datamodel/low/v3/components.go @@ -39,6 +39,7 @@ func (co *Components) FindExtension(ext string) *low.ValueReference[any] { // FindSchema attempts to locate a SchemaProxy from 'schemas' with a specific name func (co *Components) FindSchema(schema string) *low.ValueReference[*base.SchemaProxy] { return low.FindItemInMap[*base.SchemaProxy](schema, co.Schemas.Value) + } // FindResponse attempts to locate a Response from 'responses' with a specific name diff --git a/what-changed/model/comparison_functions.go b/what-changed/model/comparison_functions.go index 84caeae..7325559 100644 --- a/what-changed/model/comparison_functions.go +++ b/what-changed/model/comparison_functions.go @@ -8,6 +8,7 @@ import ( "github.com/pb33f/libopenapi/datamodel/low" "gopkg.in/yaml.v3" "strings" + "sync" ) const ( @@ -184,74 +185,8 @@ func CheckForModification[T any](l, r *yaml.Node, label string, changes *[]*Chan func CheckMapForChanges[T any, R any](expLeft, expRight map[low.KeyReference[string]]low.ValueReference[T], changes *[]*Change, label string, compareFunc func(l, r T) R) map[string]R { - lHashes := make(map[string]string) - rHashes := make(map[string]string) - lValues := make(map[string]low.ValueReference[T]) - rValues := make(map[string]low.ValueReference[T]) - - for k := range expLeft { - lHashes[k.Value] = low.GenerateHashString(expLeft[k].Value) - lValues[k.Value] = expLeft[k] - } - - for k := range expRight { - rHashes[k.Value] = low.GenerateHashString(expRight[k].Value) - rValues[k.Value] = expRight[k] - } - - expChanges := make(map[string]R) - - checkLeft := func(k string, doneChan chan bool, f, g map[string]string, p, h map[string]low.ValueReference[T]) { - rhash := g[k] - if rhash == "" { - if p[k].GetValueNode().Value == "" { - p[k].GetValueNode().Value = k - } - CreateChange(changes, ObjectRemoved, label, - p[k].GetValueNode(), nil, true, - p[k].GetValue(), nil) - doneChan <- true - return - } - if f[k] == g[k] { - doneChan <- true - return - } - // run comparison. - expChanges[k] = compareFunc(p[k].Value, h[k].Value) - doneChan <- true - } - - doneChan := make(chan bool) - count := 0 - - // check left example hashes - for k := range lHashes { - count++ - go checkLeft(k, doneChan, lHashes, rHashes, lValues, rValues) - } - - //check right example hashes - for k := range rHashes { - count++ - go checkRightValue(k, doneChan, lHashes, rValues, changes, label) - } - - // wait for all done signals. - completed := 0 - for completed < count { - select { - case <-doneChan: - completed++ - } - } - return expChanges -} - -// CheckMapForChangesUntyped checks a left and right low level map for any additions, subtractions or modifications to -// values. The compareFunc can be generic and accept any type -func CheckMapForChangesUntyped[T any, R any](expLeft, expRight map[low.KeyReference[string]]low.ValueReference[T], - changes *[]*Change, label string, compareFunc func(l, r any) R) map[string]R { + // stop concurrent threads screwing up changes. + var chLock sync.Mutex lHashes := make(map[string]string) rHashes := make(map[string]string) @@ -276,9 +211,11 @@ func CheckMapForChangesUntyped[T any, R any](expLeft, expRight map[low.KeyRefere if p[k].GetValueNode().Value == "" { p[k].GetValueNode().Value = k } + chLock.Lock() CreateChange(changes, ObjectRemoved, label, p[k].GetValueNode(), nil, true, p[k].GetValue(), nil) + chLock.Unlock() doneChan <- true return } @@ -303,7 +240,7 @@ func CheckMapForChangesUntyped[T any, R any](expLeft, expRight map[low.KeyRefere //check right example hashes for k := range rHashes { count++ - go checkRightValue(k, doneChan, lHashes, rValues, changes, label) + go checkRightValue(k, doneChan, lHashes, rValues, changes, label, &chLock) } // wait for all done signals. @@ -317,17 +254,88 @@ func CheckMapForChangesUntyped[T any, R any](expLeft, expRight map[low.KeyRefere return expChanges } +//// CheckMapForChangesUntyped checks a left and right low level map for any additions, subtractions or modifications to +//// values. The compareFunc can be generic and accept any type +//func CheckMapForChangesUntyped[T any, R any](expLeft, expRight map[low.KeyReference[string]]low.ValueReference[T], +// changes *[]*Change, label string, compareFunc func(l, r any) R) map[string]R { +// +// lHashes := make(map[string]string) +// rHashes := make(map[string]string) +// lValues := make(map[string]low.ValueReference[T]) +// rValues := make(map[string]low.ValueReference[T]) +// +// for k := range expLeft { +// lHashes[k.Value] = low.GenerateHashString(expLeft[k].Value) +// lValues[k.Value] = expLeft[k] +// } +// +// for k := range expRight { +// rHashes[k.Value] = low.GenerateHashString(expRight[k].Value) +// rValues[k.Value] = expRight[k] +// } +// +// expChanges := make(map[string]R) +// +// checkLeft := func(k string, doneChan chan bool, f, g map[string]string, p, h map[string]low.ValueReference[T]) { +// rhash := g[k] +// if rhash == "" { +// if p[k].GetValueNode().Value == "" { +// p[k].GetValueNode().Value = k // yes, a dirty thing, but it's clean for the consumer. +// } +// CreateChange(changes, ObjectRemoved, label, +// p[k].GetValueNode(), nil, true, +// p[k].GetValue(), nil) +// doneChan <- true +// return +// } +// if f[k] == g[k] { +// doneChan <- true +// return +// } +// // run comparison. +// expChanges[k] = compareFunc(p[k].Value, h[k].Value) +// doneChan <- true +// } +// +// doneChan := make(chan bool) +// count := 0 +// +// // check left example hashes +// for k := range lHashes { +// count++ +// go checkLeft(k, doneChan, lHashes, rHashes, lValues, rValues) +// } +// +// //check right example hashes +// for k := range rHashes { +// count++ +// go checkRightValue(k, doneChan, lHashes, rValues, changes, label) +// } +// +// // wait for all done signals. +// completed := 0 +// for completed < count { +// select { +// case <-doneChan: +// completed++ +// } +// } +// return expChanges +//} + func checkRightValue[T any](k string, doneChan chan bool, f map[string]string, p map[string]low.ValueReference[T], - changes *[]*Change, label string) { + changes *[]*Change, label string, lock *sync.Mutex) { lhash := f[k] if lhash == "" { if p[k].GetValueNode().Value == "" { - p[k].GetValueNode().Value = k + p[k].GetValueNode().Value = k // this is kinda dirty, but I don't want to duplicate code so sue me. } + lock.Lock() CreateChange(changes, ObjectAdded, label, nil, p[k].GetValueNode(), false, nil, p[k].GetValue()) + lock.Unlock() } doneChan <- true } diff --git a/what-changed/model/components.go b/what-changed/model/components.go index 960db69..d49b8a1 100644 --- a/what-changed/model/components.go +++ b/what-changed/model/components.go @@ -4,6 +4,7 @@ package model import ( + "github.com/pb33f/libopenapi/datamodel/low" v2 "github.com/pb33f/libopenapi/datamodel/low/v2" v3 "github.com/pb33f/libopenapi/datamodel/low/v3" "reflect" @@ -14,7 +15,7 @@ type ComponentsChanges struct { SchemaChanges map[string]*SchemaChanges ResponsesChanges map[string]*ResponseChanges ParameterChanges map[string]*ParameterChanges - ExamplesChanges map[string]*ExamplesChanges + ExamplesChanges map[string]*ExampleChanges RequestBodyChanges map[string]*RequestBodyChanges HeaderChanges map[string]*HeaderChanges SecuritySchemeChanges map[string]*SecuritySchemeChanges @@ -34,8 +35,8 @@ func CompareComponents(l, r any) *ComponentsChanges { reflect.TypeOf(&v2.ParameterDefinitions{}) == reflect.TypeOf(r) { lDef := l.(*v2.ParameterDefinitions) rDef := r.(*v2.ParameterDefinitions) - cc.ParameterChanges = CheckMapForChangesUntyped(lDef.Definitions, rDef.Definitions, &changes, - v3.ParametersLabel, CompareParameters) + cc.ParameterChanges = CheckMapForChanges(lDef.Definitions, rDef.Definitions, &changes, + v3.ParametersLabel, CompareParametersV2) } // Swagger Responses @@ -43,8 +44,8 @@ func CompareComponents(l, r any) *ComponentsChanges { reflect.TypeOf(&v2.ResponsesDefinitions{}) == reflect.TypeOf(r) { lDef := l.(*v2.ResponsesDefinitions) rDef := r.(*v2.ResponsesDefinitions) - cc.ResponsesChanges = CheckMapForChangesUntyped(lDef.Definitions, rDef.Definitions, &changes, - v3.ResponsesLabel, CompareResponse) + cc.ResponsesChanges = CheckMapForChanges(lDef.Definitions, rDef.Definitions, &changes, + v3.ResponsesLabel, CompareResponseV2) } // Swagger Schemas @@ -61,8 +62,121 @@ func CompareComponents(l, r any) *ComponentsChanges { reflect.TypeOf(&v2.SecurityDefinitions{}) == reflect.TypeOf(r) { lDef := l.(*v2.SecurityDefinitions) rDef := r.(*v2.SecurityDefinitions) - cc.SecuritySchemeChanges = CheckMapForChangesUntyped(lDef.Definitions, rDef.Definitions, &changes, - v3.SecurityDefinitionLabel, CompareSecuritySchemes) + cc.SecuritySchemeChanges = CheckMapForChanges(lDef.Definitions, rDef.Definitions, &changes, + v3.SecurityDefinitionLabel, CompareSecuritySchemesV2) + } + + // OpenAPI Components + if reflect.TypeOf(&v3.Components{}) == reflect.TypeOf(l) && + reflect.TypeOf(&v3.Components{}) == reflect.TypeOf(r) { + + lComponents := l.(*v3.Components) + rComponents := r.(*v3.Components) + + doneChan := make(chan componentComparison) + comparisons := 0 + + // run as fast as we can, thread all the things. + if !lComponents.Schemas.IsEmpty() || rComponents.Schemas.IsEmpty() { + comparisons++ + go runComparison(lComponents.Schemas.Value, rComponents.Schemas.Value, + &changes, v3.SchemasLabel, CompareSchemas, doneChan) + } + + if !lComponents.Responses.IsEmpty() || rComponents.Responses.IsEmpty() { + comparisons++ + go runComparison(lComponents.Responses.Value, rComponents.Responses.Value, + &changes, v3.ResponsesLabel, CompareResponseV3, doneChan) + } + + if !lComponents.Parameters.IsEmpty() || rComponents.Parameters.IsEmpty() { + comparisons++ + go runComparison(lComponents.Parameters.Value, rComponents.Parameters.Value, + &changes, v3.ParametersLabel, CompareParametersV3, doneChan) + } + + if !lComponents.Examples.IsEmpty() || rComponents.Examples.IsEmpty() { + comparisons++ + go runComparison(lComponents.Examples.Value, rComponents.Examples.Value, + &changes, v3.ExamplesLabel, CompareExamples, doneChan) + } + + if !lComponents.RequestBodies.IsEmpty() || rComponents.RequestBodies.IsEmpty() { + comparisons++ + go runComparison(lComponents.RequestBodies.Value, rComponents.RequestBodies.Value, + &changes, v3.RequestBodiesLabel, CompareRequestBodies, doneChan) + } + + if !lComponents.Headers.IsEmpty() || rComponents.Headers.IsEmpty() { + comparisons++ + go runComparison(lComponents.Headers.Value, rComponents.Headers.Value, + &changes, v3.HeadersLabel, CompareHeadersV3, doneChan) + } + + if !lComponents.SecuritySchemes.IsEmpty() || rComponents.SecuritySchemes.IsEmpty() { + comparisons++ + go runComparison(lComponents.SecuritySchemes.Value, rComponents.SecuritySchemes.Value, + &changes, v3.SecuritySchemesLabel, CompareSecuritySchemesV3, doneChan) + } + + if !lComponents.Links.IsEmpty() || rComponents.Links.IsEmpty() { + comparisons++ + go runComparison(lComponents.Links.Value, rComponents.Links.Value, + &changes, v3.LinksLabel, CompareLinks, doneChan) + } + + if !lComponents.Callbacks.IsEmpty() || rComponents.Callbacks.IsEmpty() { + comparisons++ + go runComparison(lComponents.Callbacks.Value, rComponents.Callbacks.Value, + &changes, v3.CallbacksLabel, CompareCallback, doneChan) + } + + cc.ExtensionChanges = CompareExtensions(lComponents.Extensions, rComponents.Extensions) + + completedComponents := 0 + for completedComponents < comparisons { + select { + case res := <-doneChan: + switch res.prop { + case v3.SchemasLabel: + completedComponents++ + cc.SchemaChanges = res.result.(map[string]*SchemaChanges) + break + case v3.ResponsesLabel: + completedComponents++ + cc.ResponsesChanges = res.result.(map[string]*ResponseChanges) + break + case v3.ParametersLabel: + completedComponents++ + cc.ParameterChanges = res.result.(map[string]*ParameterChanges) + break + case v3.ExamplesLabel: + completedComponents++ + cc.ExamplesChanges = res.result.(map[string]*ExampleChanges) + break + case v3.RequestBodiesLabel: + completedComponents++ + cc.RequestBodyChanges = res.result.(map[string]*RequestBodyChanges) + break + case v3.HeadersLabel: + completedComponents++ + cc.HeaderChanges = res.result.(map[string]*HeaderChanges) + break + case v3.SecuritySchemesLabel: + completedComponents++ + cc.SecuritySchemeChanges = res.result.(map[string]*SecuritySchemeChanges) + break + case v3.LinksLabel: + completedComponents++ + cc.LinkChanges = res.result.(map[string]*LinkChanges) + break + case v3.CallbacksLabel: + completedComponents++ + cc.CallbackChanges = res.result.(map[string]*CallbackChanges) + break + } + } + } } cc.Changes = changes @@ -72,6 +186,20 @@ func CompareComponents(l, r any) *ComponentsChanges { return cc } +type componentComparison struct { + prop string + result any +} + +// run a generic comparison in a thread which in turn splits checks into further threads. +func runComparison[T any, R any](l, r map[low.KeyReference[string]]low.ValueReference[T], + changes *[]*Change, label string, compareFunc func(l, r T) R, doneChan chan componentComparison) { + doneChan <- componentComparison{ + prop: label, + result: CheckMapForChanges(l, r, changes, label, compareFunc), + } +} + func (c *ComponentsChanges) TotalChanges() int { v := c.PropertyChanges.TotalChanges() for k := range c.SchemaChanges { diff --git a/what-changed/model/parameter.go b/what-changed/model/parameter.go index b1c6247..0f36c99 100644 --- a/what-changed/model/parameter.go +++ b/what-changed/model/parameter.go @@ -191,6 +191,14 @@ func addCommonParameterProperties(left, right low.SharedParameters, changes *[]* return props } +func CompareParametersV3(l, r *v3.Parameter) *ParameterChanges { + return CompareParameters(l, r) +} + +func CompareParametersV2(l, r *v2.Parameter) *ParameterChanges { + return CompareParameters(l, r) +} + func CompareParameters(l, r any) *ParameterChanges { var changes []*Change diff --git a/what-changed/model/security_scheme.go b/what-changed/model/security_scheme.go index 347422e..8a40a2b 100644 --- a/what-changed/model/security_scheme.go +++ b/what-changed/model/security_scheme.go @@ -44,6 +44,14 @@ func (ss *SecuritySchemeChanges) TotalBreakingChanges() int { return c } +func CompareSecuritySchemesV2(l, r *v2.SecurityScheme) *SecuritySchemeChanges { + return CompareSecuritySchemes(l, r) +} + +func CompareSecuritySchemesV3(l, r *v3.SecurityScheme) *SecuritySchemeChanges { + return CompareSecuritySchemes(l, r) +} + func CompareSecuritySchemes(l, r any) *SecuritySchemeChanges { var props []*PropertyCheck