From 86ad409fda34aeba04f5230939efdb3b9b5ae7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Jir=C3=A9nius?= Date: Tue, 30 Apr 2019 11:00:17 +0200 Subject: [PATCH] GH-26: Added group tags. --- group.go | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++ group_test.go | 86 +++++++++++++++++++++++++++++++++++ patterns.go | 30 ++++++++----- service.go | 21 +++++++-- 4 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 group.go create mode 100644 group_test.go diff --git a/group.go b/group.go new file mode 100644 index 0000000..746f885 --- /dev/null +++ b/group.go @@ -0,0 +1,121 @@ +package res + +import ( + "fmt" + "strings" +) + +type group []gpart + +type gpart struct { + str string + idx int +} + +// parseGroup takes a group name and parses it for ${tag} sequences, +// verifying the tags exists as parameter tags in the pattern as well. +// Panics if an error is encountered. +func parseGroup(g, pattern string) group { + if g == "" { + return nil + } + + tokens := splitPattern(pattern) + + var gr group + var c byte + l := len(g) + i := 0 + start := 0 + +StateDefault: + if i == l { + if i > start { + gr = append(gr, gpart{str: g[start:i]}) + } + return gr + } + if g[i] == '$' { + if i > start { + gr = append(gr, gpart{str: g[start:i]}) + } + i++ + if i == l { + goto UnexpectedEnd + } + if g[i] != '{' { + panic(fmt.Sprintf("expected character \"{\" at pos %d", i)) + } + i++ + start = i + goto StateTag + } + i++ + goto StateDefault + +StateTag: + if i == l { + goto UnexpectedEnd + } + c = g[i] + if c == '}' { + if i == start { + panic(fmt.Sprintf("empty group tag at pos %d", i)) + } + tag := "$" + g[start:i] + for j, t := range tokens { + if t == tag { + gr = append(gr, gpart{idx: j}) + goto TokenFound + } + } + panic(fmt.Sprintf("group tag %s not found in pattern", tag)) + TokenFound: + i++ + start = i + goto StateDefault + } + if (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_' && c != '-' { + panic(fmt.Sprintf("non alpha-numeric (a-z0-9_-) character in group tag at pos %d", i)) + } + i++ + goto StateTag + +UnexpectedEnd: + panic("unexpected end of group tag") +} + +func (g group) toString(rname string) string { + l := len(g) + if l == 0 { + return rname + } + + if l == 1 && g[0].str != "" { + return g[0].str + } + + var tokens []string + if len(rname) > 0 { + tokens = make([]string, 0, 32) + start := 0 + for i := 0; i < len(rname); i++ { + if rname[i] == btsep { + tokens = append(tokens, rname[start:i]) + start = i + 1 + } + } + tokens = append(tokens, rname[start:]) + } + + var b strings.Builder + for _, gp := range g { + if gp.str == "" { + b.WriteString(tokens[gp.idx]) + } else { + b.WriteString(gp.str) + } + } + + return b.String() +} diff --git a/group_test.go b/group_test.go new file mode 100644 index 0000000..4b510f1 --- /dev/null +++ b/group_test.go @@ -0,0 +1,86 @@ +package res + +import ( + "testing" +) + +// Test parseGroup panics when expected +func TestParseGroup(t *testing.T) { + tbl := []struct { + Group string + Pattern string + Panic bool + }{ + // Valid groups + {"", "test", false}, + {"test", "test", false}, + {"test", "test.$foo", false}, + {"test.${foo}", "test.$foo", false}, + {"${foo}", "test.$foo", false}, + {"${foo}.test", "test.$foo", false}, + {"${foo}${bar}", "test.$foo.$bar", false}, + {"${bar}${foo}", "test.$foo.$bar", false}, + {"${foo}.${bar}", "test.$foo.$bar.>", false}, + {"${foo}${foo}", "test.$foo.$bar", false}, + + // Invalid groups + {"$", "test.$foo", true}, + {"${", "test.$foo", true}, + {"${foo", "test.$foo", true}, + {"${}", "test.$foo", true}, + {"${$foo}", "test.$foo", true}, + {"${bar}", "test.$foo", true}, + } + + for _, l := range tbl { + func() { + defer func() { + if r := recover(); r != nil { + if !l.Panic { + t.Errorf("expected parseGroup not to panic, but it did:\n\tpanic : %s\n\tgroup : %s\n\tpattern : %s", r, l.Group, l.Pattern) + } + } else { + if l.Panic { + t.Errorf("expected parseGroup to panic, but it didn't\n\tgroup : %s\n\tpattern : %s", l.Group, l.Pattern) + } + } + }() + + parseGroup(l.Group, l.Pattern) + }() + } +} + +// Test group toString +func TestGroupToString(t *testing.T) { + tbl := []struct { + Group string + Pattern string + RName string + Expected string + }{ + {"", "test", "test", "test"}, + {"test", "test", "test", "test"}, + {"foo", "test", "test", "foo"}, + {"test", "test.$foo", "test.42", "test"}, + {"test.${foo}", "test.$foo", "test.42", "test.42"}, + {"bar.${foo}", "test.$foo", "test.42", "bar.42"}, + {"${foo}", "test.$foo", "test.42", "42"}, + {"${foo}.test", "test.$foo", "test.42", "42.test"}, + {"${foo}${bar}", "test.$foo.$bar", "test.42.baz", "42baz"}, + {"${bar}${foo}", "test.$foo.$bar", "test.42.baz", "baz42"}, + {"${foo}.${bar}", "test.$foo.$bar.>", "test.42.baz.extra.all", "42.baz"}, + {"${foo}${foo}", "test.$foo.$bar", "test.42.baz", "4242"}, + {"${foo}.test.this.${bar}", "test.$foo.$bar", "test.42.baz", "42.test.this.baz"}, + } + + for _, l := range tbl { + func() { + gr := parseGroup(l.Group, l.Pattern) + wid := gr.toString(l.RName) + if wid != l.Expected { + t.Errorf("expected parseGroup(%#v, %#v).toString(%#v) to return:\n\t%#v\nbut got:\n\t%#v", l.Group, l.Pattern, l.RName, l.Expected, wid) + } + }() + } +} diff --git a/patterns.go b/patterns.go index 1bb1908..e7fcd3a 100644 --- a/patterns.go +++ b/patterns.go @@ -43,18 +43,8 @@ type nodeMatch struct { // add inserts new handlers to the pattern store. // An invalid pattern, or a pattern already registered will cause panic. func (ls *patterns) add(pattern string, hs *regHandler) { - var tokens []string - if len(pattern) > 0 { - tokens = make([]string, 0, 32) - start := 0 - for i := 0; i < len(pattern); i++ { - if pattern[i] == btsep { - tokens = append(tokens, pattern[start:i]) - start = i + 1 - } - } - tokens = append(tokens, pattern[start:]) - } + tokens := splitPattern(pattern) + var params []pathParam l := ls.root @@ -192,3 +182,19 @@ func matchNode(l *node, toks []string, i int, m *nodeMatch) bool { return false } + +func splitPattern(p string) []string { + if len(p) == 0 { + return nil + } + tokens := make([]string, 0, 32) + start := 0 + for i := 0; i < len(p); i++ { + if p[i] == btsep { + tokens = append(tokens, p[start:i]) + start = i + 1 + } + } + tokens = append(tokens, p[start:]) + return tokens +} diff --git a/service.go b/service.go index 6ed2575..6ce72e3 100644 --- a/service.go +++ b/service.go @@ -77,13 +77,16 @@ type Handler struct { // Group is the identifier of the group the resource belongs to. // All resources of the same group will be handled on the same // goroutine. + // The group may contain tags, ${tagName}, where the tag name matches + // a parameter placeholder name in the resource pattern. // If empty, the resource name will be used as identifier. Group string } type regHandler struct { Handler - typ rtype + group group + typ rtype } const ( @@ -206,8 +209,10 @@ func (s *Service) AddHandler(pattern string, hs Handler) { if hs.Access != nil { s.withAccess = true } + h := regHandler{ Handler: hs, + group: parseGroup(hs.Group, pattern), typ: validateGetHandlers(hs), } s.patterns.add(s.Name+"."+pattern, &h) @@ -295,6 +300,16 @@ func Auth(method string, h AuthHandler) HandlerOption { } } +// Group sets a group ID. All resources of the same group will be handled +// on the same goroutine. +// The group may contain tags, ${tagName}, where the tag name matches +// a parameter placeholder name in the resource pattern. +func Group(group string) HandlerOption { + return func(hs *Handler) { + hs.Group = group + } +} + // SetReset sets the patterns used for resources and access when a reset is made.ยจ // For more details on system reset, see: // https://github.com/jirenius/resgate/blob/master/docs/res-service-protocol.md#system-reset-event @@ -558,8 +573,8 @@ func (s *Service) runWith(hs *regHandler, rname string, cb func()) { } wid := rname - if hs != nil && hs.Group != "" { - wid = hs.Group + if hs != nil { + wid = hs.group.toString(rname) } s.mu.Lock()