diff --git a/diff/components_diff.go b/diff/components_diff.go index 75c5d5b2..5fa4f4e0 100644 --- a/diff/components_diff.go +++ b/diff/components_diff.go @@ -15,6 +15,11 @@ type ComponentsDiff struct { CallbacksDiff *CallbacksDiff `json:"callbacks,omitempty" yaml:"callbacks,omitempty"` } +// Empty indicates whether a change was found in this element +func (diff *ComponentsDiff) Empty() bool { + return diff == nil || *diff == ComponentsDiff{} +} + func getComponentsDiff(config *Config, s1, s2 openapi3.Components) (ComponentsDiff, error) { result := ComponentsDiff{} diff --git a/diff/diff_breaking_test.go b/diff/diff_breaking_test.go index fa4bb687..09f1c489 100644 --- a/diff/diff_breaking_test.go +++ b/diff/diff_breaking_test.go @@ -9,34 +9,34 @@ import ( ) func TestBreaking_Same(t *testing.T) { - require.True(t, d(t, &diff.Config{BreakingOnly: true}, 1, 1).Empty()) + require.Empty(t, d(t, &diff.Config{BreakingOnly: true}, 1, 1)) } func TestBreaking_DeletedPaths(t *testing.T) { - require.False(t, d(t, &diff.Config{BreakingOnly: true}, 1, 2).Empty()) + require.NotEmpty(t, d(t, &diff.Config{BreakingOnly: true}, 1, 2)) } func TestBreaking_DeletedTagAllChanges(t *testing.T) { - require.False(t, d(t, &diff.Config{ + require.NotEmpty(t, d(t, &diff.Config{ BreakingOnly: false, - }, 1, 5).PathsDiff.Modified[securityScorePath].OperationsDiff.Modified["GET"].TagsDiff.Empty()) + }, 1, 5).PathsDiff.Modified[securityScorePath].OperationsDiff.Modified["GET"].TagsDiff) } func TestBreaking_DeletedTag(t *testing.T) { - require.True(t, d(t, &diff.Config{ + require.Empty(t, d(t, &diff.Config{ BreakingOnly: true, - }, 1, 5).PathsDiff.Modified[securityScorePath].OperationsDiff.Modified["GET"].TagsDiff.Empty()) + }, 1, 5).PathsDiff.Modified[securityScorePath].OperationsDiff.Modified["GET"].TagsDiff) } func TestBreaking_DeletedEnum(t *testing.T) { - require.False(t, + require.NotEmpty(t, d(t, &diff.Config{ BreakingOnly: true, - }, 3, 1).PathsDiff.Modified[installCommandPath].OperationsDiff.Modified["GET"].ParametersDiff.Modified[openapi3.ParameterInPath]["project"].SchemaDiff.EnumDiff.Empty()) + }, 3, 1).PathsDiff.Modified[installCommandPath].OperationsDiff.Modified["GET"].ParametersDiff.Modified[openapi3.ParameterInPath]["project"].SchemaDiff.EnumDiff) } func TestBreaking_AddedEnum(t *testing.T) { - require.Nil(t, + require.Empty(t, d(t, &diff.Config{ BreakingOnly: true, }, 1, 3).PathsDiff.Modified[installCommandPath].OperationsDiff.Modified["GET"].ParametersDiff.Modified[openapi3.ParameterInPath]) @@ -48,23 +48,12 @@ func TestBreaking_ModifiedExtension(t *testing.T) { IncludeExtensions: diff.StringSet{"x-extension-test2": struct{}{}}, } - require.True(t, d(t, &config, 1, 3).ExtensionsDiff.Empty()) + require.Empty(t, d(t, &config, 1, 3).ExtensionsDiff) } func TestBreaking_Components(t *testing.T) { - - dd := d(t, &diff.Config{BreakingOnly: true}, - 1, 3) - - require.Empty(t, dd.SchemasDiff) - require.Empty(t, dd.ParametersDiff) - require.Empty(t, dd.HeadersDiff) - require.Empty(t, dd.RequestBodiesDiff) - require.Empty(t, dd.ResponsesDiff) - require.Empty(t, dd.SecuritySchemesDiff) - require.Empty(t, dd.ExamplesDiff) - require.Empty(t, dd.LinksDiff) - require.Empty(t, dd.CallbacksDiff) + require.Empty(t, d(t, &diff.Config{BreakingOnly: true}, + 1, 3).ComponentsDiff) } func TestCompareWithDefault(t *testing.T) { @@ -79,11 +68,112 @@ func TestCompareWithDefault_Nil(t *testing.T) { ) } +func TestBreaking_NewRequiredProperty(t *testing.T) { + s1 := l(t, 1) + s2 := l(t, 1) + + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + Description: "Unique ID of the course", + }, + } + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{"courseId"} + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + require.NotEmpty(t, d) +} + +func TestBreaking_NewNonRequiredProperty(t *testing.T) { + s1 := l(t, 1) + s2 := l(t, 1) + + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + Description: "Unique ID of the course", + }, + } + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + require.Empty(t, d) +} + +func TestBreaking_PropertyRequiredEnabled(t *testing.T) { + s1 := l(t, 1) + s2 := l(t, 1) + + sr := openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + Description: "Unique ID of the course", + }, + } + + s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr + s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{} + + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{"courseId"} + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + require.NotEmpty(t, d) +} + +func TestBreaking_PropertyRequiredDisabled(t *testing.T) { + s1 := l(t, 1) + s2 := l(t, 1) + + sr := openapi3.SchemaRef{ + Value: &openapi3.Schema{ + Type: "string", + Description: "Unique ID of the course", + }, + } + + s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr + s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{"courseId"} + + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Properties["courseId"] = &sr + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Schema.Value.Required = []string{} + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + require.Empty(t, d) +} + +func deleteParam(op *openapi3.Operation, in string, name string) { + + result := openapi3.NewParameters() + + for _, item := range op.Parameters { + if v := item.Value; v != nil { + if v.Name == name && v.In == in { + continue + } + result = append(result, item) + } + } + op.Parameters = result +} + func TestBreaking_NewPathParam(t *testing.T) { s1 := l(t, 1) s2 := l(t, 1) - s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInPath, "domain").Name = "" + deleteParam(s1.Paths[installCommandPath].Get, openapi3.ParameterInPath, "project") + // note: path params are always required d, err := diff.Get(&diff.Config{ BreakingOnly: true, @@ -92,14 +182,14 @@ func TestBreaking_NewPathParam(t *testing.T) { require.Contains(t, d.PathsDiff.Modified[installCommandPath].OperationsDiff.Modified["GET"].ParametersDiff.Added[openapi3.ParameterInPath], - "domain") + "project") } func TestBreaking_NewRequiredHeaderParam(t *testing.T) { s1 := l(t, 1) s2 := l(t, 1) - s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Name = "" + deleteParam(s1.Paths[installCommandPath].Get, openapi3.ParameterInHeader, "network-policies") s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Required = true d, err := diff.Get(&diff.Config{ @@ -112,11 +202,11 @@ func TestBreaking_NewRequiredHeaderParam(t *testing.T) { "network-policies") } -func TestBreaking_NewNoneRequiredHeaderParam(t *testing.T) { +func TestBreaking_NewNonRequiredHeaderParam(t *testing.T) { s1 := l(t, 1) s2 := l(t, 1) - s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Name = "" + deleteParam(s1.Paths[installCommandPath].Get, openapi3.ParameterInHeader, "network-policies") s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Required = false d, err := diff.Get(&diff.Config{ @@ -124,9 +214,42 @@ func TestBreaking_NewNoneRequiredHeaderParam(t *testing.T) { }, s1, s2) require.NoError(t, err) - require.NotContains(t, - d.PathsDiff.Modified[installCommandPath].OperationsDiff.Modified["GET"].ParametersDiff.Added[openapi3.ParameterInPath], - "network-policies") + require.Empty(t, d) +} + +func TestBreaking_HeaderParamRequiredEnabled(t *testing.T) { + s1 := l(t, 1) + s2 := l(t, 1) + + s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Required = false + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Required = true + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + require.Equal(t, + &diff.ValueDiff{ + From: false, + To: true, + }, + d.PathsDiff.Modified[installCommandPath].OperationsDiff.Modified["GET"].ParametersDiff.Modified[openapi3.ParameterInHeader]["network-policies"].RequiredDiff) +} + +func TestBreaking_HeaderParamRequiredDisabled(t *testing.T) { + s1 := l(t, 1) + s2 := l(t, 1) + + s1.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Required = true + s2.Paths[installCommandPath].Get.Parameters.GetByInAndName(openapi3.ParameterInHeader, "network-policies").Required = false + + d, err := diff.Get(&diff.Config{ + BreakingOnly: true, + }, s1, s2) + require.NoError(t, err) + + require.Empty(t, d) } func TestBreaking_MaxLengthSmaller(t *testing.T) { @@ -143,7 +266,7 @@ func TestBreaking_MaxLengthSmaller(t *testing.T) { BreakingOnly: true, }, s1, s2) require.NoError(t, err) - require.False(t, d.Empty()) + require.NotEmpty(t, d) } func TestBreaking_MaxLengthGreater(t *testing.T) { @@ -160,7 +283,7 @@ func TestBreaking_MaxLengthGreater(t *testing.T) { BreakingOnly: true, }, s1, s2) require.NoError(t, err) - require.True(t, d.Empty()) + require.Empty(t, d) } func TestBreaking_MaxLengthFromNil(t *testing.T) { @@ -176,7 +299,7 @@ func TestBreaking_MaxLengthFromNil(t *testing.T) { BreakingOnly: true, }, s1, s2) require.NoError(t, err) - require.False(t, d.Empty()) + require.NotEmpty(t, d) } func TestBreaking_MaxLengthToNil(t *testing.T) { @@ -192,7 +315,7 @@ func TestBreaking_MaxLengthToNil(t *testing.T) { BreakingOnly: true, }, s1, s2) require.NoError(t, err) - require.True(t, d.Empty()) + require.Empty(t, d) } func TestBreaking_MaxLengthBothNil(t *testing.T) { @@ -206,7 +329,7 @@ func TestBreaking_MaxLengthBothNil(t *testing.T) { BreakingOnly: true, }, s1, s2) require.NoError(t, err) - require.True(t, d.Empty()) + require.Empty(t, d) } func TestBreaking_MinItemsSmaller(t *testing.T) { @@ -220,7 +343,7 @@ func TestBreaking_MinItemsSmaller(t *testing.T) { BreakingOnly: true, }, s1, s2) require.NoError(t, err) - require.True(t, d.Empty()) + require.Empty(t, d) } func TestBreaking_MinItemsGreater(t *testing.T) { @@ -234,5 +357,5 @@ func TestBreaking_MinItemsGreater(t *testing.T) { BreakingOnly: true, }, s1, s2) require.NoError(t, err) - require.False(t, d.Empty()) + require.NotEmpty(t, d) } diff --git a/diff/headers_diff.go b/diff/headers_diff.go index d8181bdc..8243b8a0 100644 --- a/diff/headers_diff.go +++ b/diff/headers_diff.go @@ -24,14 +24,6 @@ func (headersDiff *HeadersDiff) Empty() bool { len(headersDiff.Modified) == 0 } -func (headersDiff *HeadersDiff) emptyNonBreaking(breakingOnly bool) { - if !breakingOnly { - return - } - - headersDiff.Added = nil -} - func (headersDiff *HeadersDiff) removeNonBreaking() { if headersDiff.Empty() { diff --git a/diff/required_diff.go b/diff/required_diff.go new file mode 100644 index 00000000..bd2fc443 --- /dev/null +++ b/diff/required_diff.go @@ -0,0 +1,51 @@ +package diff + +// RequiredPropertiesDiff describes the changes between a pair of lists of required properties +type RequiredPropertiesDiff struct { + StringsDiff +} + +// Empty indicates whether a change was found in this element +func (diff *RequiredPropertiesDiff) Empty() bool { + if diff == nil { + return true + } + + return diff.StringsDiff.Empty() +} + +func (diff *RequiredPropertiesDiff) removeNonBreaking() { + if diff.Empty() { + return + } + + if diff.StringsDiff.Empty() { + return + } + + diff.Deleted = nil +} + +func getRequiredPropertiesDiff(config *Config, strings1, strings2 StringList) *RequiredPropertiesDiff { + + diff := getRequiredPropertiesDiffInternal(strings1, strings2) + + if config.BreakingOnly { + diff.removeNonBreaking() + } + + if diff.Empty() { + return nil + } + + return diff +} + +func getRequiredPropertiesDiffInternal(strings1, strings2 StringList) *RequiredPropertiesDiff { + if stringsDiff := getStringsDiff(strings1, strings2); stringsDiff != nil { + return &RequiredPropertiesDiff{ + StringsDiff: *stringsDiff, + } + } + return nil +} diff --git a/diff/schema_diff.go b/diff/schema_diff.go index 5dc4a17b..42a7bb5f 100644 --- a/diff/schema_diff.go +++ b/diff/schema_diff.go @@ -8,46 +8,46 @@ import ( // SchemaDiff describes the changes between a pair of schema objects: https://swagger.io/specification/#schema-object type SchemaDiff struct { - SchemaAdded bool `json:"schemaAdded,omitempty" yaml:"schemaAdded,omitempty"` - SchemaDeleted bool `json:"schemaDeleted,omitempty" yaml:"schemaDeleted,omitempty"` - ExtensionsDiff *ExtensionsDiff `json:"extensions,omitempty" yaml:"extensions,omitempty"` - OneOfDiff *SchemaListDiff `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` - AnyOfDiff *SchemaListDiff `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` - AllOfDiff *SchemaListDiff `json:"allOf,omitempty" yaml:"allOf,omitempty"` - NotDiff *SchemaDiff `json:"not,omitempty" yaml:"not,omitempty"` - TypeDiff *ValueDiff `json:"type,omitempty" yaml:"type,omitempty"` - TitleDiff *ValueDiff `json:"title,omitempty" yaml:"title,omitempty"` - FormatDiff *ValueDiff `json:"format,omitempty" yaml:"format,omitempty"` - DescriptionDiff *ValueDiff `json:"description,omitempty" yaml:"description,omitempty"` - EnumDiff *EnumDiff `json:"enum,omitempty" yaml:"enum,omitempty"` - DefaultDiff *ValueDiff `json:"default,omitempty" yaml:"default,omitempty"` - ExampleDiff *ValueDiff `json:"example,omitempty" yaml:"example,omitempty"` - ExternalDocsDiff *ExternalDocsDiff `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` - AdditionalPropertiesAllowedDiff *ValueDiff `json:"additionalPropertiesAllowed,omitempty" yaml:"additionalPropertiesAllowed,omitempty"` - UniqueItemsDiff *ValueDiff `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` - ExclusiveMinDiff *ValueDiff `json:"exclusiveMin,omitempty" yaml:"exclusiveMin,omitempty"` - ExclusiveMaxDiff *ValueDiff `json:"exclusiveMax,omitempty" yaml:"exclusiveMax,omitempty"` - NullableDiff *ValueDiff `json:"nullable,omitempty" yaml:"nullable,omitempty"` - ReadOnlyDiff *ValueDiff `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` - WriteOnlyDiff *ValueDiff `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` - AllowEmptyValueDiff *ValueDiff `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` - XMLDiff *ValueDiff `json:"XML,omitempty" yaml:"XML,omitempty"` - DeprecatedDiff *ValueDiff `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` - MinDiff *ValueDiff `json:"min,omitempty" yaml:"min,omitempty"` - MaxDiff *ValueDiff `json:"max,omitempty" yaml:"max,omitempty"` - MultipleOfDiff *ValueDiff `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` - MinLengthDiff *ValueDiff `json:"minLength,omitempty" yaml:"minLength,omitempty"` - MaxLengthDiff *ValueDiff `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` - PatternDiff *ValueDiff `json:"pattern,omitempty" yaml:"pattern,omitempty"` - MinItemsDiff *ValueDiff `json:"minItems,omitempty" yaml:"minItems,omitempty"` - MaxItemsDiff *ValueDiff `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` - ItemsDiff *SchemaDiff `json:"items,omitempty" yaml:"items,omitempty"` - RequiredDiff *StringsDiff `json:"required,omitempty" yaml:"required,omitempty"` - PropertiesDiff *SchemasDiff `json:"properties,omitempty" yaml:"properties,omitempty"` - MinPropsDiff *ValueDiff `json:"minProps,omitempty" yaml:"minProps,omitempty"` - MaxPropsDiff *ValueDiff `json:"maxProps,omitempty" yaml:"maxProps,omitempty"` - AdditionalPropertiesDiff *SchemaDiff `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` - DiscriminatorDiff *DiscriminatorDiff `json:"discriminatorDiff,omitempty" yaml:"discriminatorDiff,omitempty"` + SchemaAdded bool `json:"schemaAdded,omitempty" yaml:"schemaAdded,omitempty"` + SchemaDeleted bool `json:"schemaDeleted,omitempty" yaml:"schemaDeleted,omitempty"` + ExtensionsDiff *ExtensionsDiff `json:"extensions,omitempty" yaml:"extensions,omitempty"` + OneOfDiff *SchemaListDiff `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` + AnyOfDiff *SchemaListDiff `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` + AllOfDiff *SchemaListDiff `json:"allOf,omitempty" yaml:"allOf,omitempty"` + NotDiff *SchemaDiff `json:"not,omitempty" yaml:"not,omitempty"` + TypeDiff *ValueDiff `json:"type,omitempty" yaml:"type,omitempty"` + TitleDiff *ValueDiff `json:"title,omitempty" yaml:"title,omitempty"` + FormatDiff *ValueDiff `json:"format,omitempty" yaml:"format,omitempty"` + DescriptionDiff *ValueDiff `json:"description,omitempty" yaml:"description,omitempty"` + EnumDiff *EnumDiff `json:"enum,omitempty" yaml:"enum,omitempty"` + DefaultDiff *ValueDiff `json:"default,omitempty" yaml:"default,omitempty"` + ExampleDiff *ValueDiff `json:"example,omitempty" yaml:"example,omitempty"` + ExternalDocsDiff *ExternalDocsDiff `json:"externalDocs,omitempty" yaml:"externalDocs,omitempty"` + AdditionalPropertiesAllowedDiff *ValueDiff `json:"additionalPropertiesAllowed,omitempty" yaml:"additionalPropertiesAllowed,omitempty"` + UniqueItemsDiff *ValueDiff `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + ExclusiveMinDiff *ValueDiff `json:"exclusiveMin,omitempty" yaml:"exclusiveMin,omitempty"` + ExclusiveMaxDiff *ValueDiff `json:"exclusiveMax,omitempty" yaml:"exclusiveMax,omitempty"` + NullableDiff *ValueDiff `json:"nullable,omitempty" yaml:"nullable,omitempty"` + ReadOnlyDiff *ValueDiff `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` + WriteOnlyDiff *ValueDiff `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` + AllowEmptyValueDiff *ValueDiff `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` + XMLDiff *ValueDiff `json:"XML,omitempty" yaml:"XML,omitempty"` + DeprecatedDiff *ValueDiff `json:"deprecated,omitempty" yaml:"deprecated,omitempty"` + MinDiff *ValueDiff `json:"min,omitempty" yaml:"min,omitempty"` + MaxDiff *ValueDiff `json:"max,omitempty" yaml:"max,omitempty"` + MultipleOfDiff *ValueDiff `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + MinLengthDiff *ValueDiff `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MaxLengthDiff *ValueDiff `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + PatternDiff *ValueDiff `json:"pattern,omitempty" yaml:"pattern,omitempty"` + MinItemsDiff *ValueDiff `json:"minItems,omitempty" yaml:"minItems,omitempty"` + MaxItemsDiff *ValueDiff `json:"maxItems,omitempty" yaml:"maxItems,omitempty"` + ItemsDiff *SchemaDiff `json:"items,omitempty" yaml:"items,omitempty"` + RequiredDiff *RequiredPropertiesDiff `json:"required,omitempty" yaml:"required,omitempty"` + PropertiesDiff *SchemasDiff `json:"properties,omitempty" yaml:"properties,omitempty"` + MinPropsDiff *ValueDiff `json:"minProps,omitempty" yaml:"minProps,omitempty"` + MaxPropsDiff *ValueDiff `json:"maxProps,omitempty" yaml:"maxProps,omitempty"` + AdditionalPropertiesDiff *SchemaDiff `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + DiscriminatorDiff *DiscriminatorDiff `json:"discriminatorDiff,omitempty" yaml:"discriminatorDiff,omitempty"` } // Empty indicates whether a change was found in this element @@ -55,7 +55,7 @@ func (diff *SchemaDiff) Empty() bool { return diff == nil || *diff == SchemaDiff{} } -func (diff *SchemaDiff) removeNonBreaking() { +func (diff *SchemaDiff) removeNonBreaking(schema2 *openapi3.SchemaRef) { if diff.Empty() { return @@ -68,22 +68,10 @@ func (diff *SchemaDiff) removeNonBreaking() { diff.ExampleDiff = nil diff.ExternalDocsDiff = nil - if !diff.AdditionalPropertiesAllowedDiff.CompareWithDefault(true, false, true) { - diff.AdditionalPropertiesAllowedDiff = nil - } - if !diff.UniqueItemsDiff.CompareWithDefault(false, true, false) { // TODO: check default value diff.UniqueItemsDiff = nil } - if !diff.ExclusiveMinDiff.CompareWithDefault(false, true, false) { // TODO: check default value - diff.ExclusiveMinDiff = nil - } - - if !diff.ExclusiveMaxDiff.CompareWithDefault(false, true, false) { // TODO: check default value - diff.ExclusiveMaxDiff = nil - } - if !diff.NullableDiff.CompareWithDefault(true, false, false) { // TODO: check default value diff.NullableDiff = nil } @@ -104,6 +92,7 @@ func (diff *SchemaDiff) removeNonBreaking() { diff.DeprecatedDiff = nil } + // Number if !diff.MinDiff.minBreaking() { diff.MinDiff = nil } @@ -112,6 +101,15 @@ func (diff *SchemaDiff) removeNonBreaking() { diff.MaxDiff = nil } + if !diff.ExclusiveMinDiff.CompareWithDefault(false, true, false) { // TODO: check default value + diff.ExclusiveMinDiff = nil + } + + if !diff.ExclusiveMaxDiff.CompareWithDefault(false, true, false) { // TODO: check default value + diff.ExclusiveMaxDiff = nil + } + + // String if !diff.MinLengthDiff.minBreaking() { diff.MinLengthDiff = nil } @@ -120,6 +118,22 @@ func (diff *SchemaDiff) removeNonBreaking() { diff.MaxLengthDiff = nil } + // Array + if !diff.MinItemsDiff.minBreaking() { + diff.MinItemsDiff = nil + } + + if !diff.MaxItemsDiff.maxBreaking() { + diff.MaxItemsDiff = nil + } + + // Object + diff.removeAddedButNonRequiredProperties(schema2) + + if !diff.AdditionalPropertiesAllowedDiff.CompareWithDefault(true, false, true) { + diff.AdditionalPropertiesAllowedDiff = nil + } + if !diff.MinPropsDiff.minBreaking() { diff.MinPropsDiff = nil } @@ -127,13 +141,30 @@ func (diff *SchemaDiff) removeNonBreaking() { if !diff.MaxPropsDiff.maxBreaking() { diff.MaxPropsDiff = nil } +} - if !diff.MinItemsDiff.minBreaking() { - diff.MinItemsDiff = nil +func (diff *SchemaDiff) removeAddedButNonRequiredProperties(schema2 *openapi3.SchemaRef) { + + if diff.Empty() || diff.PropertiesDiff.Empty() { + return } - if !diff.MaxItemsDiff.maxBreaking() { - diff.MaxItemsDiff = nil + if schema2 == nil || schema2.Value == nil { + return + } + + requiredMap := StringList(schema2.Value.Required).toStringSet() + + newList := StringList{} + for _, property := range diff.PropertiesDiff.Added { + if _, ok := requiredMap[property]; ok { + newList = append(newList, property) + } + } + diff.PropertiesDiff.Added = newList + + if diff.PropertiesDiff.Empty() { + diff.PropertiesDiff = nil } } @@ -144,7 +175,7 @@ func getSchemaDiff(config *Config, schema1, schema2 *openapi3.SchemaRef) (*Schem } if config.BreakingOnly { - diff.removeNonBreaking() + diff.removeNonBreaking(schema2) } if diff.Empty() { @@ -221,7 +252,8 @@ func getSchemaDiffInternal(config *Config, schema1, schema2 *openapi3.SchemaRef) return nil, err } - result.RequiredDiff = getStringsDiff(value1.Required, value2.Required) + // Object + result.RequiredDiff = getRequiredPropertiesDiff(config, value1.Required, value2.Required) result.PropertiesDiff, err = getSchemasDiff(config, value1.Properties, value2.Properties) if err != nil { return nil, err diff --git a/diff/schemas_diff.go b/diff/schemas_diff.go index e0a7ce06..ca38492e 100644 --- a/diff/schemas_diff.go +++ b/diff/schemas_diff.go @@ -22,15 +22,6 @@ func (schemasDiff *SchemasDiff) Empty() bool { len(schemasDiff.Modified) == 0 } -func (schemasDiff *SchemasDiff) removeNonBreaking() { - - if schemasDiff.Empty() { - return - } - - schemasDiff.Added = nil -} - func newSchemasDiff() *SchemasDiff { return &SchemasDiff{ Added: StringList{}, @@ -52,10 +43,6 @@ func getSchemasDiff(config *Config, schemas1, schemas2 openapi3.Schemas) (*Schem return nil, err } - if config.BreakingOnly { - diff.removeNonBreaking() - } - if diff.Empty() { return nil, nil }