add pattern matching

This commit is contained in:
Aleksandr Trushkin
2024-02-07 16:33:44 +03:00
parent 2f4a973b45
commit b8ce09f7ec
3 changed files with 327 additions and 8 deletions

View File

@ -26,6 +26,7 @@ import (
"git.loyso.art/frx/eway/internal/entity"
"git.loyso.art/frx/eway/internal/export"
"git.loyso.art/frx/eway/internal/interconnect/eway"
"git.loyso.art/frx/eway/internal/matcher"
"github.com/rodaine/table"
"github.com/rs/zerolog"
@ -387,8 +388,18 @@ func newViewItemsUniqueParams() *cli.Command {
func newViewItemsParamsKnownValues() *cli.Command {
return &cli.Command{
Name: "params-values",
Usage: "Show all values of requested parameters",
Name: "params-values",
Usage: "Show all values of requested parameters",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "case-insensitive",
Usage: "Ignores cases of keys",
},
&cli.StringSliceFlag{
Name: "regex",
Usage: "Registers regex to match",
},
},
Action: decorateAction(viewItemsParamsKnownValuesAction),
}
}
@ -558,19 +569,45 @@ func viewItemsParamsKnownValuesAction(ctx context.Context, c *cli.Command) error
}
params := c.Args().Slice()
requestedValues := make(map[string]map[string]struct{}, len(params))
for _, param := range params {
log.Debug().Str("param", param).Msg("registering param")
requestedValues[param] = make(map[string]struct{}, 16)
opts := make([]matcher.RadixOpt, 0, 1)
if c.Bool("case-insensitive") {
opts = append(opts, matcher.RadixCaseInsensitive())
}
m := matcher.NewRadix(opts...)
for _, param := range params {
log.Debug().Str("param", param).Msg("registering param")
m.Register(param)
}
for _, regexp := range c.StringSlice("regex") {
log.Debug().Str("regexp", regexp).Msg("registering regexp")
m.RegisterRegexp(regexp)
}
requestedValues := make(map[string]map[string]struct{}, len(params))
requestedValuesByPattern := make(map[string]map[string]struct{}, len(params))
iter := getItemsIter(ctx, repository.GoodsItem())
for iter.Next() {
item := iter.Get()
for k, v := range item.Parameters {
if _, ok := requestedValues[k]; ok {
requestedValues[k][v] = struct{}{}
matchedPattern := m.MatchByPattern(k)
if matchedPattern == "" {
continue
}
values, ok := requestedValues[k]
if !ok {
values = make(map[string]struct{})
}
values[v] = struct{}{}
requestedValues[k] = values
values, ok = requestedValuesByPattern[matchedPattern]
if !ok {
values = map[string]struct{}{}
}
values[v] = struct{}{}
requestedValuesByPattern[matchedPattern] = values
}
}

164
internal/matcher/radix.go Normal file
View File

@ -0,0 +1,164 @@
package matcher
import (
"regexp"
"strings"
)
const radixWildcard = '*'
type radixNode struct {
exact bool
next map[rune]*radixNode
}
type radixMatcher struct {
root *radixNode
caseInsensitive bool
saved []string
regexps []*regexp.Regexp
}
type RadixOpt func(m *radixMatcher)
func RadixCaseInsensitive() RadixOpt {
return func(m *radixMatcher) {
m.caseInsensitive = true
}
}
func NewRadix(opts ...RadixOpt) *radixMatcher {
m := &radixMatcher{
root: &radixNode{
next: make(map[rune]*radixNode),
},
}
for _, opt := range opts {
opt(m)
}
return m
}
func (r *radixMatcher) MatchByPattern(value string) (pattern string) {
originValue := value
if r.caseInsensitive {
value = strings.ToLower(value)
}
node := r.root
lastIdx := len([]rune(value)) - 1
var sb strings.Builder
for i, v := range value {
var ok bool
if _, ok := node.next[radixWildcard]; ok {
_, _ = sb.WriteRune(radixWildcard)
return sb.String()
}
node, ok = node.next[v]
if !ok {
return r.findByRegexp(originValue)
}
sb.WriteRune(v)
if i != lastIdx {
continue
}
if !node.exact {
return r.findByRegexp(value)
}
return sb.String()
}
return r.findByRegexp(originValue)
}
func (r *radixMatcher) Match(value string) bool {
originValue := value
if r.caseInsensitive {
value = strings.ToLower(value)
}
node := r.root
lastIdx := len([]rune(value)) - 1
for i, v := range value {
var ok bool
if _, ok = node.next[radixWildcard]; ok {
return true
}
node, ok = node.next[v]
if !ok {
return r.findByRegexp(originValue) != ""
}
if i == lastIdx {
return node.exact
}
}
return r.findByRegexp(originValue) != ""
}
func (r *radixMatcher) findByRegexp(value string) (regexpPattern string) {
for _, rx := range r.regexps {
if rx.MatchString(value) {
return rx.String()
}
}
return ""
}
func (r *radixMatcher) RegisterRegexp(regexpPattern string) {
pattern := regexp.MustCompile(regexpPattern)
r.regexps = append(r.regexps, pattern)
}
func (r *radixMatcher) Register(pattern string) {
if len(pattern) == 0 {
panic("unable to handle empty pattern")
}
if r.caseInsensitive {
pattern = strings.ToLower(pattern)
}
r.saved = append(r.saved, pattern)
node := r.root
lastIdx := len([]rune(pattern)) - 1
for i, v := range pattern {
nextNode, ok := node.next[v]
if !ok {
nextNode = &radixNode{
next: make(map[rune]*radixNode),
}
node.next[v] = nextNode
}
node = nextNode
if v == '*' {
return
}
if i != lastIdx {
continue
}
node.exact = true
}
}
func (r *radixMatcher) Patterns() []string {
out := make([]string, len(r.saved))
copy(out, r.saved)
return out
}

View File

@ -0,0 +1,118 @@
package matcher_test
import (
"testing"
"git.loyso.art/frx/eway/internal/matcher"
)
func TestRadixMatcherWithPattern(t *testing.T) {
m := matcher.NewRadix()
m.Register("aloha")
m.Register("hawaii")
m.Register("te*")
var tt = []struct {
name string
in string
pattern string
}{{
name: "should match exact",
in: "aloha",
pattern: "aloha",
}, {
name: "should match exact 2",
in: "hawaii",
pattern: "hawaii",
}, {
name: "should match pattern",
in: "test",
pattern: "te*",
}, {
name: "should not match",
in: "alohe",
}, {
name: "should not match 2",
in: "whoa",
}}
for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
pattern := m.MatchByPattern(tc.in)
if pattern != tc.pattern {
t.Errorf("expected %s got %s", tc.pattern, pattern)
}
})
}
}
func TestRadixMatcher(t *testing.T) {
m := matcher.NewRadix()
m.Register("aloha")
m.Register("hawaii")
m.Register("te*")
var tt = []struct {
name string
in string
match bool
}{{
name: "should match exact",
in: "aloha",
match: true,
}, {
name: "should match exact 2",
in: "hawaii",
match: true,
}, {
name: "should match pattern",
in: "test",
match: true,
}, {
name: "should not match",
in: "alohe",
}, {
name: "should not match 2",
in: "whoa",
}}
for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
match := m.Match(tc.in)
if match != tc.match {
t.Errorf("expected %t got %t", tc.match, match)
}
})
}
}
func TestRadixMatcherWildcard(t *testing.T) {
m := matcher.NewRadix()
m.Register("*")
var tt = []struct {
name string
in string
match bool
}{{
name: "should match exact",
in: "aloha",
match: true,
}, {
name: "should match exact 2",
in: "hawaii",
match: true,
}}
for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
match := m.Match(tc.in)
if match != tc.match {
t.Errorf("expected %t got %t", tc.match, match)
}
})
}
}