Adding some edge cases to what-changed code

Some specs were blowing things up, some deeper logic was required.

Signed-off-by: Dave Shanley <dave@quobix.com>
This commit is contained in:
Dave Shanley
2023-07-17 17:52:43 -04:00
committed by quobix
parent 1d6048dd42
commit 0fbcb81015
2 changed files with 281 additions and 2 deletions

View File

@@ -5,6 +5,7 @@ package model
import ( import (
"fmt" "fmt"
"github.com/pb33f/libopenapi/utils"
"reflect" "reflect"
"strings" "strings"
"sync" "sync"
@@ -160,8 +161,15 @@ func CheckForRemoval[T any](l, r *yaml.Node, label string, changes *[]*Change, b
// //
// The Change is then added to the slice of []Change[T] instances provided as a pointer. // 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) { 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 != "" { if (l == nil || l.Value == "") && (r != nil && (r.Value != "" || utils.IsNodeArray(r)) || utils.IsNodeMap(r)) {
CreateChange(changes, PropertyAdded, label, l, r, breaking, orig, new) if r != nil {
if l != nil && len(l.Content) < len(r.Content) {
CreateChange(changes, PropertyAdded, label, l, r, breaking, orig, new)
}
if l == nil {
CreateChange(changes, PropertyAdded, label, l, r, breaking, orig, new)
}
}
} }
} }
@@ -174,6 +182,46 @@ func CheckForAddition[T any](l, r *yaml.Node, label string, changes *[]*Change,
func CheckForModification[T any](l, r *yaml.Node, label string, changes *[]*Change, breaking bool, orig, new T) { 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) { 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) 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.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) {
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
} }
} }

View File

@@ -2468,3 +2468,234 @@ components:
assert.Equal(t, 1, changes.TotalBreakingChanges()) assert.Equal(t, 1, changes.TotalBreakingChanges())
} }
func TestCompareSchemas_Schema_AddExamplesArray_AllOf(t *testing.T) {
left := `openapi: 3.0
components:
schemas:
SomePayload:
type: object
allOf:
- type: array
items:
type: string
example: [ "a", "b", "c" ]`
right := `openapi: 3.0
components:
schemas:
SomePayload:
type: object
allOf:
- type: array
items:
type: string
example: [ "a", "b", "c","d","e"]`
leftDoc, rightDoc := test_BuildDoc(left, right)
// extract left reference schema and non reference schema.
changes := CompareDocuments(leftDoc, rightDoc)
assert.NotNil(t, changes)
assert.Equal(t, 1, changes.TotalChanges())
assert.Len(t, changes.GetAllChanges(), 1)
assert.Equal(t, 0, changes.TotalBreakingChanges())
}
func TestCompareSchemas_Schema_AddExampleMap_AllOf(t *testing.T) {
left := `openapi: 3.0
components:
schemas:
SomePayload:
type: object
description: payload thing
allOf:
- type: object
description: allOf thing
example:
- name: chicken`
right := `openapi: 3.0
components:
schemas:
SomePayload:
type: object
description: payload thing
allOf:
- type: object
description: allOf thing
example:
- name: nuggets`
leftDoc, rightDoc := test_BuildDoc(left, right)
// extract left reference schema and non reference schema.
changes := CompareDocuments(leftDoc, rightDoc)
assert.NotNil(t, changes)
assert.Equal(t, 1, changes.TotalChanges())
assert.Len(t, changes.GetAllChanges(), 1)
assert.Equal(t, 0, changes.TotalBreakingChanges())
}
func TestCompareSchemas_Schema_AddExamplesArray(t *testing.T) {
left := `openapi: 3.0
components:
schemas:
SomePayload:
type: object
oneOf:
- type: array
items:
type: string
example: [ "a", "b", "c" ]`
right := `openapi: 3.0
components:
schemas:
SomePayload:
type: object
oneOf:
- type: array
items:
type: string
example: [ "a", "b", "c","d","e"]`
leftDoc, rightDoc := test_BuildDoc(left, right)
// extract left reference schema and non reference schema.
changes := CompareDocuments(leftDoc, rightDoc)
assert.NotNil(t, changes)
assert.Equal(t, 1, changes.TotalChanges())
assert.Len(t, changes.GetAllChanges(), 1)
assert.Equal(t, 0, changes.TotalBreakingChanges())
}
func TestCompareSchemas_Schema_AddExamplesMap(t *testing.T) {
left := `openapi: 3.0
components:
schemas:
SomePayload:
type: object
oneOf:
- type: array
items:
type: string
example:
oh: my`
right := `openapi: 3.0
components:
schemas:
SomePayload:
type: object
oneOf:
- type: array
items:
type: string
example:
oh: why`
leftDoc, rightDoc := test_BuildDoc(left, right)
// extract left reference schema and non reference schema.
changes := CompareDocuments(leftDoc, rightDoc)
assert.NotNil(t, changes)
assert.Equal(t, 1, changes.TotalChanges())
assert.Len(t, changes.GetAllChanges(), 1)
assert.Equal(t, 0, changes.TotalBreakingChanges())
}
func TestCompareSchemas_Schema_AddExamples(t *testing.T) {
left := `openapi: 3.0
components:
schemas:
containerShared:
description: Shared properties by request payload and response
type: object
properties:
close_time:
example: '2020-07-09T00:17:55Z'
container_type:
type: string
enum:
- default
- case
example: default
custom_fields:
type:
- array
- 'null'
items:
type: object`
right := `openapi: 3.0
components:
schemas:
containerShared:
description: Shared properties by request payload and response
type: object
properties:
close_time:
example: '2020-07-09T00:17:55Z'
container_type:
type: string
enum:
- default
- case
example: default
custom_fields:
type:
- array
- 'null'
items:
type: object
example:
- name: auditedAt
source: global
dataType: text
requiredToResolve: false`
leftDoc, rightDoc := test_BuildDoc(left, right)
// extract left reference schema and non reference schema.
changes := CompareDocuments(leftDoc, rightDoc)
assert.NotNil(t, changes)
assert.Equal(t, 1, changes.TotalChanges())
assert.Len(t, changes.GetAllChanges(), 1)
assert.Equal(t, 0, changes.TotalBreakingChanges())
}
/*
containerShared:
description: Shared properties by request payload and response
type: object
properties:
close_time:
description: Date and time (in UTC) when the container was closed.
type: string
format: date-time
example: '2020-07-09T00:17:55Z'
container_type:
description: |-
The container type. Valid values are 'default' or 'case'. Containers with the 'default'
type are events in the user interface.
type: string
enum:
- default
- case
example: default
custom_fields:
type:
- array
- 'null'
description: |-
JSON objects contains key/value pairs for custom container fields. There may be
required fields defined in the administration settings. See the Administrator's Guide for
details.
items:
$ref: '#/components/schemas/custom-field'
example:
- name: auditedAt
source: global
dataType: text
requiredToResolve: false
*/