From 6a746b518f0bbd6520543d5e9f3638e3a4d7d14d Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Wed, 7 Feb 2024 16:33:44 +0300 Subject: [PATCH] add pattern matching --- cmd/cli/main.go | 53 +++++++++-- internal/matcher/radix.go | 164 +++++++++++++++++++++++++++++++++ internal/matcher/radix_test.go | 118 ++++++++++++++++++++++++ 3 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 internal/matcher/radix.go create mode 100644 internal/matcher/radix_test.go diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 40ff044..5bc823a 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -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 } } diff --git a/internal/matcher/radix.go b/internal/matcher/radix.go new file mode 100644 index 0000000..c88ac7b --- /dev/null +++ b/internal/matcher/radix.go @@ -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 +} diff --git a/internal/matcher/radix_test.go b/internal/matcher/radix_test.go new file mode 100644 index 0000000..94f9c4b --- /dev/null +++ b/internal/matcher/radix_test.go @@ -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) + } + }) + } +}