Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkg/match/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Package match implements the simple glob matching used by routing rules to
// compare namespaces and metadata values.
//
// A pattern is one of four forms: a literal (exact match), a prefix glob
// ("foo*", starts-with), a suffix glob ("*foo", ends-with), or a contains glob
// ("*foo*"). A pattern consisting only of '*' (for example "*", "**") matches
// anything. A "*" anywhere other than the leading or trailing position is
// rejected at compile time.
package match
84 changes: 84 additions & 0 deletions pkg/match/match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package match

import (
"fmt"
"strings"
)

const (
literal kind = iota
prefix kind = iota
suffix kind = iota
contains kind = iota
all kind = iota
)

type (
// Matcher is a compiled glob pattern. The zero value matches only the empty
// string. Construct one with [Compile].
Matcher struct {
kind kind
text string
}

kind int
)

// Compile parses pattern into a [Matcher]. See the package docs for the
// supported forms.
func Compile(pattern string) (Matcher, error) {
leading := strings.HasPrefix(pattern, "*")
trailing := strings.HasSuffix(pattern, "*")

var m Matcher
switch {
case len(pattern) > 0 && strings.Count(pattern, "*") == len(pattern):
return Matcher{kind: all}, nil
case leading && trailing:
m = Matcher{kind: contains, text: pattern[1 : len(pattern)-1]}
case leading:
m = Matcher{kind: suffix, text: strings.TrimPrefix(pattern, "*")}
case trailing:
m = Matcher{kind: prefix, text: strings.TrimSuffix(pattern, "*")}
default:
m = Matcher{kind: literal, text: pattern}
}

// The literal portion must be free of wildcards; a "*" here means the
// pattern had one in an unsupported interior position (e.g. "a*b").
if strings.Contains(m.text, "*") {
return Matcher{}, fmt.Errorf("match: %q has a '*' outside the leading or trailing position", pattern)
}

return m, nil
}

// MustCompile is like [Compile] but panics if pattern is invalid. It is meant
// for package-level variables and tests where an invalid pattern is a
// programming error.
func MustCompile(pattern string) Matcher {
m, err := Compile(pattern)
if err != nil {
panic(err)
}

return m
}

// Match reports whether s satisfies the compiled pattern.
func (m Matcher) Match(s string) bool {
switch m.kind {
case literal:
return s == m.text
case prefix:
return strings.HasPrefix(s, m.text)
case suffix:
return strings.HasSuffix(s, m.text)
case contains:
return strings.Contains(s, m.text)
case all:
return true
default:
return false
}
}
93 changes: 93 additions & 0 deletions pkg/match/match_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package match_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/temporalio/temporal-proxy/pkg/match"
)

func TestMatcher(t *testing.T) {
t.Parallel()

tests := []struct {
name string
pattern string
matches []string
misses []string
}{
{
name: "literal",
pattern: "foo",
matches: []string{"foo"},
misses: []string{"foobar", "barfoo", ""},
},
{
name: "prefix",
pattern: "foo*",
matches: []string{"foo", "foobar"},
misses: []string{"barfoo", ""},
},
{
name: "suffix",
pattern: "*foo",
matches: []string{"foo", "barfoo"},
misses: []string{"foobar", ""},
},
{
name: "contains",
pattern: "*-test-*",
matches: []string{"prod-test-1", "-test-"},
misses: []string{"test", "prod-1", ""},
},
{
name: "bare star matches anything",
pattern: "*",
matches: []string{"anything", ""},
},
{
name: "all stars matches anything",
pattern: "**",
matches: []string{"anything", ""},
},
{
name: "empty pattern is literal empty",
pattern: "",
matches: []string{""},
misses: []string{"foo"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

m, err := match.Compile(tt.pattern)
require.NoError(t, err)

for _, s := range tt.matches {
require.Truef(t, m.Match(s), "pattern %q should match %q", tt.pattern, s)
}
for _, s := range tt.misses {
require.Falsef(t, m.Match(s), "pattern %q should not match %q", tt.pattern, s)
}
})
}
}

func TestCompileEmbeddedStar(t *testing.T) {
t.Parallel()

for _, pattern := range []string{"a*b", "*a*b", "a*b*", "a*b*c"} {
_, err := match.Compile(pattern)
require.Errorf(t, err, "pattern %q should be rejected", pattern)
}
}

func TestMustCompile(t *testing.T) {
t.Parallel()

require.NotPanics(t, func() { match.MustCompile("foo*") })
require.Panics(t, func() { match.MustCompile("a*b") })
}
Loading