Skip to content

Commit

Permalink
feat: support exclude all & patterns on exclusions as well (#260)
Browse files Browse the repository at this point in the history
* fix: MatchItems

* commutative: The order should't matter
* handle exclusion only patterns

* feat: support exclude all & matterns on exclusions as well

* more test cases
  • Loading branch information
adityathebe authored Jan 28, 2025
1 parent 22a1887 commit fc7274b
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 32 deletions.
93 changes: 75 additions & 18 deletions collections/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package collections

import (
"net/url"
"slices"
"strings"
)

Expand Down Expand Up @@ -73,48 +74,104 @@ func Append[T any](slices ...[]T) []T {
return output
}

// matchItems returns true if any of the items in the list match the item.
// negative matches are supported by prefixing the item with a "!".
// matchItems returns true if any of the patterns in the list match the item.
// negative matches are supported by prefixing the item with a "!" and
// takes precendence over positive match.
// * matches everything
// to match prefix and suffix use "*" accordingly.
func MatchItems(item string, items ...string) bool {
if len(items) == 0 {
func MatchItems(item string, patterns ...string) bool {
if len(patterns) == 0 {
return true
}

for _, i := range items {
i = strings.TrimSpace(i)
slices.SortFunc(patterns, sortPatterns)

i, _ := url.QueryUnescape(i)
for _, p := range patterns {
pattern, err := url.QueryUnescape(strings.TrimSpace(p))
if err != nil {
continue
}

if strings.HasPrefix(i, "!") {
if item == strings.TrimPrefix(i, "!") {
if strings.HasPrefix(pattern, "!") {
if matchPattern(item, strings.TrimPrefix(pattern, "!")) {
return false
}

continue
}

if i == "*" || item == i {
if matchPattern(item, pattern) {
return true
}
}

if strings.HasPrefix(i, "*") {
if strings.HasSuffix(item, strings.TrimPrefix(i, "*")) {
return true
}
//nolint:gosimple
//lint:ignore S1008 ...
if allExclusions(patterns) {
// If all the filters were exlusions, and none of the exclusions excluded the item, then it's a match
return true
}

return false
}

func matchPattern(item, pattern string) bool {
if pattern == "*" || item == pattern {
return true
}

if strings.HasPrefix(pattern, "*") {
if strings.HasSuffix(item, strings.TrimPrefix(pattern, "*")) {
return true
}
}

if strings.HasSuffix(i, "*") {
if strings.HasPrefix(item, strings.TrimSuffix(i, "*")) {
return true
}
if strings.HasSuffix(pattern, "*") {
if strings.HasPrefix(item, strings.TrimSuffix(pattern, "*")) {
return true
}
}

return false
}

// sortPatterns defines the priority for sorting:
// exclusions ("!") have higher priority than other patterns.
func sortPatterns(a, b string) int {
if a == "!*" {
return -1
} else if b == "!*" {
return 1
}

if strings.HasPrefix(a, "!") {
return -1
} else if strings.HasPrefix(b, "!") {
return 1
}

return 0
}

func allExclusions(patterns []string) bool {
if len(patterns) == 0 {
return false
}

for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
return false
}

if !strings.HasPrefix(pattern, "!") {
return false
}
}

return true
}

func DeleteEmptyStrings(s []string) []string {
r := make([]string, 0, len(s))
for _, str := range s {
Expand Down
94 changes: 80 additions & 14 deletions collections/slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,86 +9,152 @@ func TestMatchItems(t *testing.T) {
tests := []struct {
name string
item string
items []string
patterns []string
expected bool
}{
{
name: "Exact Match",
item: "apple",
items: []string{"apple"},
patterns: []string{"apple"},
expected: true,
},
{
name: "Negative Match",
item: "apple",
items: []string{"!apple"},
patterns: []string{"!apple"},
expected: false,
},
{
name: "Empty Items List",
item: "apple",
items: []string{},
patterns: []string{},
expected: true,
},
{
name: "Wildcard Match",
item: "apple",
items: []string{"*"},
patterns: []string{"*"},
expected: true,
},
{
name: "Wildcard Prefix Match",
item: "apple",
items: []string{"appl*"},
patterns: []string{"appl*"},
expected: true,
},
{
name: "Wildcard Suffix Match",
item: "apple",
items: []string{"*ple"},
patterns: []string{"*ple"},
expected: true,
},
{
name: "Mixed Matches",
item: "apple",
items: []string{"!banana", "appl*", "cherry"},
patterns: []string{"!banana", "appl*", "cherry"},
expected: true,
},
{
name: "No Items Match",
item: "apple",
items: []string{"!apple", "banana"},
patterns: []string{"!apple", "banana"},
expected: false,
},
{
name: "Multiple Wildcards",
item: "apple",
items: []string{"ap*e", "*p*"},
patterns: []string{"ap*e", "*p*"},
expected: false,
},
{
name: "Handle whitespaces | should be trimmed",
item: "hello",
items: []string{"hello ", "world"},
patterns: []string{"hello ", "world"},
expected: true,
},
{
name: "Handle whitespaces | should not be trimmed (no match)",
item: "hello",
items: []string{"hello%20", "world"},
patterns: []string{"hello%20", "world"},
expected: false,
},
{
name: "Handle whitespaces | should not be trimmed (match)",
item: "hello ",
items: []string{"hello%20", "world"},
patterns: []string{"hello%20", "world"},
expected: true,
},
{
name: "exclusion and inclusion",
item: "mission-control",
patterns: []string{"!mission-control", "mission-control"},
expected: false,
},
{
name: "inclusion and exclusion",
item: "mission-control",
patterns: []string{"mission-control", "!mission-control"},
expected: false,
},
{
name: "exclusion",
item: "mission-control",
patterns: []string{"!default"},
expected: true,
},
{
name: "Exclude All",
item: "anyitem",
patterns: []string{"!*"},
expected: false,
},
{
name: "Exclude All with Inclusion",
item: "apple",
patterns: []string{"!*", "apple"},
expected: false,
},
{
name: "Multiple Exclusions",
item: "apple",
patterns: []string{"!banana", "!orange", "!apple"},
expected: false,
},
{
name: "Empty Item with Patterns",
item: "",
patterns: []string{"*"},
expected: true,
},
{
name: "Empty Pattern String",
item: "apple",
patterns: []string{""},
expected: false,
},
{
name: "URL Encoded Pattern Matches",
item: "hello ",
patterns: []string{"hello%20"},
expected: true,
},
{
name: "URL Encoded Pattern Does Not Match",
item: "hello",
patterns: []string{"hello%20"},
expected: false,
},
{
name: "Malformed URL Encoding",
item: "apple",
patterns: []string{"%zzapple"},
expected: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := MatchItems(test.item, test.items...)
result := MatchItems(test.item, test.patterns...)
if result != test.expected {
t.Errorf("Expected %v but got %v", test.expected, result)
}
Expand Down

0 comments on commit fc7274b

Please sign in to comment.