mirror of
https://github.com/LukeHagar/libopenapi.git
synced 2025-12-07 04:20:14 +00:00
This is a large update, I realized that extensions are not being hashed correctly, and because I have the same code everywhere, it means running back through the stack and cleaning up the invalid code that will break if multiple extensions are used in different positions in the raw spec. At the same time, I realized that the v2 model has the same primitive/enum issues that are part cleaned up in v3. This is a breaking changhe because enums are now []any and not []string, as well as primitives for bool, int etc are all pointers now instead of the copied values. This will break any consumers.
320 lines
10 KiB
Go
320 lines
10 KiB
Go
// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package model
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/pb33f/libopenapi/datamodel/low"
|
|
"gopkg.in/yaml.v3"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
HashPh = "%x"
|
|
EMPTY_STR = ""
|
|
)
|
|
|
|
// 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
|
|
*changes = append(*changes, c)
|
|
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
|
|
} else {
|
|
ctx.OriginalLine = -1
|
|
ctx.OriginalColumn = -1
|
|
}
|
|
if r != nil {
|
|
ctx.NewLine = r.Line
|
|
ctx.NewColumn = r.Column
|
|
} else {
|
|
ctx.NewLine = -1
|
|
ctx.NewColumn = -1
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
func FlattenLowLevelMap[T any](
|
|
lowMap map[low.KeyReference[string]]low.ValueReference[T]) map[string]*low.ValueReference[T] {
|
|
flat := make(map[string]*low.ValueReference[T])
|
|
for i := range lowMap {
|
|
l := lowMap[i]
|
|
flat[i.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, right)
|
|
}
|
|
if CheckSpecificObjectAdded(l, r, label) {
|
|
right = r[label].GetValue()
|
|
CreateChange(changes, ObjectAdded, label, nil, r[label].GetValueNode(),
|
|
breakingAdd, left, 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 == "") {
|
|
CreateChange(changes, PropertyRemoved, label, l, r, breaking, orig, new)
|
|
}
|
|
}
|
|
|
|
// 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 != "" {
|
|
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)
|
|
}
|
|
// the values may have not changed, but the tag (node type) type may have
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 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 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)
|
|
|
|
// check left example hashes
|
|
for k := range lHashes {
|
|
rhash := rHashes[k]
|
|
if rhash == "" {
|
|
CreateChange(changes, ObjectRemoved, label,
|
|
lValues[k].GetValueNode(), nil, false,
|
|
lValues[k].GetValue(), nil)
|
|
continue
|
|
}
|
|
if lHashes[k] == rHashes[k] {
|
|
continue
|
|
}
|
|
// run comparison.
|
|
expChanges[k] = compareFunc(lValues[k].Value, rValues[k].Value)
|
|
}
|
|
|
|
//check right example hashes
|
|
for k := range rHashes {
|
|
lhash := lHashes[k]
|
|
if lhash == "" {
|
|
CreateChange(changes, ObjectAdded, label,
|
|
nil, lValues[k].GetValueNode(), false,
|
|
nil, lValues[k].GetValue())
|
|
continue
|
|
}
|
|
}
|
|
return expChanges
|
|
}
|
|
|
|
//func CompareMapStrings(l, r low.ValueReference[string]) *Change {
|
|
//
|
|
// if l.Value != r.Value {
|
|
// return &Change{
|
|
// Context: CreateContext(l.ValueNode, r.ValueNode),
|
|
// ChangeType: Modified,
|
|
// Original: l.Value,
|
|
// New: r.Value,
|
|
// Breaking: false,
|
|
// }
|
|
// }
|
|
// return nil
|
|
//}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ExtractRawValueSliceChanges will compare two low level interface{} slices for changes.
|
|
func ExtractRawValueSliceChanges(lParam, rParam []low.ValueReference[any],
|
|
changes *[]*Change, label string, breaking bool) {
|
|
lKeys := make([]string, len(lParam))
|
|
rKeys := make([]string, len(rParam))
|
|
lValues := make(map[string]low.ValueReference[any])
|
|
rValues := make(map[string]low.ValueReference[any])
|
|
for i := range lParam {
|
|
lKeys[i] = strings.ToLower(fmt.Sprint(lParam[i].Value))
|
|
lValues[lKeys[i]] = lParam[i]
|
|
}
|
|
for i := range rParam {
|
|
rKeys[i] = strings.ToLower(fmt.Sprint(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)
|
|
}
|
|
}
|
|
}
|