]> go.fuhry.dev Git - runtime.git/commitdiff
[stringmatch] add "not" matcher
authorDan Fuhry <dan@fuhry.com>
Wed, 3 Jun 2026 04:12:29 +0000 (00:12 -0400)
committerDan Fuhry <dan@fuhry.com>
Wed, 3 Jun 2026 04:12:29 +0000 (00:12 -0400)
utils/stringmatch/matchers.go
utils/stringmatch/matchers_test.go
utils/stringmatch/serialization.go
utils/stringmatch/serialization_test.go

index 19f475cc77493602df4336fe4987248c1a1df259..99db8ba47904ad7b95be2cbc5ae025c74a8aa54f 100644 (file)
@@ -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 &notMatcher{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
 }
index 3663a57c156db06bfa0c9f40d86cd6c1bfc32f22..f8b82a98f6f47ff4ce5f63499e07b8cc61acb262 100644 (file)
@@ -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},
index 170b8159bf795f0bfe0482cd8b250d0ac49b6b7d..ae7b66dccba40e93be2c5b524ca949f4aa494988 100644 (file)
@@ -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")
                }
index 9b7f2516707a406c00f31c1f443b57aa94d5fba9..80441dc3c4c1f4186f82504184b68dd85d9494e5 100644 (file)
@@ -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 {