Initial implementation of path template.
Change-Id: I4445888cd49db279a6a4dc3a956aea11ce47a9a5
diff --git a/path_template.go b/path_template.go
new file mode 100644
index 0000000..f5f9d42
--- /dev/null
+++ b/path_template.go
@@ -0,0 +1,251 @@
+// Copyright 2016, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package gax
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "strings"
+)
+
+var (
+ customVerbRegexp = regexp.MustCompile(":([^:/*}{=]+)$")
+)
+
+type matcher interface {
+ match([]string) (int, error)
+ String() string
+}
+
+type segment struct {
+ matcher
+ name string
+}
+
+type labelMatcher string
+
+func (ls labelMatcher) match(segments []string) (int, error) {
+ if len(segments) == 0 {
+ return 0, fmt.Errorf("expected %s but no more segments found", ls)
+ }
+ if segments[0] != string(ls) {
+ return 0, fmt.Errorf("expected %s but got %s", ls, segments[0])
+ }
+ return 1, nil
+}
+
+func (ls labelMatcher) String() string {
+ return string(ls)
+}
+
+type wildcardMatcher int
+
+func (wm wildcardMatcher) match(segments []string) (int, error) {
+ if len(segments) == 0 {
+ return 0, errors.New("no more segments found")
+ }
+ return 1, nil
+}
+
+func (wm wildcardMatcher) String() string {
+ return "*"
+}
+
+type pathWildcardMatcher int
+
+func (pwm pathWildcardMatcher) match(segments []string) (int, error) {
+ length := len(segments) - int(pwm)
+ if length <= 0 {
+ return 0, errors.New("not sufficient segments are supplied for path wildcard")
+ }
+ return length, nil
+}
+
+func (pwm pathWildcardMatcher) String() string {
+ return "**"
+}
+
+type ParseError struct {
+ Pos int
+ Message string
+}
+
+func (pe ParseError) Error() string {
+ return fmt.Sprintf("at %d, %s", pe.Pos, pe.Message)
+}
+
+func parseSegments(template string) ([]segment, error) {
+ if len(template) == 0 {
+ return nil, ParseError{0, "input is empty"}
+ }
+ var pathWildcardFound bool
+ var segments []segment
+ paths := strings.Split(template, "/")
+ unnamedVariableCount := 0
+ nameSet := map[string]struct{}{}
+ charPos := 0
+ var currentVarName string
+ for i, path := range paths {
+ // Empty path with i == 0 should be allowed for the templates starting with '/'.
+ if path == "" && i != 0 {
+ return nil, ParseError{charPos, "empty path component"}
+ }
+ var matcher matcher
+ name := currentVarName
+ if strings.HasPrefix(path, "{") {
+ equalPos := strings.Index(path, "=")
+ if equalPos > 0 {
+ name = path[1:equalPos]
+ path = path[equalPos+1:]
+ if currentVarName != "" {
+ return nil, ParseError{charPos, "recursive named bindings are not allowed"}
+ }
+ currentVarName = name
+ } else {
+ if path[len(path)-1] != '}' {
+ return nil, ParseError{charPos, "'}' is expected"}
+ }
+ if currentVarName != "" {
+ return nil, ParseError{charPos, "recursive named bindings are not allowed"}
+ }
+ name = path[1 : len(path)-1]
+ path = "*"
+ }
+ if _, ok := nameSet[name]; ok {
+ return nil, ParseError{charPos, fmt.Sprintf("%s appears multiple times", name)}
+ }
+ nameSet[name] = struct{}{}
+ }
+ if strings.HasPrefix(path, "}") {
+ return nil, ParseError{charPos, "} is not allowed here"}
+ }
+ if strings.HasSuffix(path, "}") {
+ path = path[:len(path)-1]
+ currentVarName = ""
+ }
+ if path == "*" {
+ if name == "" {
+ name = fmt.Sprintf("$%d", unnamedVariableCount)
+ unnamedVariableCount++
+ }
+ matcher = wildcardMatcher(0)
+ } else if path == "**" {
+ if pathWildcardFound {
+ return nil, ParseError{charPos, "multiple ** isn't allowed"}
+ }
+ pathWildcardFound = true
+ if name == "" {
+ name = fmt.Sprintf("$%d", unnamedVariableCount)
+ unnamedVariableCount++
+ }
+ matcher = pathWildcardMatcher(len(paths) - i - 1)
+ } else {
+ matcher = labelMatcher(path)
+ }
+ segments = append(segments, segment{matcher, name})
+ charPos += len(path) + 1
+ }
+ return segments, nil
+}
+
+type PathTemplate struct {
+ segments []segment
+ customVerb string
+}
+
+func getCustomVerb(path string) (main string, customVerb string) {
+ matched := customVerbRegexp.FindStringSubmatchIndex(path)
+ if len(matched) == 0 {
+ return path, ""
+ }
+ return path[:matched[0]], path[matched[2]:]
+}
+
+func NewPathTemplate(template string) (*PathTemplate, error) {
+ template, customVerb := getCustomVerb(template)
+ segments, err := parseSegments(template)
+ if err != nil {
+ return nil, err
+ }
+ return &PathTemplate{segments: segments, customVerb: customVerb}, nil
+}
+
+func (pt *PathTemplate) Match(path string) (map[string]string, error) {
+ path, customVerb := getCustomVerb(path)
+ if pt.customVerb != customVerb {
+ return nil, errors.New("custom verb doesn't match")
+ }
+ paths := strings.Split(path, "/")
+ values := map[string]string{}
+ for _, segment := range pt.segments {
+ length, err := segment.match(paths)
+ if err != nil {
+ return nil, err
+ }
+ if segment.name != "" {
+ value := strings.Join(paths[:length], "/")
+ if oldValue, ok := values[segment.name]; ok {
+ values[segment.name] = oldValue + "/" + value
+ } else {
+ values[segment.name] = value
+ }
+ }
+ paths = paths[length:]
+ }
+ if len(paths) != 0 {
+ return nil, fmt.Errorf("Trailing path %s remains after the matching", strings.Join(paths, "/"))
+ }
+ return values, nil
+}
+
+func (pt *PathTemplate) Instantiate(binding map[string]string) (string, error) {
+ result := make([]string, 0, len(pt.segments))
+ var lastVariableName string
+ for _, segment := range pt.segments {
+ name := segment.name
+ if lastVariableName != "" && name == lastVariableName {
+ continue
+ }
+ lastVariableName = name
+ if name == "" {
+ result = append(result, segment.String())
+ } else if value, ok := binding[name]; ok {
+ result = append(result, value)
+ } else {
+ return "", fmt.Errorf("%s is not found", name)
+ }
+ }
+ built := strings.Join(result, "/")
+ if pt.customVerb != "" {
+ built += ":" + pt.customVerb
+ }
+ return built, nil
+}
diff --git a/path_template_test.go b/path_template_test.go
new file mode 100644
index 0000000..ddd929b
--- /dev/null
+++ b/path_template_test.go
@@ -0,0 +1,212 @@
+// Copyright 2016, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package gax
+
+import "testing"
+
+func TestPathTemplateMatchInstantiate(t *testing.T) {
+ testCases := []struct {
+ message string
+ template string
+ path string
+ values map[string]string
+ }{
+ {
+ "base",
+ "buckets/*/*/objects/*",
+ "buckets/f/o/objects/bar",
+ map[string]string{"$0": "f", "$1": "o", "$2": "bar"},
+ },
+ {
+ "custom verb",
+ "buckets/*/objects/*:custom",
+ "buckets/f/objects/o:custom",
+ map[string]string{"$0": "f", "$1": "o"},
+ },
+ {
+ "path wildcards",
+ "bar/**/foo/*",
+ "bar/foo/foo/foo/bar",
+ map[string]string{"$0": "foo/foo", "$1": "bar"},
+ },
+ {
+ "named binding",
+ "buckets/{foo}/objects/*",
+ "buckets/foo/objects/bar",
+ map[string]string{"$0": "bar", "foo": "foo"},
+ },
+ {
+ "named binding with complex patterns",
+ "buckets/{foo=x/*/y/**}/objects/*",
+ "buckets/x/foo/y/bar/baz/objects/quox",
+ map[string]string{"$0": "quox", "foo": "x/foo/y/bar/baz"},
+ },
+ {
+ "starts with slash",
+ "/foo/*",
+ "/foo/bar",
+ map[string]string{"$0": "bar"},
+ },
+ }
+ for _, testCase := range testCases {
+ pt, err := NewPathTemplate(testCase.template)
+ if err != nil {
+ t.Errorf("[%s] Failed to parse template %s: %v", testCase.message, testCase.template, err)
+ continue
+ }
+ values, err := pt.Match(testCase.path)
+ if err != nil {
+ t.Errorf("[%s] PathTemplate '%s' failed to match with '%s', %v", testCase.message, testCase.template, testCase.path, err)
+ continue
+ }
+ for key, expected := range testCase.values {
+ actual, ok := values[key]
+ if !ok {
+ t.Errorf("[%s] The matched data misses the value for %s", testCase.message, key)
+ continue
+ }
+ delete(values, key)
+ if actual != expected {
+ t.Errorf("[%s] Failed to match: value for '%s' is expected '%s' but is actually '%s'", testCase.message, key, expected, actual)
+ }
+ }
+ if len(values) != 0 {
+ t.Errorf("[%s] The matched data has unexpected keys: %v", testCase.message, values)
+ }
+ built, err := pt.Instantiate(testCase.values)
+ if err != nil || built != testCase.path {
+ t.Errorf("[%s] Built path '%s' is different from the expected '%s', %v", testCase.message, built, testCase.path, err)
+ }
+ }
+}
+
+func TestPathTemplateMatchFailure(t *testing.T) {
+ testCases := []struct {
+ message string
+ template string
+ path string
+ }{
+ {
+ "too many paths",
+ "buckets/*/*/objects/*",
+ "buckets/f/o/o/objects/bar",
+ },
+ {
+ "missing last path",
+ "buckets/*/*/objects/*",
+ "buckets/f/o/objects",
+ },
+ {
+ "too many paths at end",
+ "buckets/*/*/objects/*",
+ "buckets/f/o/objects/too/long",
+ },
+ {
+ "wrong custom verb",
+ "buckets/*/objects/*:bar",
+ "buckets/foo/objects/bar:bazz",
+ },
+ }
+ for _, testCase := range testCases {
+ pt, err := NewPathTemplate(testCase.template)
+ if err != nil {
+ t.Errorf("[%s] Failed to parse path %s: %v", testCase.message, testCase.template, err)
+ continue
+ }
+ if values, err := pt.Match(testCase.path); err == nil {
+ t.Errorf("[%s] PathTemplate %s doesn't expect to match %s, but succeeded somehow. Match result: %v", testCase.message, testCase.template, testCase.path, values)
+
+ }
+ }
+}
+
+func TestPathTemplateInstantiateTooManyValues(t *testing.T) {
+ // Test cases where Instantiate() succeeds but Match() doesn't return the same map.
+ testCases := []struct {
+ message string
+ template string
+ values map[string]string
+ expected string
+ }{
+ {
+ "too many",
+ "bar/*/foo/*",
+ map[string]string{"$0": "_1", "$1": "_2", "$2": "_3"},
+ "bar/_1/foo/_2",
+ },
+ }
+ for _, testCase := range testCases {
+ pt, err := NewPathTemplate(testCase.template)
+ if err != nil {
+ t.Errorf("[%s] Failed to parse template %s (error %v)", testCase.message, testCase.template, err)
+ continue
+ }
+ if result, err := pt.Instantiate(testCase.values); err != nil || result != testCase.expected {
+ t.Errorf("[%s] Failed to build the path (expected '%s' but returned '%s'", testCase.message, testCase.expected, result)
+ }
+ }
+}
+
+func TestPathTemplateParseErrors(t *testing.T) {
+ testCases := []struct {
+ message string
+ template string
+ }{
+ {
+ "multiple path wildcards",
+ "foo/**/bar/**",
+ },
+ {
+ "recursive named bindings",
+ "foo/{foo=foo/{bar}/baz/*}/baz/*",
+ },
+ {
+ "complicated multiple path wildcards patterns",
+ "foo/{foo=foo/**/bar/*}/baz/**",
+ },
+ {
+ "consective slashes",
+ "foo//bar",
+ },
+ {
+ "invalid variable pattern",
+ "foo/{foo=foo/*/}bar",
+ },
+ {
+ "same name multiple times",
+ "foo/{foo}/bar/{foo}",
+ },
+ }
+ for _, testCase := range testCases {
+ if pt, err := NewPathTemplate(testCase.template); err == nil {
+ t.Errorf("[%s] Template '%s' should fail to be parsed, but succeeded and returned %+v", testCase.message, testCase.template, pt)
+ }
+ }
+}