Files
libopenapi/what-changed/schema.go
Dave Shanley b83e751aa7 Working through test coverage of schema proxy
Main logic is now in place, ready for fleshing out. Pattern feels right.
2022-10-09 19:33:58 -04:00

745 lines
20 KiB
Go

// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley
// SPDX-License-Identifier: MIT
package what_changed
import (
"fmt"
"github.com/pb33f/libopenapi/datamodel/low"
"github.com/pb33f/libopenapi/datamodel/low/base"
v3 "github.com/pb33f/libopenapi/datamodel/low/v3"
"sort"
"sync"
)
type SchemaChanges struct {
PropertyChanges[*base.Schema]
DiscriminatorChanges *DiscriminatorChanges
AllOfChanges []*SchemaChanges
AnyOfChanges []*SchemaChanges
OneOfChanges []*SchemaChanges
NotChanges []*SchemaChanges
ItemsChanges []*SchemaChanges
SchemaPropertyChanges map[string]*SchemaChanges
ExternalDocChanges *ExternalDocChanges
ExtensionChanges *ExtensionChanges
}
func (s *SchemaChanges) TotalChanges() int {
t := s.PropertyChanges.TotalChanges()
if s.DiscriminatorChanges != nil {
t += s.DiscriminatorChanges.TotalChanges()
}
if len(s.AllOfChanges) > 0 {
for n := range s.AllOfChanges {
t += s.AllOfChanges[n].TotalChanges()
}
}
if len(s.AnyOfChanges) > 0 {
for n := range s.AnyOfChanges {
t += s.AnyOfChanges[n].TotalChanges()
}
}
if len(s.OneOfChanges) > 0 {
for n := range s.OneOfChanges {
t += s.OneOfChanges[n].TotalChanges()
}
}
if len(s.NotChanges) > 0 {
for n := range s.NotChanges {
t += s.NotChanges[n].TotalChanges()
}
}
if len(s.ItemsChanges) > 0 {
for n := range s.ItemsChanges {
t += s.ItemsChanges[n].TotalChanges()
}
}
if s.SchemaPropertyChanges != nil {
for n := range s.SchemaPropertyChanges {
t += s.SchemaPropertyChanges[n].TotalChanges()
}
}
if s.ExternalDocChanges != nil {
t += s.ExternalDocChanges.TotalChanges()
}
if s.ExtensionChanges != nil {
t += s.ExtensionChanges.TotalChanges()
}
return t
}
func (s *SchemaChanges) TotalBreakingChanges() int {
t := s.PropertyChanges.TotalBreakingChanges()
if s.DiscriminatorChanges != nil {
t += s.DiscriminatorChanges.TotalBreakingChanges()
}
if len(s.AllOfChanges) > 0 {
for n := range s.AllOfChanges {
t += s.AllOfChanges[n].TotalBreakingChanges()
}
}
if len(s.AllOfChanges) > 0 {
for n := range s.AllOfChanges {
t += s.AllOfChanges[n].TotalBreakingChanges()
}
}
if len(s.AnyOfChanges) > 0 {
for n := range s.AnyOfChanges {
t += s.AnyOfChanges[n].TotalBreakingChanges()
}
}
if len(s.OneOfChanges) > 0 {
for n := range s.OneOfChanges {
t += s.OneOfChanges[n].TotalBreakingChanges()
}
}
if len(s.NotChanges) > 0 {
for n := range s.NotChanges {
t += s.NotChanges[n].TotalBreakingChanges()
}
}
if len(s.ItemsChanges) > 0 {
for n := range s.ItemsChanges {
t += s.ItemsChanges[n].TotalBreakingChanges()
}
}
if s.SchemaPropertyChanges != nil {
for n := range s.SchemaPropertyChanges {
t += s.SchemaPropertyChanges[n].TotalBreakingChanges()
}
}
if s.ExternalDocChanges != nil {
t += s.ExternalDocChanges.TotalBreakingChanges()
}
if s.ExtensionChanges != nil {
t += s.ExtensionChanges.TotalBreakingChanges()
}
return t
}
func CompareSchemas(l, r *base.SchemaProxy) *SchemaChanges {
sc := new(SchemaChanges)
var changes []*Change[*base.Schema]
// Added
if l == nil && r != nil {
CreateChange[*base.Schema](&changes, ObjectAdded, v3.SchemaLabel,
nil, nil, true, nil, r)
sc.Changes = changes
}
// Removed
if l != nil && r == nil {
CreateChange[*base.Schema](&changes, ObjectRemoved, v3.SchemaLabel,
nil, nil, true, l, nil)
sc.Changes = changes
}
if l != nil && r != nil {
// if left proxy is a reference and right is a reference (we won't recurse into them)
if l.IsSchemaReference() && r.IsSchemaReference() {
// points to the same schema
if l.GetSchemaReference() == r.GetSchemaReference() {
// there is nothing to be done at this point.
return nil
} else {
// references are different, that's all we care to know.
CreateChange[*base.Schema](&changes, Modified, v3.RefLabel,
l.GetValueNode().Content[1], r.GetValueNode().Content[1], true, l.GetSchemaReference(),
r.GetSchemaReference())
sc.Changes = changes
return sc
}
}
// changed from inline to ref
if !l.IsSchemaReference() && r.IsSchemaReference() {
CreateChange[*base.Schema](&changes, Modified, v3.RefLabel,
l.GetValueNode(), r.GetValueNode().Content[1], true, l, r.GetSchemaReference())
sc.Changes = changes
return sc // we're done here
}
// changed from ref to inline
if l.IsSchemaReference() && !r.IsSchemaReference() {
CreateChange[*base.Schema](&changes, Modified, v3.RefLabel,
l.GetValueNode().Content[1], r.GetValueNode(), true, l.GetSchemaReference(), r)
sc.Changes = changes
return sc // done, nothing else to do.
}
lSchema := l.Schema()
rSchema := r.Schema()
if low.AreEqual(lSchema, rSchema) {
// there is no point going on, we know nothing changed!
return nil
}
var props []*PropertyCheck[*base.Schema]
// $schema (breaking change)
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.SchemaTypeRef.ValueNode,
RightNode: rSchema.SchemaTypeRef.ValueNode,
Label: v3.SchemaDialectLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// ExclusiveMaximum
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.ExclusiveMaximum.ValueNode,
RightNode: rSchema.ExclusiveMaximum.ValueNode,
Label: v3.ExclusiveMaximumLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// ExclusiveMinimum
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.ExclusiveMinimum.ValueNode,
RightNode: rSchema.ExclusiveMinimum.ValueNode,
Label: v3.ExclusiveMinimumLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// Type
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Type.ValueNode,
RightNode: rSchema.Type.ValueNode,
Label: v3.TypeLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// Title
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Title.ValueNode,
RightNode: rSchema.Title.ValueNode,
Label: v3.TitleLabel,
Changes: &changes,
Breaking: false,
Original: lSchema,
New: rSchema,
})
// MultipleOf
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.MultipleOf.ValueNode,
RightNode: rSchema.MultipleOf.ValueNode,
Label: v3.MultipleOfLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// Maximum
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Maximum.ValueNode,
RightNode: rSchema.Maximum.ValueNode,
Label: v3.MaximumLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// Minimum
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Minimum.ValueNode,
RightNode: rSchema.Minimum.ValueNode,
Label: v3.MinimumLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// MaxLength
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.MaxLength.ValueNode,
RightNode: rSchema.MaxLength.ValueNode,
Label: v3.MaxLengthLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// MinLength
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.MinLength.ValueNode,
RightNode: rSchema.MinLength.ValueNode,
Label: v3.MinLengthLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// Pattern
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Pattern.ValueNode,
RightNode: rSchema.Pattern.ValueNode,
Label: v3.PatternLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// Format
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Format.ValueNode,
RightNode: rSchema.Format.ValueNode,
Label: v3.FormatLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// MaxItems
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.MaxItems.ValueNode,
RightNode: rSchema.MaxItems.ValueNode,
Label: v3.MaxItemsLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// MinItems
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.MinItems.ValueNode,
RightNode: rSchema.MinItems.ValueNode,
Label: v3.MinItemsLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// MaxProperties
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.MaxProperties.ValueNode,
RightNode: rSchema.MaxProperties.ValueNode,
Label: v3.MaxPropertiesLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// MinProperties
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.MinProperties.ValueNode,
RightNode: rSchema.MinProperties.ValueNode,
Label: v3.MinPropertiesLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// UniqueItems
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.UniqueItems.ValueNode,
RightNode: rSchema.UniqueItems.ValueNode,
Label: v3.UniqueItemsLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// AdditionalProperties
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.AdditionalProperties.ValueNode,
RightNode: rSchema.AdditionalProperties.ValueNode,
Label: v3.AdditionalPropertiesLabel,
Changes: &changes,
Breaking: false,
Original: lSchema,
New: rSchema,
})
// Description
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Description.ValueNode,
RightNode: rSchema.Description.ValueNode,
Label: v3.DescriptionLabel,
Changes: &changes,
Breaking: false,
Original: lSchema,
New: rSchema,
})
// ContentEncoding
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.ContentEncoding.ValueNode,
RightNode: rSchema.ContentEncoding.ValueNode,
Label: v3.ContentEncodingLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// ContentMediaType
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.ContentMediaType.ValueNode,
RightNode: rSchema.ContentMediaType.ValueNode,
Label: v3.ContentMediaType,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// Default
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Default.ValueNode,
RightNode: rSchema.Default.ValueNode,
Label: v3.DefaultLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// Nullable
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Nullable.ValueNode,
RightNode: rSchema.Nullable.ValueNode,
Label: v3.NullableLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// ReadOnly
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.ReadOnly.ValueNode,
RightNode: rSchema.ReadOnly.ValueNode,
Label: v3.ReadOnlyLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// WriteOnly
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.WriteOnly.ValueNode,
RightNode: rSchema.WriteOnly.ValueNode,
Label: v3.WriteOnlyLabel,
Changes: &changes,
Breaking: true,
Original: lSchema,
New: rSchema,
})
// Example
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Example.ValueNode,
RightNode: rSchema.Example.ValueNode,
Label: v3.ExampleLabel,
Changes: &changes,
Breaking: false,
Original: lSchema,
New: rSchema,
})
// Deprecated
props = append(props, &PropertyCheck[*base.Schema]{
LeftNode: lSchema.Deprecated.ValueNode,
RightNode: rSchema.Deprecated.ValueNode,
Label: v3.DeprecatedLabel,
Changes: &changes,
Breaking: false,
Original: lSchema,
New: rSchema,
})
// Required
j := make(map[string]int)
k := make(map[string]int)
for i := range lSchema.Required.Value {
j[lSchema.Required.Value[i].Value] = i
}
for i := range rSchema.Required.Value {
k[rSchema.Required.Value[i].Value] = i
}
for g := range k {
if _, ok := j[g]; !ok {
CreateChange[*base.Schema](&changes, PropertyAdded, v3.RequiredLabel,
nil, rSchema.Required.Value[k[g]].GetValueNode(), true, nil,
rSchema.Required.Value[k[g]].GetValue)
}
}
for g := range j {
if _, ok := k[g]; !ok {
CreateChange[*base.Schema](&changes, PropertyRemoved, v3.RequiredLabel,
lSchema.Required.Value[j[g]].GetValueNode(), nil, true, lSchema.Required.Value[j[g]].GetValue,
nil)
}
}
// Enums
j = make(map[string]int)
k = make(map[string]int)
for i := range lSchema.Enum.Value {
j[lSchema.Enum.Value[i].Value] = i
}
for i := range rSchema.Enum.Value {
k[rSchema.Enum.Value[i].Value] = i
}
for g := range k {
if _, ok := j[g]; !ok {
CreateChange[*base.Schema](&changes, PropertyAdded, v3.EnumLabel,
nil, rSchema.Enum.Value[k[g]].GetValueNode(), false, nil,
rSchema.Enum.Value[k[g]].GetValue)
}
}
for g := range j {
if _, ok := k[g]; !ok {
CreateChange[*base.Schema](&changes, PropertyRemoved, v3.EnumLabel,
lSchema.Enum.Value[j[g]].GetValueNode(), nil, true, lSchema.Enum.Value[j[g]].GetValue,
nil)
}
}
// check core properties
CheckProperties(props)
propChanges := make(map[string]*SchemaChanges)
lProps := make([]string, len(lSchema.Properties.Value))
lEntities := make(map[string]*base.SchemaProxy)
rProps := make([]string, len(rSchema.Properties.Value))
rEntities := make(map[string]*base.SchemaProxy)
for w := range lSchema.Properties.Value {
if !lSchema.Properties.Value[w].Value.IsSchemaReference() {
lProps = append(lProps, w.Value)
lEntities[w.Value] = lSchema.Properties.Value[w].Value
}
}
for w := range rSchema.Properties.Value {
if !rSchema.Properties.Value[w].Value.IsSchemaReference() {
rProps = append(rProps, w.Value)
rEntities[w.Value] = rSchema.Properties.Value[w].Value
}
}
sort.Strings(lProps)
sort.Strings(rProps)
var propLock sync.Mutex
checkProperty := func(key string, lp, rp *base.SchemaProxy, propChanges map[string]*SchemaChanges, done chan bool) {
if lp != nil && rp != nil {
ls := lp.Schema()
rs := rp.Schema()
if low.AreEqual(ls, rs) {
done <- true
return
}
s := CompareSchemas(lp, rp)
propLock.Lock()
propChanges[key] = s
propLock.Unlock()
done <- true
}
}
doneChan := make(chan bool)
totalProperties := 0
if len(lProps) == len(rProps) {
for w := range lProps {
lp := lEntities[lProps[w]]
rp := rEntities[rProps[w]]
if lProps[w] == rProps[w] && lp != nil && rp != nil {
totalProperties++
go checkProperty(lProps[w], lp, rp, propChanges, doneChan)
}
// keys do not match, even after sorting, means a like for like replacement.
if lProps[w] != rProps[w] {
// old removed, new added.
CreateChange[*base.Schema](&changes, ObjectAdded, v3.PropertiesLabel,
nil, rEntities[rProps[w]].GetValueNode(), false, nil, rEntities[rProps[w]])
CreateChange[*base.Schema](&changes, ObjectRemoved, v3.PropertiesLabel,
lEntities[lProps[w]].GetValueNode(), nil, true, lEntities[lProps[w]], nil)
}
}
}
// something removed
if len(lProps) > len(rProps) {
for w := range lProps {
if w < len(rProps) {
totalProperties++
go checkProperty(lProps[w], lEntities[lProps[w]], rEntities[rProps[w]], propChanges, doneChan)
}
if w >= len(rProps) {
CreateChange[*base.Schema](&changes, ObjectRemoved, v3.PropertiesLabel,
lEntities[lProps[w]].GetValueNode(), nil, true, lEntities[lProps[w]], nil)
}
}
}
// something added
if len(rProps) > len(lProps) {
for w := range rProps {
if w < len(lProps) {
totalProperties++
go checkProperty(rProps[w], lEntities[lProps[w]], rEntities[rProps[w]], propChanges, doneChan)
}
if w >= len(rProps) {
CreateChange[*base.Schema](&changes, ObjectAdded, v3.PropertiesLabel,
nil, rEntities[rProps[w]].GetValueNode(), false, nil, rEntities[rProps[w]])
}
}
}
sc.SchemaPropertyChanges = propChanges
// check polymorphic and multi-values async for speed.
go extractSchemaChanges(lSchema.OneOf.Value, rSchema.OneOf.Value, v3.OneOfLabel,
&sc.OneOfChanges, &changes, doneChan)
go extractSchemaChanges(lSchema.AllOf.Value, rSchema.AllOf.Value, v3.AllOfLabel,
&sc.AllOfChanges, &changes, doneChan)
go extractSchemaChanges(lSchema.AnyOf.Value, rSchema.AnyOf.Value, v3.AnyOfLabel,
&sc.AnyOfChanges, &changes, doneChan)
go extractSchemaChanges(lSchema.Items.Value, rSchema.Items.Value, v3.ItemsLabel,
&sc.ItemsChanges, &changes, doneChan)
go extractSchemaChanges(lSchema.Not.Value, rSchema.Not.Value, v3.ItemsLabel,
&sc.ItemsChanges, &changes, doneChan)
totalChecks := totalProperties + 5
completedChecks := 0
for completedChecks < totalChecks {
select {
case <-doneChan:
completedChecks++
}
}
}
// done
sc.Changes = changes
if sc.TotalChanges() <= 0 {
return nil
}
return sc
}
func extractSchemaChanges(
lSchema []low.ValueReference[*base.SchemaProxy],
rSchema []low.ValueReference[*base.SchemaProxy],
label string,
sc *[]*SchemaChanges,
changes *[]*Change[*base.Schema],
done chan bool) {
// if there is nothing here, there is nothing to do.
if lSchema == nil && rSchema == nil {
done <- true
return
}
x := "%x"
// create hash key maps to check equality
lKeys := make([]string, 0, len(lSchema))
rKeys := make([]string, 0, len(rSchema))
lEntities := make(map[string]*base.SchemaProxy)
rEntities := make(map[string]*base.SchemaProxy)
for h := range lSchema {
q := lSchema[h].Value
if !q.IsSchemaReference() {
w := q.Schema()
z := fmt.Sprintf(x, w.Hash())
lKeys = append(lKeys, z)
lEntities[z] = q
}
}
for h := range rSchema {
q := rSchema[h].Value
if !q.IsSchemaReference() {
w := q.Schema()
z := fmt.Sprintf(x, w.Hash())
rKeys = append(rKeys, z)
rEntities[z] = q
}
}
if len(lKeys) <= 0 && len(rKeys) <= 0 {
done <- true
return
}
// sort slices so that like for like is all sequenced.
sort.Strings(lKeys)
sort.Strings(rKeys)
// check for identical lengths
if len(lKeys) == len(rKeys) {
for w := range lKeys {
// keys are different, which means there are changes.
if lKeys[w] != rKeys[w] {
*sc = append(*sc, CompareSchemas(lEntities[lKeys[w]], rEntities[rKeys[w]]))
}
}
}
// things were removed
if len(lKeys) > len(rKeys) {
for w := range lKeys {
if w < len(rKeys) && lKeys[w] != rKeys[w] {
*sc = append(*sc, CompareSchemas(lEntities[lKeys[w]], rEntities[rKeys[w]]))
}
if w >= len(rKeys) {
CreateChange[*base.Schema](changes, ObjectRemoved, label,
lEntities[lKeys[w]].GetValueNode(), nil, true, lEntities[lKeys[w]], nil)
}
}
}
// things were added
if len(rKeys) > len(lKeys) {
for w := range rKeys {
if w < len(lKeys) && rKeys[w] != lKeys[w] {
*sc = append(*sc, CompareSchemas(lEntities[lKeys[w]], rEntities[rKeys[w]]))
}
if w >= len(lKeys) {
CreateChange[*base.Schema](changes, ObjectAdded, label,
nil, rEntities[rKeys[w]].GetValueNode(), false, nil, rEntities[rKeys[w]])
}
}
}
done <- true
}