From a75a0691f9bcb88ab0d8db18fbde55d91f1636df Mon Sep 17 00:00:00 2001 From: marrow16 Date: Sat, 1 Jul 2023 17:22:30 +0100 Subject: [PATCH] Get vars Added `Vars()` method - to obtain path vars from template --- path_part.go | 22 +++++++ path_vars.go | 15 +++++ path_vars_test.go | 36 +++++++++++ template.go | 42 +++++++++++-- template_test.go | 151 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 260 insertions(+), 6 deletions(-) diff --git a/path_part.go b/path_part.go index 077e856..efd3505 100644 --- a/path_part.go +++ b/path_part.go @@ -149,6 +149,28 @@ func (pt *pathPart) pathFrom(tracker *positionsTracker) (string, error) { return `/` + pb.String(), nil } +func (pt *pathPart) getVars(vars []PathVar, namePosns map[string]int) []PathVar { + if !pt.fixed && len(pt.subParts) > 0 { + for _, sp := range pt.subParts { + vars = sp.getVars(vars, namePosns) + } + } else if !pt.fixed { + if pt.name == "" { + vars = append(vars, PathVar{ + Position: len(vars), + }) + } else { + vars = append(vars, PathVar{ + Name: pt.name, + NamedPosition: namePosns[pt.name], + Position: len(vars), + }) + namePosns[pt.name] = namePosns[pt.name] + 1 + } + } + return vars +} + type positionsTracker struct { vars PathVars varPosition int diff --git a/path_vars.go b/path_vars.go index 7d331cd..17681e1 100644 --- a/path_vars.go +++ b/path_vars.go @@ -41,6 +41,21 @@ func newPathVars(varsType PathVarsType) PathVars { } } +func PathVarsFromMap(m map[string]interface{}) PathVars { + result := newPathVars(Names) + for k, v := range m { + switch av := v.(type) { + case []interface{}: + for _, sv := range av { + _ = result.AddNamedValue(k, sv) + } + default: + _ = result.AddNamedValue(k, av) + } + } + return result +} + func (pvs *pathVars) GetPositional(position int) (string, bool) { if position < 0 && (len(pvs.all)+position) >= 0 { return getValueIf(pvs.all[len(pvs.all)+position].Value) diff --git a/path_vars_test.go b/path_vars_test.go index 0c1114a..cd48a19 100644 --- a/path_vars_test.go +++ b/path_vars_test.go @@ -212,3 +212,39 @@ func TestPathVars_Get_Named(t *testing.T) { require.True(t, ok) require.Equal(t, "c", v) } + +func TestPathVarsFromMap(t *testing.T) { + m := map[string]interface{}{ + "foo": "a", + "bar": "b", + } + args := PathVarsFromMap(m) + require.Equal(t, 2, args.Len()) + + v, ok := args.GetNamed("foo", 0) + require.True(t, ok) + require.Equal(t, "a", v) + v, ok = args.GetNamed("bar", 0) + require.True(t, ok) + require.Equal(t, "b", v) + + m = map[string]interface{}{ + "foo": "a", + "bar": []interface{}{"b", "c", "d"}, + } + args = PathVarsFromMap(m) + require.Equal(t, 4, args.Len()) + + v, ok = args.GetNamed("foo", 0) + require.True(t, ok) + require.Equal(t, "a", v) + v, ok = args.GetNamed("bar", 0) + require.True(t, ok) + require.Equal(t, "b", v) + v, ok = args.GetNamed("bar", 1) + require.True(t, ok) + require.Equal(t, "c", v) + v, ok = args.GetNamed("bar", 2) + require.True(t, ok) + require.Equal(t, "d", v) +} diff --git a/template.go b/template.go index a2c9690..478d7c7 100644 --- a/template.go +++ b/template.go @@ -5,6 +5,7 @@ import ( "github.com/go-andiamo/splitter" "io" "net/http" + "net/url" "strings" ) @@ -51,6 +52,9 @@ type Template interface { // Matches checks whether the specified path matches the template - // and if a successful match, returns the extracted path vars Matches(path string, options ...interface{}) (PathVars, bool) + // MatchesUrl checks whether the specified URL path matches the template - + // and if successful match, returns the extracted path vars + MatchesUrl(u url.URL, options ...interface{}) (PathVars, bool) // MatchesRequest checks whether the specified request matches the template - // and if a successful match, returns the extracted path vars MatchesRequest(req *http.Request, options ...interface{}) (PathVars, bool) @@ -60,6 +64,8 @@ type Template interface { ResolveTo(vars PathVars) (Template, error) // VarsType returns the path vars type (Positions or Names) VarsType() PathVarsType + // Vars returns the path vars of the template + Vars() []PathVar // OriginalTemplate returns the original (or generated) path template string OriginalTemplate() string } @@ -137,6 +143,26 @@ func (t *template) RequestFrom(method string, vars PathVars, body io.Reader, opt // Matches checks whether the specified path matches the template - // and if a successful match, returns the extracted path vars func (t *template) Matches(path string, options ...interface{}) (PathVars, bool) { + u, err := url.Parse(path) + if err != nil { + return nil, false + } + return t.matches(u.Path, options...) +} + +// MatchesUrl checks whether the specified URL path matches the template - +// and if successful match, returns the extracted path vars +func (t *template) MatchesUrl(u url.URL, options ...interface{}) (PathVars, bool) { + return t.matches(u.Path, options...) +} + +// MatchesRequest checks whether the specified request matches the template - +// and if a successful match, returns the extracted path vars +func (t *template) MatchesRequest(req *http.Request, options ...interface{}) (PathVars, bool) { + return t.matches(req.URL.Path, options...) +} + +func (t *template) matches(path string, options ...interface{}) (PathVars, bool) { pts, err := matchPathSplitter.Split(path) if err != nil || len(pts) != len(t.pathParts) { return nil, false @@ -153,12 +179,6 @@ func (t *template) Matches(path string, options ...interface{}) (PathVars, bool) return result, ok } -// MatchesRequest checks whether the specified request matches the template - -// and if a successful match, returns the extracted path vars -func (t *template) MatchesRequest(req *http.Request, options ...interface{}) (PathVars, bool) { - return t.Matches(req.URL.Path, options...) -} - // Sub generates a new template with added sub-path func (t *template) Sub(path string, options ...interface{}) (Template, error) { add, err := NewTemplate(path, options...) @@ -281,6 +301,16 @@ func (t *template) VarsType() PathVarsType { return Names } +// Vars returns the path vars of the template +func (t *template) Vars() []PathVar { + result := make([]PathVar, 0, len(t.pathParts)) + namePosns := map[string]int{} + for _, p := range t.pathParts { + result = p.getVars(result, namePosns) + } + return result +} + // OriginalTemplate returns the original (or generated) path template string func (t *template) OriginalTemplate() string { return t.originalTemplate diff --git a/template_test.go b/template_test.go index 9a518fb..6fde247 100644 --- a/template_test.go +++ b/template_test.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/stretchr/testify/require" "net/http" + "net/url" "regexp" "testing" ) @@ -39,6 +40,79 @@ func TestMatchString(t *testing.T) { require.Equal(t, "bbb", v) } +func TestMatchWithUrlString(t *testing.T) { + tmp, err := NewTemplate("/foo/{foo}/bar/{bar}/{bar}") + require.NoError(t, err) + require.NotNil(t, tmp) + + _, ok := tmp.Matches("https://my.org/foo") + require.False(t, ok) + + args, ok := tmp.Matches("https://my.org/foo/fooey/bar/aaa/bbb") + require.True(t, ok) + v, ok := args.GetPositional(0) + require.True(t, ok) + require.Equal(t, "fooey", v) + v, ok = args.GetPositional(1) + require.True(t, ok) + require.Equal(t, "aaa", v) + v, ok = args.GetPositional(2) + require.True(t, ok) + require.Equal(t, "bbb", v) + _, ok = args.GetPositional(3) + require.False(t, ok) + v, ok = args.GetNamedFirst("foo") + require.True(t, ok) + require.Equal(t, "fooey", v) + v, ok = args.GetNamedFirst("bar") + require.True(t, ok) + require.Equal(t, "aaa", v) + v, ok = args.GetNamedLast("bar") + require.True(t, ok) + require.Equal(t, "bbb", v) + + // bad url... + const badUrl = "://my.org" + _, ok = tmp.Matches(badUrl) + require.False(t, ok) + _, err = url.Parse(badUrl) + require.Error(t, err) +} + +func TestMatchWithUrl(t *testing.T) { + tmp, err := NewTemplate("/foo/{foo}/bar/{bar}/{bar}") + require.NoError(t, err) + require.NotNil(t, tmp) + + u, _ := url.Parse("https://my.org/foo") + _, ok := tmp.MatchesUrl(*u) + require.False(t, ok) + + u, _ = url.Parse("https://my.org/foo/fooey/bar/aaa/bbb") + args, ok := tmp.MatchesUrl(*u) + require.True(t, ok) + v, ok := args.GetPositional(0) + require.True(t, ok) + require.Equal(t, "fooey", v) + v, ok = args.GetPositional(1) + require.True(t, ok) + require.Equal(t, "aaa", v) + v, ok = args.GetPositional(2) + require.True(t, ok) + require.Equal(t, "bbb", v) + _, ok = args.GetPositional(3) + require.False(t, ok) + v, ok = args.GetNamedFirst("foo") + require.True(t, ok) + require.Equal(t, "fooey", v) + v, ok = args.GetNamedFirst("bar") + require.True(t, ok) + require.Equal(t, "aaa", v) + v, ok = args.GetNamedLast("bar") + require.True(t, ok) + require.Equal(t, "bbb", v) +} + func TestMatchPositional(t *testing.T) { tmp, err := NewTemplate("/foo/?/bar/?/?") require.NoError(t, err) @@ -456,6 +530,83 @@ func TestTemplate_MergeOptions(t *testing.T) { } } +func TestTemplate_Vars(t *testing.T) { + testCases := []struct { + path string + expect []PathVar + }{ + { + path: "/foo/{fooid}", + expect: []PathVar{ + { + Name: "fooid", + NamedPosition: 0, + Position: 0, + }, + }, + }, + { + path: "/foo/{fooid}/foo/{fooid: [a-z]*}", + expect: []PathVar{ + { + Name: "fooid", + NamedPosition: 0, + Position: 0, + }, + { + Name: "fooid", + NamedPosition: 1, + Position: 1, + }, + }, + }, + { + path: "/foo/{foo1}-{foo2}/bar/{bar}-{bar}", + expect: []PathVar{ + { + Name: "foo1", + Position: 0, + }, + { + Name: "foo2", + Position: 1, + }, + { + Name: "bar", + Position: 2, + }, + { + Name: "bar", + NamedPosition: 1, + Position: 3, + }, + }, + }, + { + path: "foo/?/bar/?", + expect: []PathVar{ + { + Position: 0, + }, + { + Position: 1, + }, + }, + }, + } + for i, tc := range testCases { + t.Run(fmt.Sprintf("[%d]", i+1), func(t *testing.T) { + tmp, err := NewTemplate(tc.path) + require.NoError(t, err) + vars := tmp.Vars() + require.Equal(t, len(tc.expect), len(vars)) + for vi, v := range vars { + require.Equal(t, tc.expect[vi], v) + } + }) + } +} + type uuidChecker struct { }