From: Dan Fuhry Date: Wed, 3 Jun 2026 04:12:29 +0000 (-0400) Subject: [stringmatch] add "not" matcher X-Git-Url: https://go.fuhry.dev/?a=commitdiff_plain;h=cd963339898a9f3df873c7d7968071d03528067a;p=runtime.git [stringmatch] add "not" matcher --- diff --git a/utils/stringmatch/matchers.go b/utils/stringmatch/matchers.go index 19f475c..99db8ba 100644 --- a/utils/stringmatch/matchers.go +++ b/utils/stringmatch/matchers.go @@ -14,13 +14,15 @@ type StringMatcher interface { Sub(vars map[string]string) StringMatcher } -type Prefix string -type Suffix string -type Exact string -type Contains string -type Regexp string -type Any struct{} -type Never struct{} +type ( + Prefix string + Suffix string + Exact string + Contains string + Regexp string + Any struct{} + Never struct{} +) type MatchableString interface { ~string @@ -120,6 +122,26 @@ func (s Never) Sub(vars map[string]string) StringMatcher { return s } +func Not(matcher StringMatcher) StringMatcher { + return ¬Matcher{matcher: matcher} +} + +type notMatcher struct { + matcher StringMatcher +} + +func (s *notMatcher) Match(input string) bool { + return !s.matcher.Match(input) +} + +func (s *notMatcher) String() string { + return fmt.Sprintf("%T(%s)", s, s.matcher.String()) +} + +func (s *notMatcher) Sub(vars map[string]string) StringMatcher { + return Not(s.matcher.Sub(vars)) +} + type andMatcher struct { matchers []StringMatcher } diff --git a/utils/stringmatch/matchers_test.go b/utils/stringmatch/matchers_test.go index 3663a57..f8b82a9 100644 --- a/utils/stringmatch/matchers_test.go +++ b/utils/stringmatch/matchers_test.go @@ -75,6 +75,21 @@ func TestRegexp(t *testing.T) { } } +func TestNot(t *testing.T) { + cases := []*testCase{ + {Not(Prefix("/foo")), "/foo/bar", false}, + {Not(Prefix("/foo")), "/bar", true}, + {Not(Suffix("/foo")), "/foo/bar", true}, + {Not(Suffix("/bar")), "/bar", false}, + {Not(Contains("o")), "/foo", false}, + {Not(Contains("a")), "/foo", true}, + } + + for _, tc := range cases { + tc.Run(t) + } +} + func TestAnd(t *testing.T) { cases := []*testCase{ {And(Prefix("/foo"), Suffix("/bar")), "/foo/bar", true}, diff --git a/utils/stringmatch/serialization.go b/utils/stringmatch/serialization.go index 170b815..ae7b66d 100644 --- a/utils/stringmatch/serialization.go +++ b/utils/stringmatch/serialization.go @@ -9,6 +9,7 @@ import ( type MatchRule struct { Mode string `yaml:"mode" json:"mode"` Value string `yaml:"value" json:"value"` + Rule *MatchRule `yaml:"rule" json:"rule"` Rules []*MatchRule `yaml:"rules" json:"rules"` } @@ -18,6 +19,9 @@ func (m *MatchRule) Matcher() (StringMatcher, error) { if m.Value == "" { return nil, errors.New("prefix matcher is missing value") } + if m.Rule != nil { + return nil, errors.New("prefix matcher may not have child rule") + } if len(m.Rules) != 0 { return nil, errors.New("prefix matcher may not have sub-rules") } @@ -26,6 +30,9 @@ func (m *MatchRule) Matcher() (StringMatcher, error) { if m.Value == "" { return nil, errors.New("suffix matcher is missing value") } + if m.Rule != nil { + return nil, errors.New("suffix matcher may not have child rule") + } if len(m.Rules) != 0 { return nil, errors.New("suffix matcher may not have sub-rules") } @@ -34,6 +41,9 @@ func (m *MatchRule) Matcher() (StringMatcher, error) { if m.Value == "" { return nil, errors.New("exact matcher is missing value") } + if m.Rule != nil { + return nil, errors.New("exact matcher may not have child rule") + } if len(m.Rules) != 0 { return nil, errors.New("exact matcher may not have sub-rules") } @@ -42,6 +52,9 @@ func (m *MatchRule) Matcher() (StringMatcher, error) { if m.Value == "" { return nil, errors.New("contains matcher is missing value") } + if m.Rule != nil { + return nil, errors.New("contains matcher may not have child rule") + } if len(m.Rules) != 0 { return nil, errors.New("contains matcher may not have sub-rules") } @@ -50,17 +63,35 @@ func (m *MatchRule) Matcher() (StringMatcher, error) { if m.Value == "" { return nil, errors.New("regexp matcher is missing value") } + if m.Rule != nil { + return nil, errors.New("regexp matcher may not have child rule") + } if len(m.Rules) != 0 { return nil, errors.New("regexp matcher may not have sub-rules") } return Regexp(m.Value), nil + case "not", "invert", "inverse": + if m.Value != "" { + return nil, errors.New("not matcher may not have a unary value") + } + if len(m.Rules) != 0 { + return nil, errors.New("not matcher may not have sub-rules") + } + if m.Rule == nil { + return nil, errors.New("not matcher must have a singular child rule in `rule'") + } + matcher, err := m.Rule.Matcher() + if err != nil { + return nil, fmt.Errorf("while processing 'not' rule: %v", err) + } + return Not(matcher), nil case "any": - if m.Value != "" || len(m.Rules) != 0 { + if m.Value != "" || m.Rule != nil || len(m.Rules) != 0 { return nil, errors.New("any matcher does not accept a value or sub-rules") } return Any{}, nil case "never": - if m.Value != "" || len(m.Rules) != 0 { + if m.Value != "" || m.Rule != nil || len(m.Rules) != 0 { return nil, errors.New("never matcher does not accept a value or sub-rules") } return Never{}, nil @@ -68,6 +99,9 @@ func (m *MatchRule) Matcher() (StringMatcher, error) { if m.Value != "" { return nil, errors.New("\"and\" matcher may not have a unary value") } + if m.Rule != nil { + return nil, errors.New("\"and\" matcher may not have a singular child rule") + } if len(m.Rules) < 1 { return nil, errors.New("no rules present in \"and\" matcher") } @@ -84,6 +118,9 @@ func (m *MatchRule) Matcher() (StringMatcher, error) { if m.Value != "" { return nil, errors.New("\"or\" matcher may not have a unary value") } + if m.Rule != nil { + return nil, errors.New("\"or\" matcher may not have a singular child rule") + } if len(m.Rules) < 1 { return nil, errors.New("no rules present in \"or\" matcher") } diff --git a/utils/stringmatch/serialization_test.go b/utils/stringmatch/serialization_test.go index 9b7f251..80441dc 100644 --- a/utils/stringmatch/serialization_test.go +++ b/utils/stringmatch/serialization_test.go @@ -1,13 +1,12 @@ package stringmatch import ( + "encoding/json" "fmt" "os" "strings" "testing" - "encoding/json" - "gopkg.in/yaml.v3" ) @@ -153,6 +152,15 @@ rules: value: b`, expect: And(Contains("a"), Contains("b")), }, + { + yaml: `--- +mode: not +rule: + mode: contains + value: a +`, + expect: Not(Contains("a")), + }, } for _, tc := range testCases {