Skip to content

Commit

Permalink
feat: support excluding paths that don't match a regex (#615)
Browse files Browse the repository at this point in the history
* add --unmatch-path flag

* add test

* test invalid regex
  • Loading branch information
reuvenharrison authored Oct 2, 2024
1 parent 887ba67 commit efc43b7
Show file tree
Hide file tree
Showing 10 changed files with 58 additions and 15 deletions.
3 changes: 2 additions & 1 deletion diff/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import (

// Config includes various settings to control the diff
type Config struct {
PathFilter string
MatchPath string
UnmatchPath string
FilterExtension string
PathPrefixBase string
PathPrefixRevision string
Expand Down
2 changes: 1 addition & 1 deletion diff/diff_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,6 @@ func TestDiff_ComponentCallbacksNil(t *testing.T) {
}

func TestFilterByRegex_Invalid(t *testing.T) {
_, err := diff.Get(&diff.Config{PathFilter: "["}, l(t, 1), l(t, 2))
_, err := diff.Get(&diff.Config{MatchPath: "["}, l(t, 1), l(t, 2))
require.EqualError(t, err, "failed to compile filter regex \"[\": error parsing regexp: missing closing ]: `[`")
}
18 changes: 17 additions & 1 deletion diff/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,13 @@ func TestSummaryInvalidComponent(t *testing.T) {
}

func TestFilterByRegex(t *testing.T) {
d, err := diff.Get(&diff.Config{PathFilter: "x"}, l(t, 1), l(t, 2))
d, err := diff.Get(&diff.Config{MatchPath: "x"}, l(t, 1), l(t, 2))
require.NoError(t, err)
require.Nil(t, d.GetSummary().Details[diff.PathsDetail])
}

func TestFilterByInvertedRegex(t *testing.T) {
d, err := diff.Get(&diff.Config{UnmatchPath: "api|subscribe|register"}, l(t, 1), l(t, 2))
require.NoError(t, err)
require.Nil(t, d.GetSummary().Details[diff.PathsDetail])
}
Expand All @@ -553,6 +559,16 @@ func TestFilterPathsByExtension(t *testing.T) {
d.GetSummary().Details[diff.PathsDetail])
}

func TestFilterByInvalidRegex(t *testing.T) {
_, err := diff.Get(&diff.Config{MatchPath: "["}, l(t, 1), l(t, 2))
require.EqualError(t, err, "failed to compile filter regex \"[\": error parsing regexp: missing closing ]: `[`")
}

func TestFilterByInvalidInvertedRegex(t *testing.T) {
_, err := diff.Get(&diff.Config{UnmatchPath: "["}, l(t, 1), l(t, 2))
require.EqualError(t, err, "failed to compile filter regex \"[\": error parsing regexp: missing closing ]: `[`")
}

func TestFilterOperationssByExtension(t *testing.T) {
d, err := diff.Get(&diff.Config{FilterExtension: "x-beta"}, l(t, 1), l(t, 3))
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion diff/endpoints_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func getEndpointsDiff(config *Config, state *state, paths1, paths2 *openapi3.Pat
return nil, nil
}

if err := filterPaths(config.PathFilter, config.FilterExtension, paths1, paths2); err != nil {
if err := filterPaths(config.MatchPath, config.UnmatchPath, config.FilterExtension, paths1, paths2); err != nil {
return nil, err
}

Expand Down
24 changes: 16 additions & 8 deletions diff/paths_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func newPathsDiff() *PathsDiff {

func getPathsDiff(config *Config, state *state, paths1, paths2 *openapi3.Paths) (*PathsDiff, error) {

if err := filterPaths(config.PathFilter, config.FilterExtension, paths1, paths2); err != nil {
if err := filterPaths(config.MatchPath, config.UnmatchPath, config.FilterExtension, paths1, paths2); err != nil {
return nil, err
}

Expand Down Expand Up @@ -103,9 +103,13 @@ func (pathsDiff *PathsDiff) addModifiedPath(config *Config, state *state, path1
return pathsDiff.Modified.addPathDiff(config, state, path1, pathItemPair)
}

func filterPaths(filter, filterExtension string, paths1, paths2 *openapi3.Paths) error {
func filterPaths(matchPath, unmatchPath, filterExtension string, paths1, paths2 *openapi3.Paths) error {

if err := filterPathsByName(filter, paths1, paths2); err != nil {
if err := filterPathsByName(matchPath, true, paths1, paths2); err != nil {
return err
}

if err := filterPathsByName(unmatchPath, false, paths1, paths2); err != nil {
return err
}

Expand All @@ -116,7 +120,7 @@ func filterPaths(filter, filterExtension string, paths1, paths2 *openapi3.Paths)
return nil
}

func filterPathsByName(filter string, paths1, paths2 *openapi3.Paths) error {
func filterPathsByName(filter string, negate bool, paths1, paths2 *openapi3.Paths) error {
if filter == "" {
return nil
}
Expand All @@ -126,15 +130,19 @@ func filterPathsByName(filter string, paths1, paths2 *openapi3.Paths) error {
return fmt.Errorf("failed to compile filter regex %q: %w", filter, err)
}

filterPathsInternal(paths1, r)
filterPathsInternal(paths2, r)
filterPathsInternal(paths1, r, negate)
filterPathsInternal(paths2, r, negate)

return nil
}

func filterPathsInternal(paths *openapi3.Paths, r *regexp.Regexp) {
func filterPathsInternal(paths *openapi3.Paths, r *regexp.Regexp, negate bool) {
for path := range paths.Map() {
if !r.MatchString(path) {
match := r.MatchString(path)
if negate {
match = !match
}
if match {
paths.Delete(path)
}
}
Expand Down
7 changes: 7 additions & 0 deletions docs/FILTERING-ENDPOINTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ For example, this diff includes only endpoints containing "/api" in the path:
```
oasdiff diff --match-path "/api" https://raw.githubusercontent.com/Tufin/oasdiff/main/data/openapi-test1.yaml https://raw.githubusercontent.com/Tufin/oasdiff/main/data/openapi-test3.yaml -f text
```

Use the `--unmatch-path` option to exclude paths that match the given regular expression.
For example, this diff excludes endpoints containing "beta" in the path:
```
oasdiff diff --unmatch-path "beta" https://raw.githubusercontent.com/Tufin/oasdiff/main/data/openapi-test1.yaml https://raw.githubusercontent.com/Tufin/oasdiff/main/data/openapi-test3.yaml -f text
```

Note:
If a path contains a callback, the filter will be applied both to the path itself and to the callback path.
To include both the path and the callback, use a regular expression with a filter for each level, for example: "path|callback-path"
Expand Down
1 change: 1 addition & 0 deletions internal/cmd_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
func addCommonDiffFlags(cmd *cobra.Command) {
cmd.PersistentFlags().BoolP("composed", "c", false, "work in 'composed' mode, compare paths in all specs matching base and revision globs")
cmd.PersistentFlags().StringP("match-path", "p", "", "include only paths that match this regular expression")
cmd.PersistentFlags().StringP("unmatch-path", "q", "", "exclude paths that match this regular expression")
cmd.PersistentFlags().String("filter-extension", "", "exclude paths and operations with an OpenAPI Extension matching this regular expression")
cmd.PersistentFlags().String("prefix-base", "", "add this prefix to paths in base-spec before comparison")
cmd.PersistentFlags().String("prefix-revision", "", "add this prefix to paths in revised-spec before comparison")
Expand Down
3 changes: 2 additions & 1 deletion internal/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ func NewFlags() *Flags {

func (flags *Flags) toConfig() *diff.Config {
config := diff.NewConfig().WithExcludeElements(flags.getExcludeElements())
config.PathFilter = flags.v.GetString("match-path")
config.MatchPath = flags.v.GetString("match-path")
config.UnmatchPath = flags.v.GetString("unmatch-path")
config.FilterExtension = flags.v.GetString("filter-extension")
config.PathPrefixBase = flags.v.GetString("prefix-base")
config.PathPrefixRevision = flags.v.GetString("prefix-revision")
Expand Down
12 changes: 10 additions & 2 deletions internal/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,18 +344,26 @@ func Test_CustomSeverityLevelsInvalidFile(t *testing.T) {
require.Equal(t, 106, internal.Run(cmdToArgs("oasdiff changelog ../data/openapi-test1.yaml ../data/openapi-test3.yaml --severity-levels ../data/invalid.txt"), io.Discard, io.Discard))
}

func Test_Changelog_WithoutPathFilter(t *testing.T) {
func Test_Changelog_WithoutMatchPath(t *testing.T) {
var stdout bytes.Buffer
require.Zero(t, internal.Run(cmdToArgs("oasdiff changelog ../data/path-filter/base.yaml ../data/path-filter/revision.yaml --format json"), &stdout, io.Discard))
bc := formatters.Changes{}
require.NoError(t, json.Unmarshal(stdout.Bytes(), &bc))
require.Len(t, bc, 2)
}

func Test_Changelog_WithPathFilter(t *testing.T) {
func Test_Changelog_WithMatchPath(t *testing.T) {
var stdout bytes.Buffer
require.Zero(t, internal.Run(cmdToArgs("oasdiff changelog ../data/path-filter/base.yaml ../data/path-filter/revision.yaml --format json -p a"), &stdout, io.Discard))
bc := formatters.Changes{}
require.NoError(t, json.Unmarshal(stdout.Bytes(), &bc))
require.Len(t, bc, 1)
}

func Test_Changelog_WithUnmatchPath(t *testing.T) {
var stdout bytes.Buffer
require.Zero(t, internal.Run(cmdToArgs("oasdiff changelog ../data/path-filter/base.yaml ../data/path-filter/revision.yaml --format json -q a"), &stdout, io.Discard))
bc := formatters.Changes{}
require.NoError(t, json.Unmarshal(stdout.Bytes(), &bc))
require.Len(t, bc, 1)
}
1 change: 1 addition & 0 deletions internal/viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ type Config struct {
Severity []string `mapstructure:"severity"`
Tags []string `mapstructure:"tags"`
MatchPath string `mapstructure:"match-path"`
UnmatchPath string `mapstructure:"unmatch-path"`
FilterExtension string `mapstructure:"filter-extension"`
PrefixBase string `mapstructure:"prefix-base"`
PrefixRevision string `mapstructure:"prefix-revision"`
Expand Down

0 comments on commit efc43b7

Please sign in to comment.