Files
libopenapi/what-changed/model/comparison_functions.go
2023-12-14 17:42:32 +00:00

451 lines
15 KiB
Go

// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
package model
import (
"fmt"
"reflect"
"strings"
"sync"
"github.com/pb33f/libopenapi/orderedmap"
"github.com/pb33f/libopenapi/utils"
"github.com/pb33f/libopenapi/datamodel/low"
"gopkg.in/yaml.v3"
)
const (
HashPh = "%x"
EMPTY_STR = ""
)
var changeMutex sync.Mutex
// CreateChange is a generic function that will create a Change of type T, populate all properties if set, and then
// add a pointer to Change[T] in the slice of Change pointers provided
func CreateChange(changes *[]*Change, changeType int, property string, leftValueNode, rightValueNode *yaml.Node,
breaking bool, originalObject, newObject any,
) *[]*Change {
// create a new context for the left and right nodes.
ctx := CreateContext(leftValueNode, rightValueNode)
c := &Change{
Context: ctx,
ChangeType: changeType,
Property: property,
Breaking: breaking,
}
// if the left is not nil, we have an original value
if leftValueNode != nil && leftValueNode.Value != "" {
c.Original = leftValueNode.Value
}
// if the right is not nil, then we have a new value
if rightValueNode != nil && rightValueNode.Value != "" {
c.New = rightValueNode.Value
}
// original and new objects
c.OriginalObject = originalObject
c.NewObject = newObject
// add the change to supplied changes slice
changeMutex.Lock()
*changes = append(*changes, c)
changeMutex.Unlock()
return changes
}
// CreateContext will return a pointer to a ChangeContext containing the original and new line and column numbers
// of the left and right value nodes.
func CreateContext(l, r *yaml.Node) *ChangeContext {
ctx := new(ChangeContext)
if l != nil {
ctx.OriginalLine = &l.Line
ctx.OriginalColumn = &l.Column
}
if r != nil {
ctx.NewLine = &r.Line
ctx.NewColumn = &r.Column
}
return ctx
}
func FlattenLowLevelOrderedMap[T any](
lowMap *orderedmap.Map[low.KeyReference[string], low.ValueReference[T]],
) map[string]*low.ValueReference[T] {
flat := make(map[string]*low.ValueReference[T])
for pair := orderedmap.First(lowMap); pair != nil; pair = pair.Next() {
k := pair.Key()
l := pair.Value()
flat[k.Value] = &l
}
return flat
}
// CountBreakingChanges counts the number of changes in a slice that are breaking
func CountBreakingChanges(changes []*Change) int {
b := 0
for i := range changes {
if changes[i].Breaking {
b++
}
}
return b
}
// CheckForObjectAdditionOrRemoval will check for the addition or removal of an object from left and right maps.
// The label is the key to look for in the left and right maps.
//
// To determine this a breaking change for an addition then set breakingAdd to true (however I can't think of many
// scenarios that adding things should break anything). Removals are generally breaking, except for non contract
// properties like descriptions, summaries and other non-binding values, so a breakingRemove value can be tuned for
// these circumstances.
func CheckForObjectAdditionOrRemoval[T any](l, r map[string]*low.ValueReference[T], label string, changes *[]*Change,
breakingAdd, breakingRemove bool,
) {
var left, right T
if CheckSpecificObjectRemoved(l, r, label) {
left = l[label].GetValue()
CreateChange(changes, ObjectRemoved, label, l[label].GetValueNode(), nil,
breakingRemove, left, nil)
}
if CheckSpecificObjectAdded(l, r, label) {
right = r[label].GetValue()
CreateChange(changes, ObjectAdded, label, nil, r[label].GetValueNode(),
breakingAdd, nil, right)
}
}
// CheckSpecificObjectRemoved returns true if a specific value is not in both maps.
func CheckSpecificObjectRemoved[T any](l, r map[string]*T, label string) bool {
return l[label] != nil && r[label] == nil
}
// CheckSpecificObjectAdded returns true if a specific value is not in both maps.
func CheckSpecificObjectAdded[T any](l, r map[string]*T, label string) bool {
return l[label] == nil && r[label] != nil
}
// CheckProperties will iterate through a slice of PropertyCheck pointers of type T. The method is a convenience method
// for running checks on the following methods in order:
//
// CheckPropertyAdditionOrRemoval
// CheckForModification
func CheckProperties(properties []*PropertyCheck) {
// todo: make this async to really speed things up.
for _, n := range properties {
CheckPropertyAdditionOrRemoval(n.LeftNode, n.RightNode, n.Label, n.Changes, n.Breaking, n.Original, n.New)
CheckForModification(n.LeftNode, n.RightNode, n.Label, n.Changes, n.Breaking, n.Original, n.New)
}
}
// CheckPropertyAdditionOrRemoval will run both CheckForRemoval (first) and CheckForAddition (second)
func CheckPropertyAdditionOrRemoval[T any](l, r *yaml.Node,
label string, changes *[]*Change, breaking bool, orig, new T,
) {
CheckForRemoval[T](l, r, label, changes, breaking, orig, new)
CheckForAddition[T](l, r, label, changes, breaking, orig, new)
}
// CheckForRemoval will check left and right yaml.Node instances for changes. Anything that is found missing on the
// right, but present on the left, is considered a removal. A new Change[T] will be created with the type
//
// PropertyRemoved
//
// The Change is then added to the slice of []Change[T] instances provided as a pointer.
func CheckForRemoval[T any](l, r *yaml.Node, label string, changes *[]*Change, breaking bool, orig, new T) {
if l != nil && l.Value != "" && (r == nil || r.Value == "" && !utils.IsNodeArray(r) && !utils.IsNodeMap(r)) {
CreateChange(changes, PropertyRemoved, label, l, r, breaking, orig, new)
return
}
if l != nil && r == nil {
CreateChange(changes, PropertyRemoved, label, l, nil, breaking, orig, nil)
}
}
// CheckForAddition will check left and right yaml.Node instances for changes. Anything that is found missing on the
// left, but present on the left, is considered an addition. A new Change[T] will be created with the type
//
// PropertyAdded
//
// The Change is then added to the slice of []Change[T] instances provided as a pointer.
func CheckForAddition[T any](l, r *yaml.Node, label string, changes *[]*Change, breaking bool, orig, new T) {
if (l == nil || l.Value == "") && (r != nil && (r.Value != "" || utils.IsNodeArray(r)) || utils.IsNodeMap(r)) {
if r != nil {
if l != nil && (len(l.Content) < len(r.Content)) && len(l.Content) <= 0 {
CreateChange(changes, PropertyAdded, label, l, r, breaking, orig, new)
}
if l == nil {
CreateChange(changes, PropertyAdded, label, l, r, breaking, orig, new)
}
}
}
}
// CheckForModification will check left and right yaml.Node instances for changes. Anything that is found in both
// sides, but vary in value is considered a modification.
//
// If there is a change in value the function adds a change type of Modified.
//
// The Change is then added to the slice of []Change[T] instances provided as a pointer.
func CheckForModification[T any](l, r *yaml.Node, label string, changes *[]*Change, breaking bool, orig, new T) {
if l != nil && l.Value != "" && r != nil && r.Value != "" && (r.Value != l.Value || r.Tag != l.Tag) {
CreateChange(changes, Modified, label, l, r, breaking, orig, new)
return
}
if l != nil && utils.IsNodeArray(l) && r != nil && !utils.IsNodeArray(r) {
CreateChange(changes, Modified, label, l, r, breaking, orig, new)
return
}
if l != nil && !utils.IsNodeArray(l) && r != nil && utils.IsNodeArray(r) {
CreateChange(changes, Modified, label, l, r, breaking, orig, new)
return
}
if l != nil && utils.IsNodeMap(l) && r != nil && !utils.IsNodeMap(r) {
CreateChange(changes, Modified, label, l, r, breaking, orig, new)
return
}
if l != nil && !utils.IsNodeMap(l) && r != nil && utils.IsNodeMap(r) {
CreateChange(changes, Modified, label, l, r, breaking, orig, new)
return
}
if l != nil && utils.IsNodeArray(l) && r != nil && utils.IsNodeArray(r) {
if len(l.Content) != len(r.Content) {
CreateChange(changes, Modified, label, l, r, breaking, orig, new)
return
}
// there is no way to know how to compare the content of the array, without
// rendering the yaml.Node to a string and comparing the string.
leftBytes, _ := yaml.Marshal(l)
rightBytes, _ := yaml.Marshal(r)
if string(leftBytes) != string(rightBytes) {
CreateChange(changes, Modified, label, l, r, breaking, orig, new)
}
return
}
if l != nil && utils.IsNodeMap(l) && r != nil && utils.IsNodeMap(r) {
// there is no way to know how to compare the content of the map, without
// rendering the yaml.Node to a string and comparing the string.
leftBytes, _ := yaml.Marshal(l)
rightBytes, _ := yaml.Marshal(r)
if string(leftBytes) != string(rightBytes) {
CreateChange(changes, Modified, label, l, r, breaking, orig, new)
}
return
}
}
// CheckMapForChanges checks a left and right low level map for any additions, subtractions or modifications to
// values. The compareFunc argument should reference the correct comparison function for the generic type.
func CheckMapForChanges[T any, R any](expLeft, expRight *orderedmap.Map[low.KeyReference[string], low.ValueReference[T]],
changes *[]*Change, label string, compareFunc func(l, r T) R,
) map[string]R {
return CheckMapForChangesWithComp(expLeft, expRight, changes, label, compareFunc, true)
}
// CheckMapForAdditionRemoval checks a left and right low level map for any additions or subtractions, but not modifications
func CheckMapForAdditionRemoval[T any](expLeft, expRight *orderedmap.Map[low.KeyReference[string], low.ValueReference[T]],
changes *[]*Change, label string,
) any {
// do nothing
doNothing := func(l, r T) any {
return nil
}
// Adding purely to make sure code is called for coverage.
var l, r T
doNothing(l, r)
// end of coverage code.
return CheckMapForChangesWithComp(expLeft, expRight, changes, label, doNothing, false)
}
// CheckMapForChangesWithComp checks a left and right low level map for any additions, subtractions or modifications to
// values. The compareFunc argument should reference the correct comparison function for the generic type. The compare
// bit determines if the comparison should be run or not.
func CheckMapForChangesWithComp[T any, R any](expLeft, expRight *orderedmap.Map[low.KeyReference[string], low.ValueReference[T]],
changes *[]*Change, label string, compareFunc func(l, r T) R, compare bool,
) map[string]R {
// stop concurrent threads screwing up changes.
var chLock sync.Mutex
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 pair := orderedmap.First(expLeft); pair != nil; pair = pair.Next() {
k := pair.Key()
lHashes[k.Value] = low.GenerateHashString(pair.Value().Value)
lValues[k.Value] = pair.Value()
}
for pair := orderedmap.First(expRight); pair != nil; pair = pair.Next() {
k := pair.Key()
rHashes[k.Value] = low.GenerateHashString(pair.Value().Value)
rValues[k.Value] = pair.Value()
}
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 == "" {
chLock.Lock()
if p[k].GetValueNode().Value == "" {
p[k].GetValueNode().Value = k
}
CreateChange(changes, ObjectRemoved, label,
p[k].GetValueNode(), nil, true,
p[k].GetValue(), nil)
chLock.Unlock()
doneChan <- true
return
}
if f[k] == g[k] {
doneChan <- true
return
}
// run comparison.
if compare {
chLock.Lock()
ch := compareFunc(p[k].Value, h[k].Value)
// incorrect map results were being generated causing panics.
// https://github.com/pb33f/libopenapi/issues/61
if !reflect.ValueOf(&ch).Elem().IsZero() {
expChanges[k] = ch
}
chLock.Unlock()
}
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, &chLock)
}
// 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, lock *sync.Mutex,
) {
lhash := f[k]
if lhash == "" {
lock.Lock()
if p[k].GetValueNode().Value == "" {
p[k].GetValueNode().Value = k // this is kinda dirty, but I don't want to duplicate code so sue me.
}
CreateChange(changes, ObjectAdded, label,
nil, p[k].GetValueNode(), false,
nil, p[k].GetValue())
lock.Unlock()
}
doneChan <- true
}
// ExtractStringValueSliceChanges will compare two low level string slices for changes.
func ExtractStringValueSliceChanges(lParam, rParam []low.ValueReference[string],
changes *[]*Change, label string, breaking bool,
) {
lKeys := make([]string, len(lParam))
rKeys := make([]string, len(rParam))
lValues := make(map[string]low.ValueReference[string])
rValues := make(map[string]low.ValueReference[string])
for i := range lParam {
lKeys[i] = strings.ToLower(lParam[i].Value)
lValues[lKeys[i]] = lParam[i]
}
for i := range rParam {
rKeys[i] = strings.ToLower(rParam[i].Value)
rValues[rKeys[i]] = rParam[i]
}
for i := range lValues {
if _, ok := rValues[i]; !ok {
CreateChange(changes, PropertyRemoved, label,
lValues[i].ValueNode,
nil,
breaking,
lValues[i].Value,
nil)
}
}
for i := range rValues {
if _, ok := lValues[i]; !ok {
CreateChange(changes, PropertyAdded, label,
nil,
rValues[i].ValueNode,
false,
nil,
rValues[i].Value)
}
}
}
func toString(v any) string {
if y, ok := v.(*yaml.Node); ok {
_ = y.Encode(&v)
}
return fmt.Sprint(v)
}
// ExtractRawValueSliceChanges will compare two low level interface{} slices for changes.
func ExtractRawValueSliceChanges[T any](lParam, rParam []low.ValueReference[T],
changes *[]*Change, label string, breaking bool,
) {
lKeys := make([]string, len(lParam))
rKeys := make([]string, len(rParam))
lValues := make(map[string]low.ValueReference[T])
rValues := make(map[string]low.ValueReference[T])
for i := range lParam {
lKeys[i] = strings.ToLower(toString(lParam[i].Value))
lValues[lKeys[i]] = lParam[i]
}
for i := range rParam {
rKeys[i] = strings.ToLower(toString(rParam[i].Value))
rValues[rKeys[i]] = rParam[i]
}
for i := range lValues {
if _, ok := rValues[i]; !ok {
CreateChange(changes, PropertyRemoved, label,
lValues[i].ValueNode,
nil,
breaking,
lValues[i].Value,
nil)
}
}
for i := range rValues {
if _, ok := lValues[i]; !ok {
CreateChange(changes, PropertyAdded, label,
nil,
rValues[i].ValueNode,
false,
nil,
rValues[i].Value)
}
}
}