diff --git a/README.md b/README.md index bc068f4b..56b6b852 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ docker run --rm -t tufin/oasdiff changelog https://raw.githubusercontent.com/Tuf - [Multiple versions of the same endpoint](MATCHING-ENDPOINTS.md) - [Merge allOf schemas](ALLOF.md) - [Merge common parameters](COMMON-PARAMS.md) +- [Case-insensitive header comparison](#case-insensitive-header-comparison) - [Path prefix modification](#path-prefix-modification) - [Path parameter renaming](#path-parameter-renaming) - [Excluding certain kinds of changes](#excluding-specific-kinds-of-changes) @@ -241,6 +242,13 @@ Sometimes developers decide to change names of path parameters, for example, in Oasdiff supports path parameter renaming by default. [Learn more](MATCHING-ENDPOINTS.md) about how oasdiff supports path parameter renaming. +## Case-Insensitive Header Comparison +Header names comparison is normally case-sensitive. +To make this comparison case-insensitive, add the `--case-insensitive-headers` flag: +``` +oasdiff diff data/header-case/base.yaml data/header-case/revision.yaml --case-insensitive-headers +``` + ## Excluding Specific Kinds of Changes You can use the `--exclude-elements` flag to exclude certain kinds of changes: - Use `--exclude-elements examples` to exclude [Examples](https://swagger.io/specification/#example-object) diff --git a/data/header-case/base.yaml b/data/header-case/base.yaml new file mode 100644 index 00000000..dbd32c0a --- /dev/null +++ b/data/header-case/base.yaml @@ -0,0 +1,18 @@ +openapi: 3.0.1 +info: + title: Test API + version: v1 +paths: + /test: + parameters: + - in: header + name: X-Case + required: true + schema: + type: string + get: + tags: + - Test + responses: + "200": + description: Success \ No newline at end of file diff --git a/data/header-case/revision.yaml b/data/header-case/revision.yaml new file mode 100644 index 00000000..bb6bc0f0 --- /dev/null +++ b/data/header-case/revision.yaml @@ -0,0 +1,18 @@ +openapi: 3.0.1 +info: + title: Test API + version: v1 +paths: + /test: + parameters: + - in: header + name: x-case + required: true + schema: + type: string + get: + tags: + - Test + responses: + "200": + description: Success \ No newline at end of file diff --git a/flatten/headers/doc.go b/flatten/headers/doc.go new file mode 100644 index 00000000..aaf4d940 --- /dev/null +++ b/flatten/headers/doc.go @@ -0,0 +1,4 @@ +/* +Package headers replaces all header names to lowercase +*/ +package headers diff --git a/flatten/headers/params.go b/flatten/headers/params.go new file mode 100644 index 00000000..34d64990 --- /dev/null +++ b/flatten/headers/params.go @@ -0,0 +1,41 @@ +package headers + +import ( + "strings" + + "github.com/getkin/kin-openapi/openapi3" +) + +// Lowercase replaces header names to lowercase +func Lowercase(spec *openapi3.T) { + lowerHeaderNames(spec) +} + +func lowerHeaderNames(spec *openapi3.T) { + for _, path := range spec.Paths.Map() { + + for _, paramRef := range path.Parameters { + lowerHeaderName(paramRef.Value) + } + + for _, op := range path.Operations() { + for _, paramRef := range op.Parameters { + lowerHeaderName(paramRef.Value) + } + + for _, responseRef := range op.Responses.Map() { + for _, headerRef := range responseRef.Value.Headers { + lowerHeaderName(&headerRef.Value.Parameter) + } + } + } + } +} + +func lowerHeaderName(param *openapi3.Parameter) { + if param.In != openapi3.ParameterInHeader { + return + } + + param.Name = strings.ToLower(param.Name) +} diff --git a/internal/changelog_flags.go b/internal/changelog_flags.go index 96bc88ea..ffb3f616 100644 --- a/internal/changelog_flags.go +++ b/internal/changelog_flags.go @@ -23,6 +23,7 @@ type ChangelogFlags struct { failOn string flattenAllOf bool flattenParams bool + insensitiveHeaders bool lang string errIgnoreFile string warnIgnoreFile string @@ -64,6 +65,10 @@ func (flags *ChangelogFlags) getFlattenParams() bool { return flags.flattenParams } +func (flags *ChangelogFlags) getInsensitiveHeaders() bool { + return flags.insensitiveHeaders +} + func (flags *ChangelogFlags) getCircularReferenceCounter() int { return flags.circularReferenceCounter } @@ -172,6 +177,10 @@ func (flags *ChangelogFlags) refFlattenParams() *bool { return &flags.flattenParams } +func (flags *ChangelogFlags) refInsensitiveHeaders() *bool { + return &flags.insensitiveHeaders +} + func (flags *ChangelogFlags) refLang() *string { return &flags.lang } diff --git a/internal/cmd_flags.go b/internal/cmd_flags.go index 4daa53fb..4b45518e 100644 --- a/internal/cmd_flags.go +++ b/internal/cmd_flags.go @@ -20,6 +20,7 @@ func addCommonDiffFlags(cmd *cobra.Command, flags Flags) { cmd.PersistentFlags().BoolVarP(flags.refIncludePathParams(), "include-path-params", "", false, "include path parameter names in endpoint matching") cmd.PersistentFlags().BoolVarP(flags.refFlattenAllOf(), "flatten-allof", "", false, "merge subschemas under allOf before diff") cmd.PersistentFlags().BoolVarP(flags.refFlattenParams(), "flatten-params", "", false, "merge common parameters at path level with operation parameters") + cmd.PersistentFlags().BoolVarP(flags.refInsensitiveHeaders(), "case-insensitive-headers", "", false, "case-insensitive header name comparison") addDeprecatedFlattenFlag(cmd, flags) } diff --git a/internal/delta_flags.go b/internal/delta_flags.go index e7ee0f58..f04d32e7 100644 --- a/internal/delta_flags.go +++ b/internal/delta_flags.go @@ -56,3 +56,7 @@ func (flags *DeltaFlags) refFlattenAllOf() *bool { func (flags *DeltaFlags) refFlattenParams() *bool { return &flags.flattenParams } + +func (flags *DeltaFlags) refInsensitiveHeaders() *bool { + return &flags.insensitiveHeaders +} diff --git a/internal/diff.go b/internal/diff.go index 5c996f67..f636cf03 100644 --- a/internal/diff.go +++ b/internal/diff.go @@ -99,13 +99,14 @@ func normalDiff(loader load.Loader, flags Flags) (*diffResult, *ReturnError) { flattenAllOf := load.GetOption(load.WithFlattenAllOf(), flags.getFlattenAllOf()) flattenParams := load.GetOption(load.WithFlattenParams(), flags.getFlattenParams()) + lowerHeaderNames := load.GetOption(load.WithLowercaseHeaders(), flags.getInsensitiveHeaders()) - s1, err := load.NewSpecInfo(loader, flags.getBase(), flattenAllOf, flattenParams) + s1, err := load.NewSpecInfo(loader, flags.getBase(), flattenAllOf, flattenParams, lowerHeaderNames) if err != nil { return nil, getErrFailedToLoadSpec("base", flags.getBase(), err) } - s2, err := load.NewSpecInfo(loader, flags.getRevision(), flattenAllOf, flattenParams) + s2, err := load.NewSpecInfo(loader, flags.getRevision(), flattenAllOf, flattenParams, lowerHeaderNames) if err != nil { return nil, getErrFailedToLoadSpec("revision", flags.getRevision(), err) } @@ -127,13 +128,14 @@ func composedDiff(loader load.Loader, flags Flags) (*diffResult, *ReturnError) { flattenAllOf := load.GetOption(load.WithFlattenAllOf(), flags.getFlattenAllOf()) flattenParams := load.GetOption(load.WithFlattenParams(), flags.getFlattenParams()) + lowerHeaderNames := load.GetOption(load.WithLowercaseHeaders(), flags.getInsensitiveHeaders()) - s1, err := load.NewSpecInfoFromGlob(loader, flags.getBase().Path, flattenAllOf, flattenParams) + s1, err := load.NewSpecInfoFromGlob(loader, flags.getBase().Path, flattenAllOf, flattenParams, lowerHeaderNames) if err != nil { return nil, getErrFailedToLoadSpecs("base", flags.getBase().Path, err) } - s2, err := load.NewSpecInfoFromGlob(loader, flags.getRevision().Path, flattenAllOf, flattenParams) + s2, err := load.NewSpecInfoFromGlob(loader, flags.getRevision().Path, flattenAllOf, flattenParams, lowerHeaderNames) if err != nil { return nil, getErrFailedToLoadSpecs("revision", flags.getRevision().Path, err) } diff --git a/internal/diff_flags.go b/internal/diff_flags.go index a93f9907..27d30cc6 100644 --- a/internal/diff_flags.go +++ b/internal/diff_flags.go @@ -19,6 +19,7 @@ type DiffFlags struct { failOnDiff bool flattenAllOf bool flattenParams bool + insensitiveHeaders bool circularReferenceCounter int includePathParams bool excludeElements []string @@ -57,6 +58,10 @@ func (flags *DiffFlags) getFlattenParams() bool { return flags.flattenParams } +func (flags *DiffFlags) getInsensitiveHeaders() bool { + return flags.insensitiveHeaders +} + func (flags *DiffFlags) getCircularReferenceCounter() int { return flags.circularReferenceCounter } @@ -165,6 +170,10 @@ func (flags *DiffFlags) refFlattenParams() *bool { return &flags.flattenParams } +func (flags *DiffFlags) refInsensitiveHeaders() *bool { + return &flags.insensitiveHeaders +} + func (flags *DiffFlags) refLang() *string { return nil } diff --git a/internal/flags.go b/internal/flags.go index 9e92d755..dc267e2e 100644 --- a/internal/flags.go +++ b/internal/flags.go @@ -13,6 +13,7 @@ type Flags interface { getRevision() *load.Source getFlattenAllOf() bool getFlattenParams() bool + getInsensitiveHeaders() bool getCircularReferenceCounter() int getIncludeChecks() []string getDeprecationDaysBeta() int @@ -43,6 +44,7 @@ type Flags interface { refIncludePathParams() *bool refFlattenAllOf() *bool refFlattenParams() *bool + refInsensitiveHeaders() *bool refLang() *string refErrIgnoreFile() *string refWarnIgnoreFile() *string diff --git a/internal/run_test.go b/internal/run_test.go index c165ee26..f1f7d826 100644 --- a/internal/run_test.go +++ b/internal/run_test.go @@ -274,6 +274,10 @@ func Test_BreakingChangesFlattenCommonParams(t *testing.T) { require.Zero(t, internal.Run(cmdToArgs("oasdiff breaking ../data/common-params/params_in_path.yaml ../data/common-params/params_in_op.yaml --flatten-params --fail-on ERR"), io.Discard, io.Discard)) } +func Test_BreakingChangesCaseInsensitiveHeaders(t *testing.T) { + require.Zero(t, internal.Run(cmdToArgs("oasdiff breaking ../data/header-case/base.yaml ../data/header-case/revision.yaml --case-insensitive-headers --fail-on ERR"), io.Discard, io.Discard)) +} + func Test_FlattenCmdOK(t *testing.T) { require.Zero(t, internal.Run(cmdToArgs("oasdiff flatten ../data/allof/simple.yaml"), io.Discard, io.Discard)) } diff --git a/load/option.go b/load/option.go index d31fab7f..bc408ae3 100644 --- a/load/option.go +++ b/load/option.go @@ -3,6 +3,7 @@ package load import ( "github.com/tufin/oasdiff/flatten/allof" "github.com/tufin/oasdiff/flatten/commonparams" + "github.com/tufin/oasdiff/flatten/headers" ) // option functions can be used to preprocess specs after loading them @@ -46,3 +47,13 @@ func WithFlattenParams() Option { return specInfos, nil } } + +// WithLowercaseHeaders returns SpecInfos with header names converted to lowercase +func WithLowercaseHeaders() Option { + return func(loader Loader, specInfos []*SpecInfo) ([]*SpecInfo, error) { + for _, specInfo := range specInfos { + headers.Lowercase(specInfo.Spec) + } + return specInfos, nil + } +}