--- /dev/null
+package slices2
+
+func Map[S any, V any](in []S, transform func(S) V) []V {
+ out := make([]V, len(in))
+ for i, val := range in {
+ out[i] = transform(val)
+ }
+ return out
+}
package stringmatch
import (
+ "fmt"
"regexp"
"strings"
+
+ "go.fuhry.dev/runtime/utils/slices2"
)
type StringMatcher interface {
Match(input string) bool
+ String() string
}
type Prefix string
return strings.HasPrefix(input, string(s))
}
+func (s Prefix) String() string {
+ return fmt.Sprintf("%T(%s)", s, string(s))
+}
+
func (s Suffix) Match(input string) bool {
return strings.HasSuffix(input, string(s))
}
+func (s Suffix) String() string {
+ return fmt.Sprintf("%T(%s)", s, string(s))
+}
+
func (s Exact) Match(input string) bool {
return input == string(s)
}
+func (s Exact) String() string {
+ return fmt.Sprintf("%T(%s)", s, string(s))
+}
+
func (s Contains) Match(input string) bool {
return strings.Contains(input, string(s))
}
+func (s Contains) String() string {
+ return fmt.Sprintf("%T(%s)", s, string(s))
+}
+
func (s Regexp) Match(input string) bool {
re := regexp.MustCompile(string(s))
return re.MatchString(input)
}
+func (s Regexp) String() string {
+ return fmt.Sprintf("%T(%s)", s, string(s))
+}
+
type andMatcher struct {
matchers []StringMatcher
}
return true
}
+func (mm *andMatcher) String() string {
+ matcherStrs := slices2.Map(
+ mm.matchers,
+ func(sm StringMatcher) string { return sm.String() },
+ )
+ return fmt.Sprintf("And(%s)", strings.Join(matcherStrs, " & "))
+}
+
func And(matchers ...StringMatcher) StringMatcher {
return &andMatcher{
matchers: matchers,
return false
}
+func (mm *orMatcher) String() string {
+ matcherStrs := slices2.Map(
+ mm.matchers,
+ func(sm StringMatcher) string { return sm.String() },
+ )
+ return fmt.Sprintf("Or(%s)", strings.Join(matcherStrs, " & "))
+}
+
func Or(matchers ...StringMatcher) StringMatcher {
return &orMatcher{
matchers: matchers,
--- /dev/null
+package stringmatch
+
+import (
+ "fmt"
+ "os"
+ "testing"
+)
+
+type testCase struct {
+ matcher StringMatcher
+ input string
+ expect bool
+}
+
+func (tc *testCase) Run(t *testing.T) {
+ if tc.matcher.Match(tc.input) != tc.expect {
+ fmt.Fprintf(os.Stderr, "FAILED: %s on %q: expect: %t, got %t\n",
+ tc.matcher.String(), tc.input, tc.expect, !tc.expect)
+ t.Fail()
+ }
+}
+
+func TestPrefix(t *testing.T) {
+ cases := []*testCase{
+ {Prefix("/foo"), "/foo/bar", true},
+ {Prefix("/foo"), "/bar", false},
+ }
+
+ for _, tc := range cases {
+ tc.Run(t)
+ }
+}
+
+func TestSuffix(t *testing.T) {
+ cases := []*testCase{
+ {Suffix("/foo"), "/foo/bar", false},
+ {Suffix("/bar"), "/bar", true},
+ }
+
+ for _, tc := range cases {
+ tc.Run(t)
+ }
+}
+
+func TestExact(t *testing.T) {
+ cases := []*testCase{
+ {Exact("/foo"), "/foo", true},
+ {Exact("/foo"), "/bar", false},
+ }
+
+ for _, tc := range cases {
+ tc.Run(t)
+ }
+}
+
+func TestContains(t *testing.T) {
+ cases := []*testCase{
+ {Contains("o"), "/foo", true},
+ {Contains("a"), "/foo", false},
+ }
+
+ for _, tc := range cases {
+ tc.Run(t)
+ }
+}
+
+func TestRegexp(t *testing.T) {
+ cases := []*testCase{
+ {Regexp("^/foo"), "/foo/bar", true},
+ {Regexp("/foo$"), "/bar", false},
+ }
+
+ for _, tc := range cases {
+ tc.Run(t)
+ }
+}
+
+func TestAnd(t *testing.T) {
+ cases := []*testCase{
+ {And(Prefix("/foo"), Suffix("/bar")), "/foo/bar", true},
+ {And(Prefix("/foo"), Suffix("/baz")), "/foo/bar", false},
+ }
+
+ for _, tc := range cases {
+ tc.Run(t)
+ }
+}
+
+func TestOr(t *testing.T) {
+ cases := []*testCase{
+ {Or(Prefix("/foo"), Suffix("/bar")), "/foo/bar", true},
+ {Or(Contains("/baz"), Contains("/quux")), "/foo/bar", false},
+ }
+
+ for _, tc := range cases {
+ tc.Run(t)
+ }
+}
--- /dev/null
+package stringmatch
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+)
+
+type MatchRule struct {
+ Mode string `yaml:"mode" json:"mode"`
+ Value string `yaml:"value" json:"value"`
+ Rules []*MatchRule `yaml:"rules" json:"rules"`
+}
+
+func (m *MatchRule) Matcher() (StringMatcher, error) {
+ switch strings.ToLower(m.Mode) {
+ case "prefix", "startswith":
+ if m.Value == "" {
+ return nil, errors.New("prefix matcher is missing value")
+ }
+ if len(m.Rules) != 0 {
+ return nil, errors.New("prefix matcher may not have sub-rules")
+ }
+ return Prefix(m.Value), nil
+ case "suffix", "endswith":
+ if m.Value == "" {
+ return nil, errors.New("suffix matcher is missing value")
+ }
+ if len(m.Rules) != 0 {
+ return nil, errors.New("suffix matcher may not have sub-rules")
+ }
+ return Suffix(m.Value), nil
+ case "exact", "equals", "eq":
+ if m.Value == "" {
+ return nil, errors.New("exact matcher is missing value")
+ }
+ if len(m.Rules) != 0 {
+ return nil, errors.New("exact matcher may not have sub-rules")
+ }
+ return Exact(m.Value), nil
+ case "contains", "in":
+ if m.Value == "" {
+ return nil, errors.New("contains matcher is missing value")
+ }
+ if len(m.Rules) != 0 {
+ return nil, errors.New("contains matcher may not have sub-rules")
+ }
+ return Contains(m.Value), nil
+ case "regexp", "regex":
+ if m.Value == "" {
+ return nil, errors.New("regexp matcher is missing value")
+ }
+ if len(m.Rules) != 0 {
+ return nil, errors.New("regexp matcher may not have sub-rules")
+ }
+ return Regexp(m.Value), nil
+ case "and":
+ if m.Value != "" {
+ return nil, errors.New("\"and\" matcher may not have a unary value")
+ }
+ if len(m.Rules) < 1 {
+ return nil, errors.New("no rules present in \"and\" matcher")
+ }
+ and := &andMatcher{}
+ for i, r := range m.Rules {
+ matcher, err := r.Matcher()
+ if err != nil {
+ return nil, fmt.Errorf("while processing rule %d: %v", i, err)
+ }
+ and.matchers = append(and.matchers, matcher)
+ }
+ return and, nil
+ case "or":
+ if m.Value != "" {
+ return nil, errors.New("\"or\" matcher may not have a unary value")
+ }
+ if len(m.Rules) < 1 {
+ return nil, errors.New("no rules present in \"or\" matcher")
+ }
+ or := &orMatcher{}
+ for i, r := range m.Rules {
+ matcher, err := r.Matcher()
+ if err != nil {
+ return nil, fmt.Errorf("while processing rule %d: %v", i, err)
+ }
+ or.matchers = append(or.matchers, matcher)
+ }
+ return or, nil
+ }
+
+ return nil, fmt.Errorf("unknown matching mode: %q", m.Mode)
+}
+
+func (m *MatchRule) Match(s string) bool {
+ matcher, err := m.Matcher()
+ if err != nil {
+ panic(err)
+ }
+ return matcher.Match(s)
+}
--- /dev/null
+package stringmatch
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ "encoding/json"
+
+ "gopkg.in/yaml.v3"
+)
+
+type serializationTestCase struct {
+ json string
+ yaml string
+ expect StringMatcher
+ expectErr StringMatcher
+}
+
+func (tc *serializationTestCase) Test(t *testing.T) {
+ if tc.json != "" {
+ tc.testJson(t)
+ } else if tc.yaml != "" {
+ tc.testYaml(t)
+ } else {
+ fmt.Fprintf(os.Stderr, "test case json or yaml is unset\n")
+ t.FailNow()
+ }
+}
+
+func (tc *serializationTestCase) testJson(t *testing.T) {
+ rule := &MatchRule{}
+ err := json.Unmarshal([]byte(strings.TrimSpace(tc.json)), rule)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "json unmarshal failed for %q: %v\n", tc.json, err)
+ t.Fail()
+ return
+ }
+
+ tc.testRule(t, rule)
+}
+
+func (tc *serializationTestCase) testYaml(t *testing.T) {
+ rule := &MatchRule{}
+ err := yaml.Unmarshal([]byte(strings.TrimSpace(tc.yaml)), rule)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "yaml unmarshal failed for %q: %v\n", tc.json, err)
+ t.Fail()
+ return
+ }
+
+ tc.testRule(t, rule)
+}
+
+func (tc *serializationTestCase) testRule(t *testing.T, rule *MatchRule) {
+ matcher, err := rule.Matcher()
+ if err != nil {
+ if tc.expectErr == nil {
+ fmt.Fprintf(
+ os.Stderr,
+ "serialization rule %q matcher compilation unexpectedly failed: %v\n",
+ tc.json, err)
+
+ t.Fail()
+ return
+ }
+
+ if !tc.expectErr.Match(err.Error()) {
+ fmt.Fprintf(
+ os.Stderr,
+ "serialization rule %q matcher compilation failed with an unexpected error: %v\n",
+ tc.json, err)
+
+ t.Fail()
+ return
+ }
+
+ // error matches expected error
+ return
+ }
+
+ if matcher.String() != tc.expect.String() {
+ fmt.Fprintf(
+ os.Stderr,
+ "json serialization rule %q matcher compilation failed with an unexpected error: %v\n",
+ tc.json, err)
+
+ t.Fail()
+ }
+}
+
+func TestJson(t *testing.T) {
+ testCases := []*serializationTestCase{
+ {
+ json: `{"mode":"prefix","value":"test"}`,
+ expect: Prefix("test"),
+ },
+ {
+ json: `{"mode":"prefix","value":""}`,
+ expectErr: Exact("prefix matcher is missing value"),
+ },
+ {
+ json: `{"mode":"prefix","value":"x","rules":[{"mode":"exact"}]}`,
+ expectErr: Exact("prefix matcher may not have sub-rules"),
+ },
+ {
+ json: `{
+ "mode":"and",
+ "rules":[
+ {"mode":"contains","value":"a"},
+ {"mode":"contains","value":"b"}
+ ]}`,
+ expect: And(Contains("a"), Contains("b")),
+ },
+ }
+
+ for _, tc := range testCases {
+ tc.Test(t)
+ }
+}
+
+func TestYaml(t *testing.T) {
+ testCases := []*serializationTestCase{
+ {
+ yaml: `---
+mode: prefix
+value: test
+`,
+ expect: Prefix("test"),
+ },
+ {
+ yaml: `---
+mode: prefix
+value: ""`,
+ expectErr: Exact("prefix matcher is missing value"),
+ },
+ {
+ yaml: `---
+mode: prefix
+value: x
+rules:
+ - mode: exact`,
+ expectErr: Exact("prefix matcher may not have sub-rules"),
+ },
+ {
+ yaml: `---
+mode: and
+rules:
+ - mode: contains
+ value: a
+ - mode: contains
+ value: b`,
+ expect: And(Contains("a"), Contains("b")),
+ },
+ }
+
+ for _, tc := range testCases {
+ tc.Test(t)
+ }
+}