add suffix and pathSuffix filters (#1659)

## Description

Preparing for handling sharepoint webUrl
selectors, we will need suffix-matching filters.

## Type of change

- [x] 🌻 Feature

## Issue(s)

* #1616

## Test Plan

- [x]  Unit test
This commit is contained in:
Keepers 2022-12-01 21:44:01 -07:00 committed by GitHub
parent 34832591ce
commit 34ba06cf61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 181 additions and 0 deletions

View File

@ -28,10 +28,14 @@ const (
IdentityValue IdentityValue
// "foo" is a prefix of "foobarbaz" // "foo" is a prefix of "foobarbaz"
TargetPrefixes TargetPrefixes
// "baz" is a suffix of "foobarbaz"
TargetSuffixes
// "foo" equals any complete element prefix of "foo/bar/baz" // "foo" equals any complete element prefix of "foo/bar/baz"
TargetPathPrefix TargetPathPrefix
// "foo" equals any complete element in "foo/bar/baz" // "foo" equals any complete element in "foo/bar/baz"
TargetPathContains TargetPathContains
// "baz" equals any complete element suffix of "foo/bar/baz"
TargetPathSuffix
) )
func norm(s string) string { func norm(s string) string {
@ -161,6 +165,18 @@ func NotPrefix(target string) Filter {
return newFilter(TargetPrefixes, target, true) return newFilter(TargetPrefixes, target, true)
} }
// Suffix creates a filter where Compare(v) is true if
// target.Suffix(v)
func Suffix(target string) Filter {
return newFilter(TargetSuffixes, target, false)
}
// NotSuffix creates a filter where Compare(v) is true if
// !target.Suffix(v)
func NotSuffix(target string) Filter {
return newFilter(TargetSuffixes, target, true)
}
// PathPrefix creates a filter where Compare(v) is true if // PathPrefix creates a filter where Compare(v) is true if
// target.Prefix(v) && // target.Prefix(v) &&
// split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1 // split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1
@ -241,6 +257,44 @@ func NotPathContains(targets []string) Filter {
return newSliceFilter(TargetPathContains, targets, tgts, true) return newSliceFilter(TargetPathContains, targets, tgts, true)
} }
// PathSuffix creates a filter where Compare(v) is true if
// target.Suffix(v) &&
// split(target)[i].Equals(split(v)[i]) for _all_ i in 0..len(target)-1
// ex: target "/bar/baz" returns true for input "/foo/bar/baz",
// but false for "/foobar/baz"
//
// Unlike single-target filters, this filter accepts a
// slice of targets, will compare an input against each target
// independently, and returns true if one or more of the
// comparisons succeed.
func PathSuffix(targets []string) Filter {
tgts := make([]string, len(targets))
for i := range targets {
tgts[i] = normPathElem(targets[i])
}
return newSliceFilter(TargetPathSuffix, targets, tgts, false)
}
// NotPathSuffix creates a filter where Compare(v) is true if
// !target.Suffix(v) ||
// !split(target)[i].Equals(split(v)[i]) for _any_ i in 0..len(target)-1
// ex: target "/bar/baz" returns false for input "/foo/bar/baz",
// but true for "/foobar/baz"
//
// Unlike single-target filters, this filter accepts a
// slice of targets, will compare an input against each target
// independently, and returns true if one or more of the
// comparisons succeed.
func NotPathSuffix(targets []string) Filter {
tgts := make([]string, len(targets))
for i := range targets {
tgts[i] = normPathElem(targets[i])
}
return newSliceFilter(TargetPathSuffix, targets, tgts, true)
}
// newFilter is the standard filter constructor. // newFilter is the standard filter constructor.
func newFilter(c comparator, target string, negate bool) Filter { func newFilter(c comparator, target string, negate bool) Filter {
return Filter{ return Filter{
@ -302,12 +356,17 @@ func (f Filter) Compare(input string) bool {
cmp = in cmp = in
case TargetPrefixes: case TargetPrefixes:
cmp = prefixed cmp = prefixed
case TargetSuffixes:
cmp = suffixed
case TargetPathPrefix: case TargetPathPrefix:
cmp = pathPrefix cmp = pathPrefix
hasSlice = true hasSlice = true
case TargetPathContains: case TargetPathContains:
cmp = pathContains cmp = pathContains
hasSlice = true hasSlice = true
case TargetPathSuffix:
cmp = pathSuffix
hasSlice = true
case Passes: case Passes:
return true return true
case Fails: case Fails:
@ -364,6 +423,11 @@ func prefixed(target, input string) bool {
return strings.HasPrefix(input, target) return strings.HasPrefix(input, target)
} }
// true if target has input as a suffix.
func suffixed(target, input string) bool {
return strings.HasSuffix(input, target)
}
// true if target is an _element complete_ prefix match // true if target is an _element complete_ prefix match
// on the input. Element complete means we do not // on the input. Element complete means we do not
// succeed on partial element matches (ex: "/foo" does // succeed on partial element matches (ex: "/foo" does
@ -393,6 +457,20 @@ func pathContains(target, input string) bool {
return strings.Contains(normPathElem(input), target) return strings.Contains(normPathElem(input), target)
} }
// true if target is an _element complete_ suffix match
// on the input. Element complete means we do not
// succeed on partial element matches (ex: "/bar" does
// not match "/foobar").
//
// As a precondition, assumes the target value has been
// passed through normPathElem().
//
// The input is assumed to be the complete path that may
// have the target as a suffix.
func pathSuffix(target, input string) bool {
return strings.HasSuffix(normPathElem(input), target)
}
// ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------
// Helpers // Helpers
// ---------------------------------------------------------------------------------------------------- // ----------------------------------------------------------------------------------------------------
@ -405,8 +483,10 @@ var prefixString = map[comparator]string{
TargetContains: "cont:", TargetContains: "cont:",
TargetIn: "in:", TargetIn: "in:",
TargetPrefixes: "pfx:", TargetPrefixes: "pfx:",
TargetSuffixes: "sfx:",
TargetPathPrefix: "pathPfx:", TargetPathPrefix: "pathPfx:",
TargetPathContains: "pathCont:", TargetPathContains: "pathCont:",
TargetPathSuffix: "pathSfx:",
} }
func (f Filter) String() string { func (f Filter) String() string {

View File

@ -206,6 +206,31 @@ func (suite *FiltersSuite) TestPrefixes() {
} }
} }
func (suite *FiltersSuite) TestSuffixes() {
target := "folderB"
f := filters.Suffix(target)
nf := filters.NotSuffix(target)
table := []struct {
name string
input string
expectF assert.BoolAssertionFunc
expectNF assert.BoolAssertionFunc
}{
{"Exact match - same case", "folderB", assert.True, assert.False},
{"Exact match - different case", "Folderb", assert.True, assert.False},
{"Suffix match - same case", "folderA/folderB", assert.True, assert.False},
{"Suffix match - different case", "Foldera/folderb", assert.True, assert.False},
{"Should not match substring", "folderB/folder1", assert.False, assert.True},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
test.expectF(t, f.Compare(test.input), "filter")
test.expectNF(t, nf.Compare(test.input), "negated filter")
})
}
}
func (suite *FiltersSuite) TestPathPrefix() { func (suite *FiltersSuite) TestPathPrefix() {
table := []struct { table := []struct {
name string name string
@ -360,3 +385,79 @@ func (suite *FiltersSuite) TestPathContains_NormalizedTargets() {
}) })
} }
} }
func (suite *FiltersSuite) TestPathSuffix() {
table := []struct {
name string
targets []string
input string
expectF assert.BoolAssertionFunc
expectNF assert.BoolAssertionFunc
}{
{"Exact - same case", []string{"fA"}, "/fA", assert.True, assert.False},
{"Exact - different case", []string{"fa"}, "/fA", assert.True, assert.False},
{"Suffix - same case", []string{"fB"}, "/fA/fB", assert.True, assert.False},
{"Suffix - different case", []string{"fb"}, "/fA/fB", assert.True, assert.False},
{"Exact - multiple folders", []string{"fA/fB"}, "/fA/fB", assert.True, assert.False},
{"Suffix - single folder partial", []string{"f"}, "/fA/fB", assert.False, assert.True},
{"Suffix - multi folder partial", []string{"A/fB"}, "/fA/fB", assert.False, assert.True},
{"Target Longer - single folder", []string{"fA"}, "/f", assert.False, assert.True},
{"Target Longer - multi folder", []string{"fA/fB"}, "/fA/f", assert.False, assert.True},
{"Not suffix - single folder", []string{"fA"}, "/af", assert.False, assert.True},
{"Not suffix - multi folder", []string{"fA/fB"}, "/Af/fB", assert.False, assert.True},
{"Exact - target variations - none", []string{"fA"}, "/fA", assert.True, assert.False},
{"Exact - target variations - prefix", []string{"/fA"}, "/fA", assert.True, assert.False},
{"Exact - target variations - suffix", []string{"fA/"}, "/fA", assert.True, assert.False},
{"Exact - target variations - both", []string{"/fA/"}, "/fA", assert.True, assert.False},
{"Exact - input variations - none", []string{"fA"}, "fA", assert.True, assert.False},
{"Exact - input variations - prefix", []string{"fA"}, "/fA", assert.True, assert.False},
{"Exact - input variations - suffix", []string{"fA"}, "fA/", assert.True, assert.False},
{"Exact - input variations - both", []string{"fA"}, "/fA/", assert.True, assert.False},
{"Suffix - target variations - none", []string{"fb"}, "/fA/fb", assert.True, assert.False},
{"Suffix - target variations - prefix", []string{"/fb"}, "/fA/fb", assert.True, assert.False},
{"Suffix - target variations - suffix", []string{"fb/"}, "/fA/fb", assert.True, assert.False},
{"Suffix - target variations - both", []string{"/fb/"}, "/fA/fb", assert.True, assert.False},
{"Suffix - input variations - none", []string{"fb"}, "fA/fb", assert.True, assert.False},
{"Suffix - input variations - prefix", []string{"fb"}, "/fA/fb", assert.True, assert.False},
{"Suffix - input variations - suffix", []string{"fb"}, "fA/fb/", assert.True, assert.False},
{"Suffix - input variations - both", []string{"fb"}, "/fA/fb/", assert.True, assert.False},
{"Slice - one matches", []string{"foo", "fa/f", "fb"}, "/fA/fb", assert.True, assert.True},
{"Slice - none match", []string{"foo", "fa/f", "f"}, "/fA/fb", assert.False, assert.True},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
f := filters.PathSuffix(test.targets)
nf := filters.NotPathSuffix(test.targets)
test.expectF(t, f.Compare(test.input), "filter")
test.expectNF(t, nf.Compare(test.input), "negated filter")
})
}
}
func (suite *FiltersSuite) TestPathSuffix_NormalizedTargets() {
table := []struct {
name string
targets []string
expect []string
}{
{"Single - no slash", []string{"fA"}, []string{"/fA/"}},
{"Single - pre slash", []string{"/fA"}, []string{"/fA/"}},
{"Single - suff slash", []string{"fA/"}, []string{"/fA/"}},
{"Single - both slashes", []string{"/fA/"}, []string{"/fA/"}},
{"Multipath - no slash", []string{"fA/fB"}, []string{"/fA/fB/"}},
{"Multipath - pre slash", []string{"/fA/fB"}, []string{"/fA/fB/"}},
{"Multipath - suff slash", []string{"fA/fB/"}, []string{"/fA/fB/"}},
{"Multipath - both slashes", []string{"/fA/fB/"}, []string{"/fA/fB/"}},
{"Multi input - no slash", []string{"fA", "fB"}, []string{"/fA/", "/fB/"}},
{"Multi input - pre slash", []string{"/fA", "/fB"}, []string{"/fA/", "/fB/"}},
{"Multi input - suff slash", []string{"fA/", "fB/"}, []string{"/fA/", "/fB/"}},
{"Multi input - both slashes", []string{"/fA/", "/fB/"}, []string{"/fA/", "/fB/"}},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
f := filters.PathSuffix(test.targets)
assert.Equal(t, test.expect, f.NormalizedTargets)
})
}
}