Compare commits
13 Commits
Author | SHA1 | Date |
---|---|---|
Alexander Andreev | 32ae3a3d0d | |
Alexander Andreev | 32e7468eef | |
Alexander Andreev | 3e6d03db1a | |
Alexander Andreev | c237f8c566 | |
Alexander Andreev | a23264b00f | |
Alexander Andreev | b4163d2162 | |
Alexander Andreev | 5d613b34ee | |
Alexander Andreev | cc2cd72df8 | |
Alexander Andreev | 92692454da | |
Alexander Andreev | a2cb6182e8 | |
Alexander Andreev | aba211f3ec | |
Alexander Andreev | d53622908b | |
Alexander Andreev | 7c8baeecf5 |
149
httpr.go
149
httpr.go
|
@ -9,20 +9,47 @@ import (
|
|||
|
||||
type path []string
|
||||
|
||||
// newPath splits a path and ensures that it starts with a slash (/) and doesn't
|
||||
// have more than 1 catch-all parameter.
|
||||
// newPath ensures that a path provided is correct and splits it.
|
||||
func newPath(path string) (path, error) {
|
||||
pathLen := len(path)
|
||||
|
||||
if pathLen == 0 {
|
||||
return nil, errors.New("empty path is not allowed")
|
||||
}
|
||||
|
||||
if path[0] != '/' {
|
||||
return nil, errors.New("path should start with a slash (/) symbol")
|
||||
return nil, errors.New("path should start with a slash symbol \"/\"")
|
||||
}
|
||||
|
||||
if strings.Count(path, "*") > 1 {
|
||||
return nil, errors.New("there can be only one catch-all (*) parameter in path")
|
||||
return nil, errors.New("path can have only one catch-all parameter \"*\"")
|
||||
}
|
||||
|
||||
if path[pathLen-1] == '/' {
|
||||
path = path[:pathLen-1]
|
||||
}
|
||||
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
parts[0] = "/"
|
||||
|
||||
return parts, nil
|
||||
}
|
||||
|
||||
// newServePath is a reduced version of newPath for ServeHTTP.
|
||||
func newServePath(path string) (path, error) {
|
||||
if path[0] != '/' {
|
||||
return nil, errors.New("path should start with a slash symbol \"/\"")
|
||||
}
|
||||
|
||||
path = strings.ReplaceAll(path, "//", "/")
|
||||
|
||||
parts := strings.Split(strings.TrimSuffix(path, "/"), "/")
|
||||
pathLen := len(path)
|
||||
if path[pathLen-1] == '/' {
|
||||
path = path[:pathLen-1]
|
||||
}
|
||||
|
||||
parts := strings.Split(path, "/")
|
||||
|
||||
parts[0] = "/"
|
||||
|
||||
|
@ -62,7 +89,9 @@ outer:
|
|||
path[i] = curNode.endpoint + ":" + path[i]
|
||||
}
|
||||
|
||||
if pathLen == i+1 {
|
||||
pathNextIdx := i + 1
|
||||
|
||||
if pathLen == pathNextIdx {
|
||||
var params Params = make(Params)
|
||||
|
||||
for _, part := range path {
|
||||
|
@ -75,14 +104,15 @@ outer:
|
|||
return curNode.handler, params
|
||||
}
|
||||
|
||||
if pathLen > i+1 {
|
||||
var paramNode *node
|
||||
if pathLen > pathNextIdx {
|
||||
if len(curNode.children) == 0 {
|
||||
break outer
|
||||
}
|
||||
|
||||
var paramNode *node
|
||||
|
||||
for _, next := range curNode.children {
|
||||
if next.endpoint == path[i+1] {
|
||||
if next.endpoint == path[pathNextIdx] {
|
||||
curNode = next
|
||||
continue outer
|
||||
}
|
||||
|
@ -105,12 +135,12 @@ outer:
|
|||
}
|
||||
|
||||
func (n *node) add(path path, handler http.HandlerFunc) error {
|
||||
pathLen := len(path)
|
||||
pathLastIdx := len(path) - 1
|
||||
curNode := n
|
||||
|
||||
outer:
|
||||
for i := range path {
|
||||
if pathLen == i+1 {
|
||||
if pathLastIdx == i {
|
||||
if curNode.handler != nil {
|
||||
return errors.New("attempt to redefine a handler for already existing path")
|
||||
}
|
||||
|
@ -119,28 +149,29 @@ outer:
|
|||
return nil
|
||||
}
|
||||
|
||||
pathNextIdx := i + 1
|
||||
|
||||
for _, child := range curNode.children {
|
||||
firstChar := path[i+1][0]
|
||||
firstChar := path[pathNextIdx][0]
|
||||
if (firstChar == ':' || firstChar == '*') && firstChar == child.endpoint[0] {
|
||||
// Do not allow different param names, because only the first one
|
||||
// is saved, so a param won't be available by a new name.
|
||||
//
|
||||
// I am not the one to judge, but it is a little strange to
|
||||
// expect different types of param in one place.
|
||||
if path[i+1] != child.endpoint {
|
||||
return errors.New("param names " + path[i+1] + " and " + child.endpoint + " are differ")
|
||||
// Therefore, it is good to return an error because in this case
|
||||
// you're doing something wrong.
|
||||
if path[pathNextIdx] != child.endpoint {
|
||||
return errors.New("param names " + path[pathNextIdx] + " and " + child.endpoint + " are differ")
|
||||
}
|
||||
curNode = child
|
||||
continue outer
|
||||
}
|
||||
|
||||
if child.endpoint == path[i+1] {
|
||||
if child.endpoint == path[pathNextIdx] {
|
||||
curNode = child
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
|
||||
newChild := &node{endpoint: path[i+1]}
|
||||
newChild := &node{endpoint: path[pathNextIdx]}
|
||||
curNode.children = append(curNode.children, newChild)
|
||||
curNode = newChild
|
||||
}
|
||||
|
@ -158,31 +189,34 @@ func New() *Router {
|
|||
}
|
||||
|
||||
func (rr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if tree, ok := rr.tree[r.Method]; ok {
|
||||
path, err := newPath(r.URL.Path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
tree, ok := rr.tree[r.Method]
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
if handler, params := tree.get(path); handler != nil {
|
||||
if params != nil {
|
||||
r = r.WithContext(context.WithValue(r.Context(), ParamsKey, params))
|
||||
}
|
||||
handler(w, r)
|
||||
} else {
|
||||
if rr.NotFoundHandler != nil {
|
||||
rr.NotFoundHandler(w, r)
|
||||
} else {
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
}
|
||||
path, err := newServePath(r.URL.Path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
|
||||
if handler, params := tree.get(path); handler != nil {
|
||||
if params != nil {
|
||||
r = r.WithContext(context.WithValue(r.Context(), ParamsKey, params))
|
||||
}
|
||||
handler(w, r)
|
||||
} else {
|
||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
if rr.NotFoundHandler != nil {
|
||||
rr.NotFoundHandler(w, r)
|
||||
} else {
|
||||
http.Error(w, "Not Found", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handler registers a handler for provided pattern for a given HTTP method.
|
||||
// Pattern must start with a slash (/) symbol.
|
||||
func (rr *Router) Handler(method, pattern string, handler http.HandlerFunc) error {
|
||||
path, err := newPath(pattern)
|
||||
if err != nil {
|
||||
|
@ -193,11 +227,7 @@ func (rr *Router) Handler(method, pattern string, handler http.HandlerFunc) erro
|
|||
rr.tree[method] = &node{endpoint: "/"}
|
||||
}
|
||||
|
||||
if err := rr.tree[method].add(path, handler); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return rr.tree[method].add(path, handler)
|
||||
}
|
||||
|
||||
// ServeStatic serves a given file system.
|
||||
|
@ -212,36 +242,41 @@ func (rr *Router) ServeStatic(path string, root http.FileSystem) error {
|
|||
})
|
||||
}
|
||||
|
||||
// subPath contains a root path that is being attached in front of a pattern
|
||||
// passed by a Handler() func.
|
||||
// subPath attaches a base path in front of a pattern.
|
||||
//
|
||||
// It is not a sub-router, it just passes a resulted pattern down to
|
||||
// a router instance.
|
||||
type subPath struct {
|
||||
r *Router
|
||||
root string
|
||||
base string
|
||||
}
|
||||
|
||||
// Sub returns a sub-path with a root path, after that you can shorten patterns.
|
||||
// Sub creates a group of handlers with the same base path.
|
||||
//
|
||||
// E.g. instead of writing each time "/api/something/other" create a
|
||||
// sub-router with a root path "/api/something" and then pass just "/other" in
|
||||
// a Handler() func of subPath struct.
|
||||
func (rr *Router) Sub(root string) *subPath {
|
||||
if root[len(root)-1] == '/' {
|
||||
root = root[:len(root)-1]
|
||||
// How to use:
|
||||
//
|
||||
// r := httpr.New()
|
||||
// ...
|
||||
// s := r.Sub("/api/v1")
|
||||
// s.Handler(http.MethodGet, "/", func(w, r) {...})
|
||||
// s.Handler(http.MethodGet, "/section", func(w, r) {...})
|
||||
func (rr *Router) Sub(base string) *subPath {
|
||||
if base[len(base)-1] == '/' {
|
||||
base = base[:len(base)-1]
|
||||
}
|
||||
|
||||
return &subPath{
|
||||
r: rr,
|
||||
root: root,
|
||||
base: base,
|
||||
}
|
||||
}
|
||||
|
||||
// Handler attaches root path to a given pattern and pass it to a router.
|
||||
// Handler registers a handler for a sub-path.
|
||||
func (sp *subPath) Handler(method, pattern string, handler http.HandlerFunc) error {
|
||||
return sp.r.Handler(method, sp.root+pattern, handler)
|
||||
return sp.r.Handler(method, sp.base+pattern, handler)
|
||||
}
|
||||
|
||||
// Param returns a URL parameter (that is set like `/a/b/:key/d`) with a key
|
||||
// or an empty string if no such parameter found.
|
||||
// Param returns a URL parameter set with :key, or an empty string if not found.
|
||||
func Param(r *http.Request, key string) string {
|
||||
if params := r.Context().Value(ParamsKey).(Params); params != nil {
|
||||
return params[key]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package httpr_test
|
||||
package httpr
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
@ -6,12 +6,10 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.arav.su/Arav/httpr"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
r := httpr.New()
|
||||
r := New()
|
||||
|
||||
err := r.Handler(http.MethodGet, "/", func(w http.ResponseWriter, r *http.Request) {})
|
||||
if err != nil {
|
||||
|
@ -53,7 +51,7 @@ func Test(t *testing.T) {
|
|||
func TestPaths(t *testing.T) {
|
||||
found := false
|
||||
|
||||
r := httpr.New()
|
||||
r := New()
|
||||
|
||||
err := r.Handler(http.MethodGet, "/:lel", func(w http.ResponseWriter, r *http.Request) { found = true })
|
||||
if err != nil {
|
||||
|
@ -88,7 +86,7 @@ func TestPaths(t *testing.T) {
|
|||
func TestSubPaths(t *testing.T) {
|
||||
found := true
|
||||
|
||||
r := httpr.New()
|
||||
r := New()
|
||||
|
||||
s := r.Sub("/api/v1")
|
||||
|
||||
|
@ -126,3 +124,33 @@ func TestSubPaths(t *testing.T) {
|
|||
t.Error(found, "Path", p, "should return 404")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPathParsing(t *testing.T) {
|
||||
p, err := newPath("/api/v1/../.")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Log(p)
|
||||
}
|
||||
|
||||
const testStr = "/api/v1/foo/bar/baz/abc/def/fucc/b0y/of/a/local/dungeon/master/got/his/ass/fisted/for/free/and/he/was/absolutely/happy/with/that/"
|
||||
|
||||
func BenchmarkPatternPathParsing(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
p, err := newPath(testStr)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Log(len(p))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkServePathParsing(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
p, err := newServePath(testStr)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.Log(len(p))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue