1
0
Fork 0

Compare commits

...

21 Commits

Author SHA1 Message Date
Alexander Andreev 32ae3a3d0d
A little optimisation in get() and add() methods. 2023-09-05 18:14:56 +04:00
Alexander Andreev 32e7468eef
len(path) moved down to where it is used. 2023-09-05 17:56:02 +04:00
Alexander Andreev 3e6d03db1a
Added a test for path parsing and benchmarks for newPath and newServePath. 2023-09-05 06:03:29 +04:00
Alexander Andreev c237f8c566
Moved httpr_test.go to httpr package to get access to private funcs. 2023-09-05 06:02:56 +04:00
Alexander Andreev a23264b00f
Added a notice to a comment for Handler() method. 2023-09-05 06:02:11 +04:00
Alexander Andreev b4163d2162
StatusMethodNotAllowed was replaced by a correct StatusNotImplemented for if there is no tree for such method. 2023-09-05 06:01:30 +04:00
Alexander Andreev 5d613b34ee
Added a newServePath() func that is a special variant of newPath that is used in ServeHTTP (it lacks an unnecessary check for * catch-all symbol). 2023-09-05 06:00:42 +04:00
Alexander Andreev cc2cd72df8
A little optimisation of newPath() func. 2023-09-05 05:58:29 +04:00
Alexander Andreev 92692454da
Inversed logic of if else statement to reduce nesting by 1. 2023-08-12 19:37:51 +04:00
Alexander Andreev a2cb6182e8
Updated comments. 2023-08-12 19:19:45 +04:00
Alexander Andreev aba211f3ec
Renamed a root var to a more logically suitable name base. 2023-08-12 19:19:04 +04:00
Alexander Andreev d53622908b
Shortened a Router.Handler() method by removing if statement in the end, here just returning a result of add() func is sufficient. 2023-08-12 18:56:50 +04:00
Alexander Andreev 7c8baeecf5
Changed error messages in newPath(). 2023-08-12 18:55:00 +04:00
Alexander Andreev 468606e4fd
Also added sub-path to an example in the README.md. 2023-08-11 18:46:49 +04:00
Alexander Andreev d9e5024d4d
Add an outer label to this continue statement as well. Just for good looking. :) 2023-08-11 18:45:17 +04:00
Alexander Andreev c68d7b324a
A test for Sub-path functionality. 2023-08-11 18:42:46 +04:00
Alexander Andreev 3cb32c5ec9
Sub-path implemented, now you can make a sub for a section using Router's Sub(root) method and then write only what this section contains. Like s := Sub("/api/v1") and then s.Handler("/"). 2023-08-11 18:42:28 +04:00
Alexander Andreev 0717a2e3d3
In case a path continues, but no child was found then break out of main for loop.
It was a bug that when there are children in the next node, but no parameterised one,  having a non-existent path element caused the main loop to continue and firing an if pathLen == i+1 case like if there was a legit node found.
2023-08-11 18:36:13 +04:00
Alexander Andreev e25a8a42c3
A year and copyright holders weren't filled in a LICENSE, LOL. 2023-07-23 23:27:49 +04:00
Alexander Andreev bc11a46806
Added a test for paths. 2023-07-23 23:26:35 +04:00
Alexander Andreev 33de30fe23
Sanitise double slashes, and return nil in get() if a path continues but a node doesn't have children. 2023-07-23 23:19:58 +04:00
4 changed files with 233 additions and 49 deletions

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2023 Alexander "Arav" Andreev <me@arav.su>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -24,6 +24,12 @@ This router is used like many others., example:
...
}
s := r.Sub("/api/v1")
s.Handler(http.MethodGet, "/", func(w http.ResponseWriter, r *http.Request) {
...
})
if err := http.ListenAndServe(":8000", r); err != nil {
...
}

159
httpr.go
View File

@ -9,18 +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 \"*\"")
}
parts := strings.Split(strings.TrimSuffix(path, "/"), "/")
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, "//", "/")
pathLen := len(path)
if path[pathLen-1] == '/' {
path = path[:pathLen-1]
}
parts := strings.Split(path, "/")
parts[0] = "/"
@ -60,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 {
@ -73,10 +104,15 @@ outer:
return curNode.handler, params
}
if pathLen > i+1 {
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
}
@ -88,8 +124,10 @@ outer:
if paramNode != nil {
curNode = paramNode
continue
continue outer
}
break outer
}
}
@ -97,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")
}
@ -111,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
}
@ -150,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 {
@ -185,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.
@ -204,8 +242,41 @@ func (rr *Router) ServeStatic(path string, root http.FileSystem) error {
})
}
// 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.
// 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
base string
}
// Sub creates a group of handlers with the same base path.
//
// 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,
base: base,
}
}
// 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.base+pattern, handler)
}
// 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]

View File

@ -1,15 +1,15 @@
package httpr_test
package httpr
import (
"net/http"
"net/http/httptest"
"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 {
@ -47,3 +47,110 @@ func Test(t *testing.T) {
}
}
func TestPaths(t *testing.T) {
found := false
r := New()
err := r.Handler(http.MethodGet, "/:lel", func(w http.ResponseWriter, r *http.Request) { found = true })
if err != nil {
t.Fatal(err)
}
r.NotFoundHandler = func(w http.ResponseWriter, r *http.Request) { found = false }
w := httptest.NewRecorder()
p := "/xmpp://me@arav.su"
req := httptest.NewRequest(http.MethodGet, p, strings.NewReader(""))
r.ServeHTTP(w, req)
if found {
t.Error("Path", p, "should return 404")
}
p = "/lel"
req = httptest.NewRequest(http.MethodGet, p, strings.NewReader(""))
r.ServeHTTP(w, req)
if !found {
t.Error("Path", p, "should return 200")
}
p = "/lel/lol"
req = httptest.NewRequest(http.MethodGet, p, strings.NewReader(""))
r.ServeHTTP(w, req)
if found {
t.Error("Path", p, "should return 404")
}
}
func TestSubPaths(t *testing.T) {
found := true
r := New()
s := r.Sub("/api/v1")
err := s.Handler(http.MethodGet, "/", func(w http.ResponseWriter, r *http.Request) { found = true })
if err != nil {
t.Fatal(err)
}
err = s.Handler(http.MethodGet, "/test", func(w http.ResponseWriter, r *http.Request) { found = true })
if err != nil {
t.Fatal(err)
}
r.NotFoundHandler = func(w http.ResponseWriter, r *http.Request) { found = false }
w := httptest.NewRecorder()
p := "/api/v1/"
req := httptest.NewRequest(http.MethodGet, p, strings.NewReader(""))
r.ServeHTTP(w, req)
if !found {
t.Error("Path", p, "should return 200")
}
p = "/api/v1/test"
req = httptest.NewRequest(http.MethodGet, p, strings.NewReader(""))
r.ServeHTTP(w, req)
if !found {
t.Error("Path", p, "should return 200")
}
p = "/api/v1/nonexistent"
req = httptest.NewRequest(http.MethodGet, p, strings.NewReader(""))
r.ServeHTTP(w, req)
if found {
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))
}
}