Add basic prefix matcher type (#3054)

Adds a basic generic prefix matcher type. The
first implementation just uses a map and iterates
through things

This is expected to be used for at least the
exclude list for backups (inner type of
`map[string]struct{}`) and logic for merging
backup details (inner type still TBD a bit,
but either `*path.Builder` or a custom struct)

---

#### Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [x]  No

#### Type of change

- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #2486

#### Test Plan

- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-04-10 12:11:53 -07:00 committed by GitHub
parent 27909592b8
commit 89ac00e64e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 195 additions and 0 deletions

View File

@ -0,0 +1,68 @@
package prefixmatcher
import (
"strings"
)
type View[T any] interface {
Get(key string) (T, bool)
LongestPrefix(key string) (string, T, bool)
Empty() bool
}
type Matcher[T any] interface {
// Add adds or updates the item with key to have value value.
Add(key string, value T)
View[T]
}
type prefixMatcher[T any] struct {
data map[string]T
}
func (m *prefixMatcher[T]) Add(key string, value T) {
m.data[key] = value
}
func (m *prefixMatcher[T]) Get(key string) (T, bool) {
if m == nil {
return *new(T), false
}
res, ok := m.data[key]
return res, ok
}
func (m *prefixMatcher[T]) LongestPrefix(key string) (string, T, bool) {
if m == nil {
return "", *new(T), false
}
var (
rk string
rv T
found bool
// Set to -1 so if there's "" as a prefix ("all match") we still select it.
longest = -1
)
for k, v := range m.data {
if strings.HasPrefix(key, k) && len(k) > longest {
found = true
longest = len(k)
rk = k
rv = v
}
}
return rk, rv, found
}
func (m prefixMatcher[T]) Empty() bool {
return len(m.data) == 0
}
func NewMatcher[T any]() Matcher[T] {
return &prefixMatcher[T]{data: map[string]T{}}
}

View File

@ -0,0 +1,127 @@
package prefixmatcher_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/tester"
)
type PrefixMatcherUnitSuite struct {
tester.Suite
}
func TestPrefixMatcherUnitSuite(t *testing.T) {
suite.Run(t, &PrefixMatcherUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *PrefixMatcherUnitSuite) TestEmpty() {
pm := prefixmatcher.NewMatcher[string]()
assert.True(suite.T(), pm.Empty())
}
func (suite *PrefixMatcherUnitSuite) TestAdd_Get() {
t := suite.T()
pm := prefixmatcher.NewMatcher[string]()
kvs := map[string]string{
"hello": "world",
"hola": "mundo",
"foo": "bar",
}
for k, v := range kvs {
pm.Add(k, v)
}
for k, v := range kvs {
val, ok := pm.Get(k)
assert.True(t, ok, "searching for key", k)
assert.Equal(t, v, val, "returned value")
}
}
func (suite *PrefixMatcherUnitSuite) TestLongestPrefix() {
key := "hello"
value := "world"
table := []struct {
name string
inputKVs map[string]string
searchKey string
expectedKey string
expectedValue string
expectedFound assert.BoolAssertionFunc
}{
{
name: "Empty Prefix",
inputKVs: map[string]string{
"": value,
},
searchKey: key,
expectedKey: "",
expectedValue: value,
expectedFound: assert.True,
},
{
name: "Exact Match",
inputKVs: map[string]string{
key: value,
},
searchKey: key,
expectedKey: key,
expectedValue: value,
expectedFound: assert.True,
},
{
name: "Prefix Match",
inputKVs: map[string]string{
key[:len(key)-2]: value,
},
searchKey: key,
expectedKey: key[:len(key)-2],
expectedValue: value,
expectedFound: assert.True,
},
{
name: "Longest Prefix Match",
inputKVs: map[string]string{
key[:len(key)-2]: value,
"": value + "2",
key[:len(key)-4]: value + "3",
},
searchKey: key,
expectedKey: key[:len(key)-2],
expectedValue: value,
expectedFound: assert.True,
},
{
name: "No Match",
inputKVs: map[string]string{
"foo": value,
},
searchKey: key,
expectedKey: "",
expectedValue: "",
expectedFound: assert.False,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
pm := prefixmatcher.NewMatcher[string]()
for k, v := range test.inputKVs {
pm.Add(k, v)
}
k, v, ok := pm.LongestPrefix(test.searchKey)
assert.Equal(t, test.expectedKey, k, "key")
assert.Equal(t, test.expectedValue, v, "value")
test.expectedFound(t, ok, "found")
})
}
}