diff options
| author | Shulhan <m.shulhan@gmail.com> | 2019-09-11 23:31:39 +0700 |
|---|---|---|
| committer | Shulhan <m.shulhan@gmail.com> | 2019-09-11 23:49:34 +0700 |
| commit | d8368c713d49ef0606a2c59baec0e55f0c61baea (patch) | |
| tree | ba8e90b36fcc627228ae19c4e99f270a03ad7e75 | |
| parent | c49ffa3d1b2ffe9183b4d6775624b32fe216c7d4 (diff) | |
| download | pakakeh.go-d8368c713d49ef0606a2c59baec0e55f0c61baea.tar.xz | |
http: add type node and route to handle Endpoint with key
Most of HTTP server allow binding key in the routing, which in turn
allow pretty REST API in URL. For example, to delete a resource with
unique ID, we can register an Endpoint using HTTP method DELETE with Path
set to "/resource/:id". The ":id" is the key. Its value will be
available in the http.Request's Form field using request.Form.Get("id").
| -rw-r--r-- | lib/http/node.go | 13 | ||||
| -rw-r--r-- | lib/http/route.go | 128 | ||||
| -rw-r--r-- | lib/http/route_test.go | 249 |
3 files changed, 390 insertions, 0 deletions
diff --git a/lib/http/node.go b/lib/http/node.go new file mode 100644 index 00000000..0f31debe --- /dev/null +++ b/lib/http/node.go @@ -0,0 +1,13 @@ +package http + +// +// node represent sub-path as key or as raw path. +// The original path is splitted by "/" and each splitted string will be +// stored as node. A sub-path that start with colon ":" is a key; otherwise +// its normal sub-path. +// +type node struct { + key string + name string + isKey bool +} diff --git a/lib/http/route.go b/lib/http/route.go new file mode 100644 index 00000000..0946830e --- /dev/null +++ b/lib/http/route.go @@ -0,0 +1,128 @@ +package http + +import ( + "strings" +) + +// +// route represent the route to endpoint. +// +type route struct { + path string // path contains Endpoint's path that has been cleaned up. + nodes []*node // nodes contains sub-path. + nkey int // nkey contains the number of keys in nodes. + endpoint *Endpoint // endpoint of route. +} + +// +// newRoute parse the Endpoint's path, store the key(s) in path if available +// in nodes. +// +// The key is sub-path that start with colon ":". +// For example, the following path "/:user/:repo" contains two nodes with both +// are keys. +// If path is invalid, for example, "/:user/:" or "/:user/:user" (key with +// duplicate names), it will return nil. +// +func newRoute(ep *Endpoint) (rute *route, err error) { + rute = &route{ + endpoint: ep, + } + + paths := strings.Split(strings.ToLower(strings.Trim(ep.Path, "/")), "/") + + for _, path := range paths { + path = strings.TrimSpace(path) + if len(path) == 0 { + continue + } + + nod := &node{} + + if path[0] == ':' { + nod.key = strings.TrimSpace(path[1:]) + if len(nod.key) == 0 { + return nil, ErrEndpointKeyEmpty + } + + if rute.isKeyExist(nod.key) { + return nil, ErrEndpointKeyDuplicate + } + + nod.isKey = true + rute.nkey++ + } else { + nod.name = path + } + + rute.nodes = append(rute.nodes, nod) + } + if len(rute.nodes) == 0 { + rute.nodes = append(rute.nodes, &node{}) + } + + rute.path = rute.generatePath() + + return rute, nil +} + +// +// isKeyExist will return true if the key already exist in nodes; otherwise it +// will return false. +// +func (rute *route) isKeyExist(key string) bool { + for _, node := range rute.nodes { + if !node.isKey { + continue + } + if node.key == key { + return true + } + } + return false +} + +// +// parse the path and return the key-value association and true if path is +// matched with current route; otherwise it will return nil and false. +// +func (rute *route) parse(path string) (vals map[string]string, ok bool) { + if rute.nkey == 0 { + if path == rute.path { + return nil, true + } + } + + paths := strings.Split(strings.ToLower(strings.Trim(path, "/")), "/") + + if len(paths) != len(rute.nodes) { + return nil, false + } + + vals = make(map[string]string, rute.nkey) + for x, node := range rute.nodes { + if node.isKey { + vals[node.key] = paths[x] + } else if paths[x] != node.name { + return nil, false + } + } + + return vals, true +} + +// +// generatePath generate a clean path without any white spaces and single "/" +// between sub-path. +// +func (rute *route) generatePath() (path string) { + for _, node := range rute.nodes { + path += "/" + if node.isKey { + path += ":" + node.key + } else { + path += node.name + } + } + return path +} diff --git a/lib/http/route_test.go b/lib/http/route_test.go new file mode 100644 index 00000000..83cfc491 --- /dev/null +++ b/lib/http/route_test.go @@ -0,0 +1,249 @@ +package http + +import ( + "testing" + + "github.com/shuLhan/share/lib/test" +) + +func TestNewRoute(t *testing.T) { + cases := []struct { + desc string + ep *Endpoint + exp *route + expError string + }{{ + desc: "With empty path", + ep: &Endpoint{ + Path: "", + }, + exp: &route{ + path: "/", + nodes: []*node{{}}, + endpoint: &Endpoint{ + Path: "", + }, + }, + }, { + desc: "With root path", + ep: &Endpoint{ + Path: "/", + }, + exp: &route{ + path: "/", + nodes: []*node{{}}, + endpoint: &Endpoint{ + Path: "/", + }, + }, + }, { + desc: "With empty key", + ep: &Endpoint{ + Path: "/:user /:", + }, + expError: ErrEndpointKeyEmpty.Error(), + }, { + desc: "With duplicate keys", + ep: &Endpoint{ + Path: "/:user/a/b/:user/c", + }, + expError: ErrEndpointKeyDuplicate.Error(), + }, { + desc: "With valid keys", + ep: &Endpoint{ + Path: "/: user/ :repo ", + }, + exp: &route{ + path: "/:user/:repo", + nodes: []*node{{ + key: "user", + isKey: true, + }, { + key: "repo", + isKey: true, + }}, + nkey: 2, + endpoint: &Endpoint{ + Path: "/: user/ :repo ", + }, + }, + }, { + desc: "With double slash on path", + ep: &Endpoint{ + Path: "/user//repo", + }, + exp: &route{ + path: "/user/repo", + nodes: []*node{{ + name: "user", + }, { + name: "repo", + }}, + endpoint: &Endpoint{ + Path: "/user//repo", + }, + }, + }, { + desc: "Without key", + ep: &Endpoint{ + Path: "/user/repo", + }, + exp: &route{ + path: "/user/repo", + nodes: []*node{{ + name: "user", + }, { + name: "repo", + }}, + endpoint: &Endpoint{ + Path: "/user/repo", + }, + }, + }} + + for _, c := range cases { + t.Log(c.desc) + + got, err := newRoute(c.ep) + if err != nil { + test.Assert(t, "error", c.expError, err.Error(), true) + continue + } + + test.Assert(t, "newRoute", c.exp, got, true) + } +} + +func TestRoute_parse(t *testing.T) { + type testPath struct { + path string + expOK bool + expVals map[string]string + } + + cases := []struct { + desc string + ep *Endpoint + paths []testPath + }{{ + desc: "With empty path", + ep: &Endpoint{ + Path: "", + }, + paths: []testPath{{ + path: "/", + expOK: true, + }, { + path: "/a", + }}, + }, { + desc: "With single key at the beginning", + ep: &Endpoint{ + Path: "/:user/repo", + }, + paths: []testPath{{ + path: "/", + }, { + path: "/me", + }, { + path: "/me/repo", + expOK: true, + expVals: map[string]string{ + "user": "me", + }, + }, { + path: "/me/repo/", + expOK: true, + expVals: map[string]string{ + "user": "me", + }, + }}, + }, { + desc: "With single key at the middle", + ep: &Endpoint{ + Path: "/your/:user/repo", + }, + paths: []testPath{{ + path: "/", + }, { + path: "/your", + }, { + path: "/your/name", + }, { + path: "/your/name/repo", + expOK: true, + expVals: map[string]string{ + "user": "name", + }, + }, { + path: "/your/name/repo/", + expOK: true, + expVals: map[string]string{ + "user": "name", + }, + }, { + path: "/your/name/repo/here", + }}, + }, { + desc: "With single key at the end", + ep: &Endpoint{ + Path: "/your/user/:repo", + }, + paths: []testPath{{ + path: "/", + }, { + path: "/your", + }, { + path: "/your/user", + }, { + path: "/your/user/x", + expOK: true, + expVals: map[string]string{ + "repo": "x", + }, + }, { + path: "/your/user/x/", + expOK: true, + expVals: map[string]string{ + "repo": "x", + }, + }, { + path: "/your/name/x", + }}, + }, { + desc: "With double keys", + ep: &Endpoint{ + Path: "/:user /: repo ", + }, + paths: []testPath{{ + path: "/", + }, { + path: "/user", + }, { + path: "/user/repo", + expOK: true, + expVals: map[string]string{ + "user": "user", + "repo": "repo", + }, + }, { + path: "/user/repo/here", + }}, + }} + + for _, c := range cases { + t.Log(c.desc) + + rute, err := newRoute(c.ep) + if err != nil { + continue + } + + for _, tp := range c.paths { + gotVals, gotOK := rute.parse(tp.path) + + test.Assert(t, "vals", tp.expVals, gotVals, true) + test.Assert(t, "ok", tp.expOK, gotOK, true) + } + } +} |
