// Copyright 2022 Princess B33f Heavy Industries / Dave Shanley // SPDX-License-Identifier: MIT package model import ( "github.com/pb33f/libopenapi/datamodel/low" "github.com/pb33f/libopenapi/datamodel/low/base" v2 "github.com/pb33f/libopenapi/datamodel/low/v2" v3 "github.com/pb33f/libopenapi/datamodel/low/v3" "gopkg.in/yaml.v3" "reflect" ) // ParameterChanges represents changes found between Swagger or OpenAPI Parameter objects. type ParameterChanges struct { *PropertyChanges SchemaChanges *SchemaChanges `json:"schemas,omitempty" yaml:"schemas,omitempty"` ExtensionChanges *ExtensionChanges `json:"extensions,omitempty" yaml:"extensions,omitempty"` // Swagger supports Items. ItemsChanges *ItemsChanges `json:"items,omitempty" yaml:"items,omitempty"` // OpenAPI supports examples and content types. ExamplesChanges map[string]*ExampleChanges `json:"examples,omitempty" yaml:"examples,omitempty"` ContentChanges map[string]*MediaTypeChanges `json:"content,omitempty" yaml:"content,omitempty"` } // GetAllChanges returns a slice of all changes made between Parameter objects func (p *ParameterChanges) GetAllChanges() []*Change { var changes []*Change changes = append(changes, p.Changes...) if p.SchemaChanges != nil { changes = append(changes, p.SchemaChanges.GetAllChanges()...) } for i := range p.ExamplesChanges { changes = append(changes, p.ExamplesChanges[i].GetAllChanges()...) } if p.ItemsChanges != nil { changes = append(changes, p.ItemsChanges.GetAllChanges()...) } if p.ExtensionChanges != nil { changes = append(changes, p.ExtensionChanges.GetAllChanges()...) } for i := range p.ContentChanges { changes = append(changes, p.ContentChanges[i].GetAllChanges()...) } return changes } // TotalChanges returns a count of everything that changed func (p *ParameterChanges) TotalChanges() int { c := p.PropertyChanges.TotalChanges() if p.SchemaChanges != nil { c += p.SchemaChanges.TotalChanges() } for i := range p.ExamplesChanges { c += p.ExamplesChanges[i].TotalChanges() } if p.ItemsChanges != nil { c += p.ItemsChanges.TotalChanges() } if p.ExtensionChanges != nil { c += p.ExtensionChanges.TotalChanges() } for i := range p.ContentChanges { c += p.ContentChanges[i].TotalChanges() } return c } // TotalBreakingChanges always returns 0 for ExternalDoc objects, they are non-binding. func (p *ParameterChanges) TotalBreakingChanges() int { c := p.PropertyChanges.TotalBreakingChanges() if p.SchemaChanges != nil { c += p.SchemaChanges.TotalBreakingChanges() } if p.ItemsChanges != nil { c += p.ItemsChanges.TotalBreakingChanges() } for i := range p.ContentChanges { c += p.ContentChanges[i].TotalBreakingChanges() } return c } func addPropertyCheck(props *[]*PropertyCheck, lvn, rvn *yaml.Node, lv, rv any, changes *[]*Change, label string, breaking bool) { *props = append(*props, &PropertyCheck{ LeftNode: lvn, RightNode: rvn, Label: label, Changes: changes, Breaking: breaking, Original: lv, New: rv, }) } func addOpenAPIParameterProperties(left, right low.OpenAPIParameter, changes *[]*Change) []*PropertyCheck { var props []*PropertyCheck // style addPropertyCheck(&props, left.GetStyle().ValueNode, right.GetStyle().ValueNode, left.GetStyle(), right.GetStyle(), changes, v3.StyleLabel, false) // allow reserved addPropertyCheck(&props, left.GetAllowReserved().ValueNode, right.GetAllowReserved().ValueNode, left.GetAllowReserved(), right.GetAllowReserved(), changes, v3.AllowReservedLabel, true) // explode addPropertyCheck(&props, left.GetExplode().ValueNode, right.GetExplode().ValueNode, left.GetExplode(), right.GetExplode(), changes, v3.ExplodeLabel, false) // deprecated addPropertyCheck(&props, left.GetDeprecated().ValueNode, right.GetDeprecated().ValueNode, left.GetDeprecated(), right.GetDeprecated(), changes, v3.DeprecatedLabel, false) // example addPropertyCheck(&props, left.GetExample().ValueNode, right.GetExample().ValueNode, left.GetExample(), right.GetExample(), changes, v3.ExampleLabel, false) return props } func addSwaggerParameterProperties(left, right low.SwaggerParameter, changes *[]*Change) []*PropertyCheck { var props []*PropertyCheck // type addPropertyCheck(&props, left.GetType().ValueNode, right.GetType().ValueNode, left.GetType(), right.GetType(), changes, v3.TypeLabel, true) // format addPropertyCheck(&props, left.GetFormat().ValueNode, right.GetFormat().ValueNode, left.GetFormat(), right.GetFormat(), changes, v3.FormatLabel, true) // collection format addPropertyCheck(&props, left.GetCollectionFormat().ValueNode, right.GetCollectionFormat().ValueNode, left.GetCollectionFormat(), right.GetCollectionFormat(), changes, v3.CollectionFormatLabel, true) // maximum addPropertyCheck(&props, left.GetMaximum().ValueNode, right.GetMaximum().ValueNode, left.GetMaximum(), right.GetMaximum(), changes, v3.MaximumLabel, true) // minimum addPropertyCheck(&props, left.GetMinimum().ValueNode, right.GetMinimum().ValueNode, left.GetMinimum(), right.GetMinimum(), changes, v3.MinimumLabel, true) // exclusive maximum addPropertyCheck(&props, left.GetExclusiveMaximum().ValueNode, right.GetExclusiveMaximum().ValueNode, left.GetExclusiveMaximum(), right.GetExclusiveMaximum(), changes, v3.ExclusiveMaximumLabel, true) // exclusive minimum addPropertyCheck(&props, left.GetExclusiveMinimum().ValueNode, right.GetExclusiveMinimum().ValueNode, left.GetExclusiveMinimum(), right.GetExclusiveMinimum(), changes, v3.ExclusiveMinimumLabel, true) // max length addPropertyCheck(&props, left.GetMaxLength().ValueNode, right.GetMaxLength().ValueNode, left.GetMaxLength(), right.GetMaxLength(), changes, v3.MaxLengthLabel, true) // min length addPropertyCheck(&props, left.GetMinLength().ValueNode, right.GetMinLength().ValueNode, left.GetMinLength(), right.GetMinLength(), changes, v3.MinLengthLabel, true) // pattern addPropertyCheck(&props, left.GetPattern().ValueNode, right.GetPattern().ValueNode, left.GetPattern(), right.GetPattern(), changes, v3.PatternLabel, true) // max items addPropertyCheck(&props, left.GetMaxItems().ValueNode, right.GetMaxItems().ValueNode, left.GetMaxItems(), right.GetMaxItems(), changes, v3.MaxItemsLabel, true) // min items addPropertyCheck(&props, left.GetMinItems().ValueNode, right.GetMinItems().ValueNode, left.GetMinItems(), right.GetMinItems(), changes, v3.MinItemsLabel, true) // unique items addPropertyCheck(&props, left.GetUniqueItems().ValueNode, right.GetUniqueItems().ValueNode, left.GetUniqueItems(), right.GetUniqueItems(), changes, v3.UniqueItemsLabel, true) // default addPropertyCheck(&props, left.GetDefault().ValueNode, right.GetDefault().ValueNode, left.GetDefault(), right.GetDefault(), changes, v3.DefaultLabel, true) // multiple of addPropertyCheck(&props, left.GetMultipleOf().ValueNode, right.GetMultipleOf().ValueNode, left.GetMultipleOf(), right.GetMultipleOf(), changes, v3.MultipleOfLabel, true) return props } func addCommonParameterProperties(left, right low.SharedParameters, changes *[]*Change) []*PropertyCheck { var props []*PropertyCheck addPropertyCheck(&props, left.GetName().ValueNode, right.GetName().ValueNode, left.GetName(), right.GetName(), changes, v3.NameLabel, true) // in addPropertyCheck(&props, left.GetIn().ValueNode, right.GetIn().ValueNode, left.GetIn(), right.GetIn(), changes, v3.InLabel, true) // description addPropertyCheck(&props, left.GetDescription().ValueNode, right.GetDescription().ValueNode, left.GetDescription(), right.GetDescription(), changes, v3.DescriptionLabel, false) // required addPropertyCheck(&props, left.GetRequired().ValueNode, right.GetRequired().ValueNode, left.GetRequired(), right.GetRequired(), changes, v3.RequiredLabel, true) // allow empty value addPropertyCheck(&props, left.GetAllowEmptyValue().ValueNode, right.GetAllowEmptyValue().ValueNode, left.GetAllowEmptyValue(), right.GetAllowEmptyValue(), changes, v3.AllowEmptyValueLabel, true) return props } // CompareParametersV3 is an OpenAPI type safe proxy for CompareParameters func CompareParametersV3(l, r *v3.Parameter) *ParameterChanges { return CompareParameters(l, r) } // CompareParameters compares a left and right Swagger or OpenAPI Parameter object for any changes. If found returns // a pointer to ParameterChanges. If nothing is found, returns nil. func CompareParameters(l, r any) *ParameterChanges { var changes []*Change var props []*PropertyCheck pc := new(ParameterChanges) var lSchema *base.SchemaProxy var rSchema *base.SchemaProxy var lext, rext map[low.KeyReference[string]]low.ValueReference[any] if reflect.TypeOf(&v2.Parameter{}) == reflect.TypeOf(l) && reflect.TypeOf(&v2.Parameter{}) == reflect.TypeOf(r) { lParam := l.(*v2.Parameter) rParam := r.(*v2.Parameter) // perform hash check to avoid further processing if low.AreEqual(lParam, rParam) { return nil } props = append(props, addSwaggerParameterProperties(lParam, rParam, &changes)...) props = append(props, addCommonParameterProperties(lParam, rParam, &changes)...) // extract schema if lParam != nil { lSchema = lParam.Schema.Value lext = lParam.Extensions } if rParam != nil { rext = rParam.Extensions rSchema = rParam.Schema.Value } // items if !lParam.Items.IsEmpty() && !rParam.Items.IsEmpty() { if lParam.Items.Value.Hash() != rParam.Items.Value.Hash() { pc.ItemsChanges = CompareItems(lParam.Items.Value, rParam.Items.Value) } } if lParam.Items.IsEmpty() && !rParam.Items.IsEmpty() { CreateChange(&changes, ObjectAdded, v3.ItemsLabel, nil, rParam.Items.ValueNode, true, nil, rParam.Items.Value) } if !lParam.Items.IsEmpty() && rParam.Items.IsEmpty() { CreateChange(&changes, ObjectRemoved, v3.ItemsLabel, lParam.Items.ValueNode, nil, true, lParam.Items.Value, nil) } // enum if len(lParam.Enum.Value) > 0 || len(rParam.Enum.Value) > 0 { ExtractRawValueSliceChanges(lParam.Enum.Value, rParam.Enum.Value, &changes, v3.EnumLabel, true) } } // OpenAPI if reflect.TypeOf(&v3.Parameter{}) == reflect.TypeOf(l) && reflect.TypeOf(&v3.Parameter{}) == reflect.TypeOf(r) { lParam := l.(*v3.Parameter) rParam := r.(*v3.Parameter) // perform hash check to avoid further processing if low.AreEqual(lParam, rParam) { return nil } props = append(props, addOpenAPIParameterProperties(lParam, rParam, &changes)...) props = append(props, addCommonParameterProperties(lParam, rParam, &changes)...) if lParam != nil { lext = lParam.Extensions lSchema = lParam.Schema.Value } if rParam != nil { rext = rParam.Extensions rSchema = rParam.Schema.Value } // example checkParameterExample(lParam.Example, rParam.Example, changes) // examples pc.ExamplesChanges = CheckMapForChanges(lParam.Examples.Value, rParam.Examples.Value, &changes, v3.ExamplesLabel, CompareExamples) // content pc.ContentChanges = CheckMapForChanges(lParam.Content.Value, rParam.Content.Value, &changes, v3.ContentLabel, CompareMediaTypes) } CheckProperties(props) if lSchema != nil && rSchema != nil { pc.SchemaChanges = CompareSchemas(lSchema, rSchema) } if lSchema != nil && rSchema == nil { CreateChange(&changes, ObjectRemoved, v3.SchemaLabel, lSchema.GetValueNode(), nil, true, lSchema, nil) } if lSchema == nil && rSchema != nil { CreateChange(&changes, ObjectAdded, v3.SchemaLabel, nil, rSchema.GetValueNode(), true, nil, rSchema) } pc.PropertyChanges = NewPropertyChanges(changes) pc.ExtensionChanges = CompareExtensions(lext, rext) return pc } func checkParameterExample(expLeft, expRight low.NodeReference[any], changes []*Change) { if !expLeft.IsEmpty() && !expRight.IsEmpty() { if low.GenerateHashString(expLeft.GetValue()) != low.GenerateHashString(expRight.GetValue()) { CreateChange(&changes, Modified, v3.ExampleLabel, expLeft.GetValueNode(), expRight.GetValueNode(), false, expLeft.GetValue(), expRight.GetValue()) } } if expLeft.Value == nil && expRight.Value != nil { CreateChange(&changes, PropertyAdded, v3.ExampleLabel, nil, expRight.GetValueNode(), false, nil, expRight.GetValue()) } if expLeft.Value != nil && expRight.Value == nil { CreateChange(&changes, PropertyRemoved, v3.ExampleLabel, expLeft.GetValueNode(), nil, false, expLeft.GetValue(), nil) } }