refactory: rebuild route tree
This commit is contained in:
parent
137d4ee5c8
commit
039f4cf8c0
213
app.go
213
app.go
@ -5,41 +5,61 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/loveuer/nf/internal/bytesconv"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"path"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
_ IRouter = (*App)(nil)
|
||||
|
||||
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
||||
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
||||
|
||||
mimePlain = []string{"text/plain"}
|
||||
)
|
||||
|
||||
type App struct {
|
||||
*RouterGroup
|
||||
RouterGroup
|
||||
config *Config
|
||||
router *router
|
||||
groups []*RouterGroup
|
||||
server *http.Server
|
||||
|
||||
trees methodTrees
|
||||
|
||||
maxParams uint16
|
||||
maxSections uint16
|
||||
|
||||
redirectTrailingSlash bool // true
|
||||
redirectFixedPath bool // false
|
||||
handleMethodNotAllowed bool // false
|
||||
useRawPath bool // false
|
||||
unescapePathValues bool // true
|
||||
removeExtraSlash bool // false
|
||||
}
|
||||
|
||||
func (a *App) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||
c := newContext(a, writer, request)
|
||||
var (
|
||||
err error
|
||||
c = newContext(a, writer, request)
|
||||
nfe = new(Err)
|
||||
)
|
||||
|
||||
for _, group := range a.groups {
|
||||
if strings.HasPrefix(request.URL.Path, group.prefix) {
|
||||
c.handlers = append(c.handlers, group.middlewares...)
|
||||
}
|
||||
if err = c.verify(); err != nil {
|
||||
if errors.As(err, nfe) {
|
||||
_ = c.Status(nfe.Status).SendString(nfe.Msg)
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.router.handle(c); err != nil {
|
||||
var ne = &Err{}
|
||||
|
||||
if errors.As(err, ne) {
|
||||
writer.WriteHeader(ne.Status)
|
||||
} else {
|
||||
writer.WriteHeader(500)
|
||||
_ = c.Status(500).SendString(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = writer.Write([]byte(err.Error()))
|
||||
}
|
||||
a.handleHTTPRequest(c)
|
||||
}
|
||||
|
||||
func (a *App) run(ln net.Listener) error {
|
||||
@ -90,3 +110,162 @@ func (a *App) RunListener(ln net.Listener) error {
|
||||
func (a *App) Shutdown(ctx context.Context) error {
|
||||
return a.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (a *App) addRoute(method, path string, handlers ...HandlerFunc) {
|
||||
elsePanic(path[0] == '/', "path must begin with '/'")
|
||||
elsePanic(method != "", "HTTP method can not be empty")
|
||||
elsePanic(len(handlers) > 0, "without enable not implement, there must be at least one handler")
|
||||
|
||||
if !a.config.DisableMessagePrint {
|
||||
fmt.Printf("[NF] Add Route: %-8s - %-25s (%2d handlers)\n", method, path, len(handlers))
|
||||
}
|
||||
|
||||
root := a.trees.get(method)
|
||||
if root == nil {
|
||||
root = new(node)
|
||||
root.fullPath = "/"
|
||||
a.trees = append(a.trees, methodTree{method: method, root: root})
|
||||
}
|
||||
|
||||
root.addRoute(path, handlers...)
|
||||
|
||||
if paramsCount := countParams(path); paramsCount > a.maxParams {
|
||||
a.maxParams = paramsCount
|
||||
}
|
||||
|
||||
if sectionsCount := countSections(path); sectionsCount > a.maxSections {
|
||||
a.maxSections = sectionsCount
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) handleHTTPRequest(c *Ctx) {
|
||||
httpMethod := c.Request.Method
|
||||
rPath := c.Request.URL.Path
|
||||
unescape := false
|
||||
if a.useRawPath && len(c.Request.URL.RawPath) > 0 {
|
||||
rPath = c.Request.URL.RawPath
|
||||
unescape = a.unescapePathValues
|
||||
}
|
||||
|
||||
if a.removeExtraSlash {
|
||||
rPath = cleanPath(rPath)
|
||||
}
|
||||
|
||||
// Find root of the tree for the given HTTP method
|
||||
t := a.trees
|
||||
for i, tl := 0, len(t); i < tl; i++ {
|
||||
if t[i].method != httpMethod {
|
||||
continue
|
||||
}
|
||||
root := t[i].root
|
||||
// Find route in tree
|
||||
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
|
||||
if value.params != nil {
|
||||
c.Params = *value.params
|
||||
}
|
||||
if value.handlers != nil {
|
||||
c.handlers = value.handlers
|
||||
c.fullPath = value.fullPath
|
||||
// todo
|
||||
c.Next()
|
||||
c.writermem.WriteHeaderNow()
|
||||
return
|
||||
}
|
||||
if httpMethod != http.MethodConnect && rPath != "/" {
|
||||
if value.tsr && a.redirectTrailingSlash {
|
||||
redirectTrailingSlash(c)
|
||||
return
|
||||
}
|
||||
if a.redirectFixedPath && redirectFixedPath(c, root, a.redirectFixedPath) {
|
||||
return
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if a.handleMethodNotAllowed {
|
||||
// According to RFC 7231 section 6.5.5, MUST generate an Allow header field in response
|
||||
// containing a list of the target resource's currently supported methods.
|
||||
allowed := make([]string, 0, len(t)-1)
|
||||
for _, tree := range a.trees {
|
||||
if tree.method == httpMethod {
|
||||
continue
|
||||
}
|
||||
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
|
||||
allowed = append(allowed, tree.method)
|
||||
}
|
||||
}
|
||||
|
||||
if len(allowed) > 0 {
|
||||
c.handlers = a.combineHandlers()
|
||||
|
||||
serveError(c, a.config.MethodNotAllowedHandler)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.handlers = a.combineHandlers()
|
||||
|
||||
serveError(c, a.config.NotFoundHandler)
|
||||
}
|
||||
|
||||
func errorHandler(c *Ctx) {
|
||||
_ = c.Status(500).SendString(_500)
|
||||
}
|
||||
|
||||
func serveError(c *Ctx, handler HandlerFunc) {
|
||||
err := c.Next()
|
||||
|
||||
if c.writermem.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
_ = handler(c)
|
||||
_ = err
|
||||
}
|
||||
|
||||
func redirectTrailingSlash(c *Ctx) {
|
||||
req := c.Request
|
||||
p := req.URL.Path
|
||||
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||
prefix = regSafePrefix.ReplaceAllString(prefix, "")
|
||||
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
|
||||
|
||||
p = prefix + "/" + req.URL.Path
|
||||
}
|
||||
req.URL.Path = p + "/"
|
||||
if length := len(p); length > 1 && p[length-1] == '/' {
|
||||
req.URL.Path = p[:length-1]
|
||||
}
|
||||
|
||||
redirectRequest(c)
|
||||
}
|
||||
|
||||
func redirectFixedPath(c *Ctx, root *node, trailingSlash bool) bool {
|
||||
req := c.Request
|
||||
rPath := req.URL.Path
|
||||
|
||||
if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
|
||||
req.URL.Path = bytesconv.BytesToString(fixedPath)
|
||||
redirectRequest(c)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func redirectRequest(c *Ctx) {
|
||||
req := c.Request
|
||||
//rPath := req.URL.Path
|
||||
rURL := req.URL.String()
|
||||
|
||||
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
|
||||
if req.Method != http.MethodGet {
|
||||
code = http.StatusTemporaryRedirect
|
||||
}
|
||||
|
||||
//debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
|
||||
|
||||
http.Redirect(c.writer, req, rURL, code)
|
||||
c.writermem.WriteHeaderNow()
|
||||
}
|
||||
|
63
ctx.go
63
ctx.go
@ -12,35 +12,54 @@ import (
|
||||
)
|
||||
|
||||
type Ctx struct {
|
||||
writermem responseWriter
|
||||
// origin objects
|
||||
writer http.ResponseWriter
|
||||
Request *http.Request
|
||||
// request info
|
||||
path string
|
||||
Method string
|
||||
method string
|
||||
// response info
|
||||
StatusCode int
|
||||
|
||||
app *App
|
||||
params map[string]string
|
||||
params *Params
|
||||
index int
|
||||
handlers []HandlerFunc
|
||||
locals map[string]interface{}
|
||||
skippedNodes *[]skippedNode
|
||||
fullPath string
|
||||
Params Params
|
||||
}
|
||||
|
||||
func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ctx {
|
||||
return &Ctx{
|
||||
|
||||
skippedNodes := make([]skippedNode, 0, app.maxSections)
|
||||
v := make(Params, 0, app.maxParams)
|
||||
|
||||
ctx := &Ctx{
|
||||
writer: writer,
|
||||
writermem: responseWriter{},
|
||||
Request: request,
|
||||
path: request.URL.Path,
|
||||
Method: request.Method,
|
||||
method: request.Method,
|
||||
StatusCode: 200,
|
||||
|
||||
app: app,
|
||||
index: -1,
|
||||
locals: map[string]interface{}{},
|
||||
handlers: make([]HandlerFunc, 0),
|
||||
skippedNodes: &skippedNodes,
|
||||
params: &v,
|
||||
}
|
||||
|
||||
ctx.writermem = responseWriter{
|
||||
ResponseWriter: ctx.writer,
|
||||
size: -1,
|
||||
status: 0,
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (c *Ctx) Locals(key string, value ...interface{}) interface{} {
|
||||
@ -52,6 +71,16 @@ func (c *Ctx) Locals(key string, value ...interface{}) interface{} {
|
||||
return data
|
||||
}
|
||||
|
||||
func (c *Ctx) Method(overWrite ...string) string {
|
||||
method := c.Request.Method
|
||||
|
||||
if len(overWrite) > 0 && overWrite[0] != "" {
|
||||
c.Request.Method = overWrite[0]
|
||||
}
|
||||
|
||||
return method
|
||||
}
|
||||
|
||||
func (c *Ctx) Path(overWrite ...string) string {
|
||||
path := c.Request.URL.Path
|
||||
if len(overWrite) > 0 && overWrite[0] != "" {
|
||||
@ -83,11 +112,17 @@ func (c *Ctx) Next() error {
|
||||
|
||||
var err error
|
||||
|
||||
if c.index < len(c.handlers) {
|
||||
err = c.handlers[c.index](c)
|
||||
for c.index < len(c.handlers) {
|
||||
if c.handlers[c.index] != nil {
|
||||
if err = c.handlers[c.index](c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
c.index++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/* ===============================================================
|
||||
@ -104,7 +139,7 @@ func (c *Ctx) verify() error {
|
||||
}
|
||||
|
||||
func (c *Ctx) Param(key string) string {
|
||||
return c.params[key]
|
||||
return c.Params.ByName(key)
|
||||
}
|
||||
|
||||
func (c *Ctx) Form(key string) string {
|
||||
@ -196,17 +231,17 @@ func (c *Ctx) QueryParser(out interface{}) error {
|
||||
=============================================================== */
|
||||
|
||||
func (c *Ctx) Status(code int) *Ctx {
|
||||
c.StatusCode = code
|
||||
c.writer.WriteHeader(code)
|
||||
c.writermem.WriteHeader(code)
|
||||
c.StatusCode = c.writermem.status
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Ctx) Set(key string, value string) {
|
||||
c.writer.Header().Set(key, value)
|
||||
c.writermem.Header().Set(key, value)
|
||||
}
|
||||
|
||||
func (c *Ctx) SetHeader(key string, value string) {
|
||||
c.writer.Header().Set(key, value)
|
||||
c.writermem.Header().Set(key, value)
|
||||
}
|
||||
|
||||
func (c *Ctx) SendString(data string) error {
|
||||
@ -223,7 +258,7 @@ func (c *Ctx) Writef(format string, values ...interface{}) (int, error) {
|
||||
func (c *Ctx) JSON(data interface{}) error {
|
||||
c.SetHeader("Content-Type", MIMEApplicationJSON)
|
||||
|
||||
encoder := json.NewEncoder(c.writer)
|
||||
encoder := json.NewEncoder(&c.writermem)
|
||||
|
||||
if err := encoder.Encode(data); err != nil {
|
||||
return err
|
||||
@ -237,7 +272,7 @@ func (c *Ctx) RawWriter() http.ResponseWriter {
|
||||
}
|
||||
|
||||
func (c *Ctx) Write(data []byte) (int, error) {
|
||||
return c.writer.Write(data)
|
||||
return c.writermem.Write(data)
|
||||
}
|
||||
|
||||
func (c *Ctx) HTML(html string) error {
|
||||
|
80
group.go
80
group.go
@ -1,80 +0,0 @@
|
||||
package nf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type RouterGroup struct {
|
||||
prefix string
|
||||
middlewares []HandlerFunc // support middleware
|
||||
parent *RouterGroup // support nesting
|
||||
app *App // all groups share a Engine instance
|
||||
}
|
||||
|
||||
// Group is defined to create a new RouterGroup
|
||||
// remember all groups share the same Engine instance
|
||||
func (group *RouterGroup) Group(prefix string) *RouterGroup {
|
||||
app := group.app
|
||||
newGroup := &RouterGroup{
|
||||
prefix: group.prefix + prefix,
|
||||
parent: group,
|
||||
app: app,
|
||||
}
|
||||
app.groups = append(app.groups, newGroup)
|
||||
return newGroup
|
||||
}
|
||||
|
||||
func (group *RouterGroup) verifyHandlers(path string, handlers ...HandlerFunc) []HandlerFunc {
|
||||
if len(handlers) == 0 {
|
||||
if !group.app.config.EnableNotImplementHandler {
|
||||
panic(fmt.Sprintf("missing handler in route: %s", path))
|
||||
}
|
||||
|
||||
handlers = append(handlers, ToDoHandler)
|
||||
}
|
||||
|
||||
for _, handler := range handlers {
|
||||
if handler == nil {
|
||||
panic(fmt.Sprintf("nil handler found in route: %s", path))
|
||||
}
|
||||
}
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
func (group *RouterGroup) addRoute(method string, comp string, handlers ...HandlerFunc) {
|
||||
handlers = group.verifyHandlers(comp, handlers...)
|
||||
pattern := group.prefix + comp
|
||||
log.Printf("Add Route %4s - %s", method, pattern)
|
||||
group.app.router.addRoute(method, pattern, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Get(pattern string, handlers ...HandlerFunc) {
|
||||
group.addRoute(http.MethodGet, pattern, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Post(pattern string, handlers ...HandlerFunc) {
|
||||
group.addRoute(http.MethodPost, pattern, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Put(pattern string, handlers ...HandlerFunc) {
|
||||
group.addRoute(http.MethodPut, pattern, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Delete(pattern string, handlers ...HandlerFunc) {
|
||||
group.addRoute(http.MethodDelete, pattern, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Patch(pattern string, handlers ...HandlerFunc) {
|
||||
group.addRoute(http.MethodPatch, pattern, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Head(pattern string, handlers ...HandlerFunc) {
|
||||
group.addRoute(http.MethodHead, pattern, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
|
||||
group.middlewares = append(group.middlewares, middlewares...)
|
||||
}
|
@ -5,5 +5,5 @@ import "fmt"
|
||||
type HandlerFunc func(*Ctx) error
|
||||
|
||||
func ToDoHandler(c *Ctx) error {
|
||||
return c.Status(501).SendString(fmt.Sprintf("%s - %s Not Implemented", c.Method, c.Path()))
|
||||
return c.Status(501).SendString(fmt.Sprintf("%s - %s Not Implemented", c.Method(), c.Path()))
|
||||
}
|
||||
|
26
internal/bytesconv/bytesconv_1.19.go
Normal file
26
internal/bytesconv/bytesconv_1.19.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2020 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !go1.20
|
||||
|
||||
package bytesconv
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// StringToBytes converts string to byte slice without a memory allocation.
|
||||
func StringToBytes(s string) []byte {
|
||||
return *(*[]byte)(unsafe.Pointer(
|
||||
&struct {
|
||||
string
|
||||
Cap int
|
||||
}{s, len(s)},
|
||||
))
|
||||
}
|
||||
|
||||
// BytesToString converts byte slice to string without a memory allocation.
|
||||
func BytesToString(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
23
internal/bytesconv/bytesconv_1.20.go
Normal file
23
internal/bytesconv/bytesconv_1.20.go
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright 2023 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.20
|
||||
|
||||
package bytesconv
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// StringToBytes converts string to byte slice without a memory allocation.
|
||||
// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077.
|
||||
func StringToBytes(s string) []byte {
|
||||
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||
}
|
||||
|
||||
// BytesToString converts byte slice to string without a memory allocation.
|
||||
// For more details, see https://github.com/golang/go/issues/53003#issuecomment-1140276077.
|
||||
func BytesToString(b []byte) string {
|
||||
return unsafe.String(unsafe.SliceData(b), len(b))
|
||||
}
|
99
internal/bytesconv/bytesconv_test.go
Normal file
99
internal/bytesconv/bytesconv_test.go
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright 2020 Gin Core Team. All rights reserved.
|
||||
// Use of this source code is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package bytesconv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var testString = "Albert Einstein: Logic will get you from A to B. Imagination will take you everywhere."
|
||||
var testBytes = []byte(testString)
|
||||
|
||||
func rawBytesToStr(b []byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func rawStrToBytes(s string) []byte {
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
// go test -v
|
||||
|
||||
func TestBytesToString(t *testing.T) {
|
||||
data := make([]byte, 1024)
|
||||
for i := 0; i < 100; i++ {
|
||||
rand.Read(data)
|
||||
if rawBytesToStr(data) != BytesToString(data) {
|
||||
t.Fatal("don't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
const (
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
var src = rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
func RandStringBytesMaskImprSrcSB(n int) string {
|
||||
sb := strings.Builder{}
|
||||
sb.Grow(n)
|
||||
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = src.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
sb.WriteByte(letterBytes[idx])
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestStringToBytes(t *testing.T) {
|
||||
for i := 0; i < 100; i++ {
|
||||
s := RandStringBytesMaskImprSrcSB(64)
|
||||
if !bytes.Equal(rawStrToBytes(s), StringToBytes(s)) {
|
||||
t.Fatal("don't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// go test -v -run=none -bench=^BenchmarkBytesConv -benchmem=true
|
||||
|
||||
func BenchmarkBytesConvBytesToStrRaw(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
rawBytesToStr(testBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytesConvBytesToStr(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
BytesToString(testBytes)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytesConvStrToBytesRaw(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
rawStrToBytes(testString)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkBytesConvStrToBytes(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
StringToBytes(testString)
|
||||
}
|
||||
}
|
@ -17,6 +17,9 @@ func NewRecover(enableStackTrace bool) HandlerFunc {
|
||||
} else {
|
||||
os.Stderr.WriteString(fmt.Sprintf("recovered from panic: %v\n", r))
|
||||
}
|
||||
|
||||
//serveError(c, 500, []byte(fmt.Sprint(r)))
|
||||
_ = c.Status(500).SendString(fmt.Sprint(r))
|
||||
}
|
||||
}()
|
||||
|
||||
@ -47,7 +50,7 @@ func NewLogger() HandlerFunc {
|
||||
unit = "s"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v %s", num, unit)
|
||||
return fmt.Sprintf("%3d %2s", num, unit)
|
||||
}
|
||||
|
||||
return func(c *Ctx) error {
|
||||
|
32
nf.go
32
nf.go
@ -3,11 +3,14 @@ package nf
|
||||
const (
|
||||
banner = " _ _ _ ___ _ \n | \\| |___| |_ | __|__ _ _ _ _ __| |\n | .` / _ \\ _| | _/ _ \\ || | ' \\/ _` |\n |_|\\_\\___/\\__| |_|\\___/\\_,_|_||_\\__,_|\n "
|
||||
_404 = "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,user-scalable=no,initial-scale=1,maximum-scale=1,minimum-scale=1\"><meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\"><title>Not Found</title><style>body{background:#333;margin:0;color:#ccc;display:flex;align-items:center;max-height:100vh;height:100vh;justify-content:center}textarea{min-height:5rem;min-width:20rem;text-align:center;border:none;background:0 0;color:#ccc;resize:none;user-input:none;user-select:none;cursor:default;-webkit-user-select:none;-webkit-touch-callout:none;-moz-user-select:none;-ms-user-select:none;outline:0}</style></head><body><textarea id=\"banner\" readonly=\"readonly\"></textarea><script type=\"text/javascript\">let htmlCodes = [\n ' _ _ _ ___ _ ',\n '| \\\\| |___| |_ | __|__ _ _ _ _ __| |',\n '| .` / _ \\\\ _| | _/ _ \\\\ || | \\' \\\\/ _` |',\n '|_|\\\\_\\\\___/\\\\__| |_|\\\\___/\\\\_,_|_||_\\\\__,_|'\n].join('\\n');\ndocument.querySelector('#banner').value = htmlCodes</script></body></html>"
|
||||
_405 = `405 Method Not Allowed`
|
||||
_500 = `500 Internal Server Error`
|
||||
)
|
||||
|
||||
type Map map[string]interface{}
|
||||
|
||||
type Config struct {
|
||||
DisableMessagePrint bool `json:"-"`
|
||||
// Default: 4 * 1024 * 1024
|
||||
BodyLimit int64 `json:"-"`
|
||||
|
||||
@ -19,8 +22,9 @@ type Config struct {
|
||||
DisableRecover bool `json:"-"`
|
||||
DisableHttpErrorLog bool `json:"-"`
|
||||
|
||||
EnableNotImplementHandler bool `json:"-"`
|
||||
//EnableNotImplementHandler bool `json:"-"`
|
||||
NotFoundHandler HandlerFunc `json:"-"`
|
||||
MethodNotAllowedHandler HandlerFunc `json:"-"`
|
||||
}
|
||||
|
||||
var (
|
||||
@ -31,16 +35,33 @@ var (
|
||||
_, err := c.Status(404).Write([]byte(_404))
|
||||
return err
|
||||
},
|
||||
MethodNotAllowedHandler: func(c *Ctx) error {
|
||||
c.Set("Content-Type", MIMETextPlain)
|
||||
_, err := c.Status(405).Write([]byte(_405))
|
||||
return err
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func New(config ...Config) *App {
|
||||
app := &App{
|
||||
router: newRouter(),
|
||||
RouterGroup: RouterGroup{
|
||||
Handlers: nil,
|
||||
basePath: "/",
|
||||
root: true,
|
||||
},
|
||||
|
||||
redirectTrailingSlash: true, // true
|
||||
redirectFixedPath: false, // false
|
||||
handleMethodNotAllowed: true, // false
|
||||
useRawPath: false, // false
|
||||
unescapePathValues: true, // true
|
||||
removeExtraSlash: false, // false
|
||||
}
|
||||
|
||||
if len(config) > 0 {
|
||||
app.config = &config[0]
|
||||
|
||||
if app.config.BodyLimit == 0 {
|
||||
app.config.BodyLimit = defaultConfig.BodyLimit
|
||||
}
|
||||
@ -49,12 +70,15 @@ func New(config ...Config) *App {
|
||||
app.config.NotFoundHandler = defaultConfig.NotFoundHandler
|
||||
}
|
||||
|
||||
if app.config.MethodNotAllowedHandler == nil {
|
||||
app.config.MethodNotAllowedHandler = defaultConfig.MethodNotAllowedHandler
|
||||
}
|
||||
|
||||
} else {
|
||||
app.config = defaultConfig
|
||||
}
|
||||
|
||||
app.RouterGroup = &RouterGroup{app: app, prefix: ""}
|
||||
app.groups = []*RouterGroup{app.RouterGroup}
|
||||
app.RouterGroup.app = app
|
||||
|
||||
if !app.config.DisableLogger {
|
||||
app.Use(NewLogger())
|
||||
|
132
response_writer.go
Normal file
132
response_writer.go
Normal file
@ -0,0 +1,132 @@
|
||||
package nf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
noWritten = -1
|
||||
defaultStatus = http.StatusOK
|
||||
)
|
||||
|
||||
// ResponseWriter ...
|
||||
type ResponseWriter interface {
|
||||
http.ResponseWriter
|
||||
http.Hijacker
|
||||
http.Flusher
|
||||
http.CloseNotifier
|
||||
|
||||
// Status returns the HTTP response status code of the current request.
|
||||
Status() int
|
||||
|
||||
// Size returns the number of bytes already written into the response http body.
|
||||
// See Written()
|
||||
Size() int
|
||||
|
||||
// WriteString writes the string into the response body.
|
||||
WriteString(string) (int, error)
|
||||
|
||||
// Written returns true if the response body was already written.
|
||||
Written() bool
|
||||
|
||||
// WriteHeaderNow forces to write the http header (status code + headers).
|
||||
WriteHeaderNow()
|
||||
|
||||
// Pusher get the http.Pusher for server push
|
||||
Pusher() http.Pusher
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
size int
|
||||
status int
|
||||
}
|
||||
|
||||
var _ ResponseWriter = (*responseWriter)(nil)
|
||||
|
||||
func (w *responseWriter) Unwrap() http.ResponseWriter {
|
||||
return w.ResponseWriter
|
||||
}
|
||||
|
||||
func (w *responseWriter) reset(writer http.ResponseWriter) {
|
||||
w.ResponseWriter = writer
|
||||
w.size = noWritten
|
||||
w.status = defaultStatus
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteHeader(code int) {
|
||||
if code > 0 && w.status != code {
|
||||
if w.Written() {
|
||||
// todo: debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
|
||||
return
|
||||
}
|
||||
w.status = code
|
||||
}
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteHeaderNow() {
|
||||
if !w.Written() {
|
||||
w.size = 0
|
||||
|
||||
if w.status == 0 {
|
||||
w.status = 200
|
||||
}
|
||||
|
||||
w.ResponseWriter.WriteHeader(w.status)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *responseWriter) Write(data []byte) (n int, err error) {
|
||||
w.WriteHeaderNow()
|
||||
n, err = w.ResponseWriter.Write(data)
|
||||
w.size += n
|
||||
return
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteString(s string) (n int, err error) {
|
||||
w.WriteHeaderNow()
|
||||
n, err = io.WriteString(w.ResponseWriter, s)
|
||||
w.size += n
|
||||
return
|
||||
}
|
||||
|
||||
func (w *responseWriter) Status() int {
|
||||
return w.status
|
||||
}
|
||||
|
||||
func (w *responseWriter) Size() int {
|
||||
return w.size
|
||||
}
|
||||
|
||||
func (w *responseWriter) Written() bool {
|
||||
return w.size != noWritten
|
||||
}
|
||||
|
||||
// Hijack implements the http.Hijacker interface.
|
||||
func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if w.size < 0 {
|
||||
w.size = 0
|
||||
}
|
||||
return w.ResponseWriter.(http.Hijacker).Hijack()
|
||||
}
|
||||
|
||||
// CloseNotify implements the http.CloseNotifier interface.
|
||||
func (w *responseWriter) CloseNotify() <-chan bool {
|
||||
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||
}
|
||||
|
||||
// Flush implements the http.Flusher interface.
|
||||
func (w *responseWriter) Flush() {
|
||||
w.WriteHeaderNow()
|
||||
w.ResponseWriter.(http.Flusher).Flush()
|
||||
}
|
||||
|
||||
func (w *responseWriter) Pusher() (pusher http.Pusher) {
|
||||
if pusher, ok := w.ResponseWriter.(http.Pusher); ok {
|
||||
return pusher
|
||||
}
|
||||
return nil
|
||||
}
|
100
router.go
100
router.go
@ -1,100 +0,0 @@
|
||||
package nf
|
||||
|
||||
import "strings"
|
||||
|
||||
type router struct {
|
||||
roots map[string]*_node
|
||||
handlers map[string][]HandlerFunc
|
||||
}
|
||||
|
||||
func newRouter() *router {
|
||||
return &router{
|
||||
roots: make(map[string]*_node),
|
||||
handlers: make(map[string][]HandlerFunc),
|
||||
}
|
||||
}
|
||||
|
||||
// Only one * is allowed
|
||||
func parsePattern(pattern string) []string {
|
||||
vs := strings.Split(pattern, "/")
|
||||
|
||||
parts := make([]string, 0)
|
||||
for _, item := range vs {
|
||||
if item != "" {
|
||||
parts = append(parts, item)
|
||||
if item[0] == '*' {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func (r *router) addRoute(method string, pattern string, handlers ...HandlerFunc) {
|
||||
parts := parsePattern(pattern)
|
||||
|
||||
key := method + "-" + pattern
|
||||
_, ok := r.roots[method]
|
||||
if !ok {
|
||||
r.roots[method] = &_node{}
|
||||
}
|
||||
r.roots[method].insert(pattern, parts, 0)
|
||||
r.handlers[key] = handlers
|
||||
}
|
||||
|
||||
func (r *router) getRoute(method string, path string) (*_node, map[string]string) {
|
||||
searchParts := parsePattern(path)
|
||||
params := make(map[string]string)
|
||||
root, ok := r.roots[method]
|
||||
|
||||
if !ok {
|
||||
return &_node{}, nil
|
||||
}
|
||||
|
||||
n := root.search(searchParts, 0)
|
||||
|
||||
if n != nil {
|
||||
parts := parsePattern(n.pattern)
|
||||
for index, part := range parts {
|
||||
if part[0] == ':' {
|
||||
params[part[1:]] = searchParts[index]
|
||||
}
|
||||
if part[0] == '*' && len(part) > 1 {
|
||||
params[part[1:]] = strings.Join(searchParts[index:], "/")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return n, params
|
||||
}
|
||||
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func (r *router) getRoutes(method string) []*_node {
|
||||
root, ok := r.roots[method]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
nodes := make([]*_node, 0)
|
||||
root.travel(&nodes)
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (r *router) handle(c *Ctx) error {
|
||||
if err := c.verify(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
node, params := r.getRoute(c.Method, c.path)
|
||||
if node != nil {
|
||||
c.params = params
|
||||
key := c.Method + "-" + node.pattern
|
||||
c.handlers = append(c.handlers, r.handlers[key]...)
|
||||
//c.handlers = append(r.handlers[key], c.handlers...)
|
||||
} else {
|
||||
return c.app.config.NotFoundHandler(c)
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
155
routergroup.go
Normal file
155
routergroup.go
Normal file
@ -0,0 +1,155 @@
|
||||
package nf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
"path"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var (
|
||||
// regEnLetter matches english letters for http method name
|
||||
regEnLetter = regexp.MustCompile("^[A-Z]+$")
|
||||
|
||||
// anyMethods for RouterGroup Any method
|
||||
anyMethods = []string{
|
||||
http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch,
|
||||
http.MethodHead, http.MethodOptions, http.MethodDelete, http.MethodConnect,
|
||||
http.MethodTrace,
|
||||
}
|
||||
)
|
||||
|
||||
// IRouter defines all router handle interface includes single and group router.
|
||||
type IRouter interface {
|
||||
IRoutes
|
||||
Group(string, ...HandlerFunc) *RouterGroup
|
||||
}
|
||||
|
||||
// IRoutes defines all router handle interface.
|
||||
type IRoutes interface {
|
||||
Use(...HandlerFunc) IRoutes
|
||||
|
||||
Handle(string, string, ...HandlerFunc) IRoutes
|
||||
Any(string, ...HandlerFunc) IRoutes
|
||||
Get(string, ...HandlerFunc) IRoutes
|
||||
Post(string, ...HandlerFunc) IRoutes
|
||||
Delete(string, ...HandlerFunc) IRoutes
|
||||
Patch(string, ...HandlerFunc) IRoutes
|
||||
Put(string, ...HandlerFunc) IRoutes
|
||||
Options(string, ...HandlerFunc) IRoutes
|
||||
Head(string, ...HandlerFunc) IRoutes
|
||||
Match([]string, string, ...HandlerFunc) IRoutes
|
||||
|
||||
//StaticFile(string, string) IRoutes
|
||||
//StaticFileFS(string, string, http.FileSystem) IRoutes
|
||||
//Static(string, string) IRoutes
|
||||
//StaticFS(string, http.FileSystem) IRoutes
|
||||
}
|
||||
|
||||
type RouterGroup struct {
|
||||
Handlers []HandlerFunc
|
||||
basePath string
|
||||
app *App
|
||||
root bool
|
||||
}
|
||||
|
||||
var _ IRouter = (*RouterGroup)(nil)
|
||||
|
||||
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
|
||||
group.Handlers = append(group.Handlers, middleware...)
|
||||
return group.returnObj()
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
|
||||
return &RouterGroup{
|
||||
Handlers: group.combineHandlers(handlers...),
|
||||
basePath: group.calculateAbsolutePath(relativePath),
|
||||
app: group.app,
|
||||
}
|
||||
}
|
||||
|
||||
func (group *RouterGroup) BasePath() string {
|
||||
return group.basePath
|
||||
}
|
||||
|
||||
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
absolutePath := group.calculateAbsolutePath(relativePath)
|
||||
handlers = group.combineHandlers(handlers...)
|
||||
group.app.addRoute(httpMethod, absolutePath, handlers...)
|
||||
return group.returnObj()
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
if matched := regEnLetter.MatchString(httpMethod); !matched {
|
||||
panic("http method " + httpMethod + " is not valid")
|
||||
}
|
||||
return group.handle(httpMethod, relativePath, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Post(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
return group.handle(http.MethodPost, relativePath, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Get(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
return group.handle(http.MethodGet, relativePath, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Delete(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
return group.handle(http.MethodDelete, relativePath, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Patch(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
return group.handle(http.MethodPatch, relativePath, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Put(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
return group.handle(http.MethodPut, relativePath, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Options(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
return group.handle(http.MethodOptions, relativePath, handlers...)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Head(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
return group.handle(http.MethodHead, relativePath, handlers...)
|
||||
}
|
||||
|
||||
// Any registers a route that matches all the HTTP methods.
|
||||
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
|
||||
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
for _, method := range anyMethods {
|
||||
group.handle(method, relativePath, handlers...)
|
||||
}
|
||||
|
||||
return group.returnObj()
|
||||
}
|
||||
|
||||
func (group *RouterGroup) Match(methods []string, relativePath string, handlers ...HandlerFunc) IRoutes {
|
||||
for _, method := range methods {
|
||||
group.handle(method, relativePath, handlers...)
|
||||
}
|
||||
|
||||
return group.returnObj()
|
||||
}
|
||||
|
||||
const abortIndex int8 = math.MaxInt8 >> 1
|
||||
|
||||
func (group *RouterGroup) combineHandlers(handlers ...HandlerFunc) []HandlerFunc {
|
||||
finalSize := len(group.Handlers) + len(handlers)
|
||||
elsePanic(finalSize < int(abortIndex), "too many handlers")
|
||||
mergedHandlers := make([]HandlerFunc, finalSize)
|
||||
copy(mergedHandlers, group.Handlers)
|
||||
copy(mergedHandlers[len(group.Handlers):], handlers)
|
||||
return mergedHandlers
|
||||
}
|
||||
|
||||
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
|
||||
return path.Join(group.basePath, relativePath)
|
||||
}
|
||||
|
||||
func (group *RouterGroup) returnObj() IRoutes {
|
||||
if group.root {
|
||||
return group.app
|
||||
}
|
||||
return group
|
||||
}
|
903
tree.go
903
tree.go
@ -1,76 +1,891 @@
|
||||
package nf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/url"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/loveuer/nf/internal/bytesconv"
|
||||
)
|
||||
|
||||
type _node struct {
|
||||
pattern string
|
||||
part string
|
||||
children []*_node
|
||||
isWild bool
|
||||
var (
|
||||
strColon = []byte(":")
|
||||
strStar = []byte("*")
|
||||
strSlash = []byte("/")
|
||||
)
|
||||
|
||||
// Param is a single URL parameter, consisting of a key and a value.
|
||||
type Param struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func (n *_node) insert(pattern string, parts []string, height int) {
|
||||
if len(parts) == height {
|
||||
n.pattern = pattern
|
||||
// Params is a Param-slice, as returned by the router.
|
||||
// The slice is ordered, the first URL parameter is also the first slice value.
|
||||
// It is therefore safe to read values by the index.
|
||||
type Params []Param
|
||||
|
||||
// Get returns the value of the first Param which key matches the given name and a boolean true.
|
||||
// If no matching Param is found, an empty string is returned and a boolean false .
|
||||
func (ps Params) Get(name string) (string, bool) {
|
||||
for _, entry := range ps {
|
||||
if entry.Key == name {
|
||||
return entry.Value, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// ByName returns the value of the first Param which key matches the given name.
|
||||
// If no matching Param is found, an empty string is returned.
|
||||
func (ps Params) ByName(name string) (va string) {
|
||||
va, _ = ps.Get(name)
|
||||
return
|
||||
}
|
||||
|
||||
part := parts[height]
|
||||
child := n.matchChild(part)
|
||||
if child == nil {
|
||||
child = &_node{part: part, isWild: part[0] == ':' || part[0] == '*'}
|
||||
n.children = append(n.children, child)
|
||||
}
|
||||
child.insert(pattern, parts, height+1)
|
||||
type methodTree struct {
|
||||
method string
|
||||
root *node
|
||||
}
|
||||
|
||||
func (n *_node) search(parts []string, height int) *_node {
|
||||
if len(parts) == height || strings.HasPrefix(n.part, "*") {
|
||||
if n.pattern == "" {
|
||||
type methodTrees []methodTree
|
||||
|
||||
func (trees methodTrees) get(method string) *node {
|
||||
for _, tree := range trees {
|
||||
if tree.method == method {
|
||||
return tree.root
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a <= b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func longestCommonPrefix(a, b string) int {
|
||||
i := 0
|
||||
max := min(len(a), len(b))
|
||||
for i < max && a[i] == b[i] {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// addChild will add a child node, keeping wildcardChild at the end
|
||||
func (n *node) addChild(child *node) {
|
||||
if n.wildChild && len(n.children) > 0 {
|
||||
wildcardChild := n.children[len(n.children)-1]
|
||||
n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
|
||||
} else {
|
||||
n.children = append(n.children, child)
|
||||
}
|
||||
}
|
||||
|
||||
func countParams(path string) uint16 {
|
||||
var n uint16
|
||||
s := bytesconv.StringToBytes(path)
|
||||
n += uint16(bytes.Count(s, strColon))
|
||||
n += uint16(bytes.Count(s, strStar))
|
||||
return n
|
||||
}
|
||||
|
||||
part := parts[height]
|
||||
children := n.matchChildren(part)
|
||||
func countSections(path string) uint16 {
|
||||
s := bytesconv.StringToBytes(path)
|
||||
return uint16(bytes.Count(s, strSlash))
|
||||
}
|
||||
|
||||
for _, child := range children {
|
||||
result := child.search(parts, height+1)
|
||||
if result != nil {
|
||||
return result
|
||||
type nodeType uint8
|
||||
|
||||
const (
|
||||
static nodeType = iota
|
||||
root
|
||||
param
|
||||
catchAll
|
||||
)
|
||||
|
||||
type node struct {
|
||||
path string
|
||||
indices string
|
||||
wildChild bool
|
||||
nType nodeType
|
||||
priority uint32
|
||||
children []*node // child nodes, at most 1 :param style node at the end of the array
|
||||
handlers []HandlerFunc
|
||||
fullPath string
|
||||
}
|
||||
|
||||
// Increments priority of the given child and reorders if necessary
|
||||
func (n *node) incrementChildPrio(pos int) int {
|
||||
cs := n.children
|
||||
cs[pos].priority++
|
||||
prio := cs[pos].priority
|
||||
|
||||
// Adjust position (move to front)
|
||||
newPos := pos
|
||||
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
|
||||
// Swap node positions
|
||||
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
|
||||
}
|
||||
|
||||
// Build new index char string
|
||||
if newPos != pos {
|
||||
n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
|
||||
n.indices[pos:pos+1] + // The index char we move
|
||||
n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
|
||||
}
|
||||
|
||||
return newPos
|
||||
}
|
||||
|
||||
// addRoute adds a node with the given handle to the path.
|
||||
// Not concurrency-safe!
|
||||
func (n *node) addRoute(path string, handlers ...HandlerFunc) {
|
||||
fullPath := path
|
||||
n.priority++
|
||||
|
||||
// Empty tree
|
||||
if len(n.path) == 0 && len(n.children) == 0 {
|
||||
n.insertChild(path, fullPath, handlers...)
|
||||
n.nType = root
|
||||
return
|
||||
}
|
||||
|
||||
parentFullPathIndex := 0
|
||||
|
||||
walk:
|
||||
for {
|
||||
// Find the longest common prefix.
|
||||
// This also implies that the common prefix contains no ':' or '*'
|
||||
// since the existing key can't contain those chars.
|
||||
i := longestCommonPrefix(path, n.path)
|
||||
|
||||
// Split edge
|
||||
if i < len(n.path) {
|
||||
child := node{
|
||||
path: n.path[i:],
|
||||
wildChild: n.wildChild,
|
||||
nType: static,
|
||||
indices: n.indices,
|
||||
children: n.children,
|
||||
handlers: n.handlers,
|
||||
priority: n.priority - 1,
|
||||
fullPath: n.fullPath,
|
||||
}
|
||||
|
||||
n.children = []*node{&child}
|
||||
// []byte for proper unicode char conversion, see #65
|
||||
n.indices = bytesconv.BytesToString([]byte{n.path[i]})
|
||||
n.path = path[:i]
|
||||
n.handlers = nil
|
||||
n.wildChild = false
|
||||
n.fullPath = fullPath[:parentFullPathIndex+i]
|
||||
}
|
||||
|
||||
// Make new node a child of this node
|
||||
if i < len(path) {
|
||||
path = path[i:]
|
||||
c := path[0]
|
||||
|
||||
// '/' after param
|
||||
if n.nType == param && c == '/' && len(n.children) == 1 {
|
||||
parentFullPathIndex += len(n.path)
|
||||
n = n.children[0]
|
||||
n.priority++
|
||||
continue walk
|
||||
}
|
||||
|
||||
// Check if a child with the next path byte exists
|
||||
for i, max := 0, len(n.indices); i < max; i++ {
|
||||
if c == n.indices[i] {
|
||||
parentFullPathIndex += len(n.path)
|
||||
i = n.incrementChildPrio(i)
|
||||
n = n.children[i]
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise insert it
|
||||
if c != ':' && c != '*' && n.nType != catchAll {
|
||||
// []byte for proper unicode char conversion, see #65
|
||||
n.indices += bytesconv.BytesToString([]byte{c})
|
||||
child := &node{
|
||||
fullPath: fullPath,
|
||||
}
|
||||
n.addChild(child)
|
||||
n.incrementChildPrio(len(n.indices) - 1)
|
||||
n = child
|
||||
} else if n.wildChild {
|
||||
// inserting a wildcard node, need to check if it conflicts with the existing wildcard
|
||||
n = n.children[len(n.children)-1]
|
||||
n.priority++
|
||||
|
||||
// Check if the wildcard matches
|
||||
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
|
||||
// Adding a child to a catchAll is not possible
|
||||
n.nType != catchAll &&
|
||||
// Check for longer wildcard, e.g. :name and :names
|
||||
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
|
||||
continue walk
|
||||
}
|
||||
|
||||
// Wildcard conflict
|
||||
pathSeg := path
|
||||
if n.nType != catchAll {
|
||||
pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
|
||||
}
|
||||
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
|
||||
panic("'" + pathSeg +
|
||||
"' in new path '" + fullPath +
|
||||
"' conflicts with existing wildcard '" + n.path +
|
||||
"' in existing prefix '" + prefix +
|
||||
"'")
|
||||
}
|
||||
|
||||
n.insertChild(path, fullPath, handlers...)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise add handle to current node
|
||||
if n.handlers != nil {
|
||||
panic("handlers are already registered for path '" + fullPath + "'")
|
||||
}
|
||||
n.handlers = handlers
|
||||
n.fullPath = fullPath
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Search for a wildcard segment and check the name for invalid characters.
|
||||
// Returns -1 as index, if no wildcard was found.
|
||||
func findWildcard(path string) (wildcard string, i int, valid bool) {
|
||||
// Find start
|
||||
for start, c := range []byte(path) {
|
||||
// A wildcard starts with ':' (param) or '*' (catch-all)
|
||||
if c != ':' && c != '*' {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find end and check for invalid characters
|
||||
valid = true
|
||||
for end, c := range []byte(path[start+1:]) {
|
||||
switch c {
|
||||
case '/':
|
||||
return path[start : start+1+end], start, valid
|
||||
case ':', '*':
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
return path[start:], start, valid
|
||||
}
|
||||
return "", -1, false
|
||||
}
|
||||
|
||||
func (n *node) insertChild(path string, fullPath string, handlers ...HandlerFunc) {
|
||||
for {
|
||||
// Find prefix until first wildcard
|
||||
wildcard, i, valid := findWildcard(path)
|
||||
if i < 0 { // No wildcard found
|
||||
break
|
||||
}
|
||||
|
||||
// The wildcard name must only contain one ':' or '*' character
|
||||
if !valid {
|
||||
panic("only one wildcard per path segment is allowed, has: '" +
|
||||
wildcard + "' in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
// check if the wildcard has a name
|
||||
if len(wildcard) < 2 {
|
||||
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
if wildcard[0] == ':' { // param
|
||||
if i > 0 {
|
||||
// Insert prefix before the current wildcard
|
||||
n.path = path[:i]
|
||||
path = path[i:]
|
||||
}
|
||||
|
||||
child := &node{
|
||||
nType: param,
|
||||
path: wildcard,
|
||||
fullPath: fullPath,
|
||||
}
|
||||
n.addChild(child)
|
||||
n.wildChild = true
|
||||
n = child
|
||||
n.priority++
|
||||
|
||||
// if the path doesn't end with the wildcard, then there
|
||||
// will be another subpath starting with '/'
|
||||
if len(wildcard) < len(path) {
|
||||
path = path[len(wildcard):]
|
||||
|
||||
child := &node{
|
||||
priority: 1,
|
||||
fullPath: fullPath,
|
||||
}
|
||||
n.addChild(child)
|
||||
n = child
|
||||
continue
|
||||
}
|
||||
|
||||
// Otherwise we're done. Insert the handle in the new leaf
|
||||
n.handlers = handlers
|
||||
return
|
||||
}
|
||||
|
||||
// catchAll
|
||||
if i+len(wildcard) != len(path) {
|
||||
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||||
pathSeg := ""
|
||||
if len(n.children) != 0 {
|
||||
pathSeg = strings.SplitN(n.children[0].path, "/", 2)[0]
|
||||
}
|
||||
panic("catch-all wildcard '" + path +
|
||||
"' in new path '" + fullPath +
|
||||
"' conflicts with existing path segment '" + pathSeg +
|
||||
"' in existing prefix '" + n.path + pathSeg +
|
||||
"'")
|
||||
}
|
||||
|
||||
// currently fixed width 1 for '/'
|
||||
i--
|
||||
if path[i] != '/' {
|
||||
panic("no / before catch-all in path '" + fullPath + "'")
|
||||
}
|
||||
|
||||
n.path = path[:i]
|
||||
|
||||
// First node: catchAll node with empty path
|
||||
child := &node{
|
||||
wildChild: true,
|
||||
nType: catchAll,
|
||||
fullPath: fullPath,
|
||||
}
|
||||
|
||||
n.addChild(child)
|
||||
n.indices = string('/')
|
||||
n = child
|
||||
n.priority++
|
||||
|
||||
// second node: node holding the variable
|
||||
child = &node{
|
||||
path: path[i:],
|
||||
nType: catchAll,
|
||||
handlers: handlers,
|
||||
priority: 1,
|
||||
fullPath: fullPath,
|
||||
}
|
||||
n.children = []*node{child}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// If no wildcard was found, simply insert the path and handle
|
||||
n.path = path
|
||||
n.handlers = handlers
|
||||
n.fullPath = fullPath
|
||||
}
|
||||
|
||||
// nodeValue holds return values of (*Node).getValue method
|
||||
type nodeValue struct {
|
||||
handlers []HandlerFunc
|
||||
params *Params
|
||||
tsr bool
|
||||
fullPath string
|
||||
}
|
||||
|
||||
type skippedNode struct {
|
||||
path string
|
||||
node *node
|
||||
paramsCount int16
|
||||
}
|
||||
|
||||
// Returns the handle registered with the given path (key). The values of
|
||||
// wildcards are saved to a map.
|
||||
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
|
||||
// made if a handle exists with an extra (without the) trailing slash for the
|
||||
// given path.
|
||||
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
|
||||
var globalParamsCount int16
|
||||
|
||||
walk: // Outer loop for walking the tree
|
||||
for {
|
||||
prefix := n.path
|
||||
if len(path) > len(prefix) {
|
||||
if path[:len(prefix)] == prefix {
|
||||
path = path[len(prefix):]
|
||||
|
||||
// Try all the non-wildcard children first by matching the indices
|
||||
idxc := path[0]
|
||||
for i, c := range []byte(n.indices) {
|
||||
if c == idxc {
|
||||
// strings.HasPrefix(n.children[len(n.children)-1].path, ":") == n.wildChild
|
||||
if n.wildChild {
|
||||
index := len(*skippedNodes)
|
||||
*skippedNodes = (*skippedNodes)[:index+1]
|
||||
(*skippedNodes)[index] = skippedNode{
|
||||
path: prefix + path,
|
||||
node: &node{
|
||||
path: n.path,
|
||||
wildChild: n.wildChild,
|
||||
nType: n.nType,
|
||||
priority: n.priority,
|
||||
children: n.children,
|
||||
handlers: n.handlers,
|
||||
fullPath: n.fullPath,
|
||||
},
|
||||
paramsCount: globalParamsCount,
|
||||
}
|
||||
}
|
||||
|
||||
n = n.children[i]
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
|
||||
if !n.wildChild {
|
||||
// If the path at the end of the loop is not equal to '/' and the current node has no child nodes
|
||||
// the current node needs to roll back to last valid skippedNode
|
||||
if path != "/" {
|
||||
for length := len(*skippedNodes); length > 0; length-- {
|
||||
skippedNode := (*skippedNodes)[length-1]
|
||||
*skippedNodes = (*skippedNodes)[:length-1]
|
||||
if strings.HasSuffix(skippedNode.path, path) {
|
||||
path = skippedNode.path
|
||||
n = skippedNode.node
|
||||
if value.params != nil {
|
||||
*value.params = (*value.params)[:skippedNode.paramsCount]
|
||||
}
|
||||
globalParamsCount = skippedNode.paramsCount
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found.
|
||||
// We can recommend to redirect to the same URL without a
|
||||
// trailing slash if a leaf exists for that path.
|
||||
value.tsr = path == "/" && n.handlers != nil
|
||||
return value
|
||||
}
|
||||
|
||||
// Handle wildcard child, which is always at the end of the array
|
||||
n = n.children[len(n.children)-1]
|
||||
globalParamsCount++
|
||||
|
||||
switch n.nType {
|
||||
case param:
|
||||
// fix truncate the parameter
|
||||
// tree_test.go line: 204
|
||||
|
||||
// Find param end (either '/' or path end)
|
||||
end := 0
|
||||
for end < len(path) && path[end] != '/' {
|
||||
end++
|
||||
}
|
||||
|
||||
// Save param value
|
||||
if params != nil {
|
||||
// Preallocate capacity if necessary
|
||||
if cap(*params) < int(globalParamsCount) {
|
||||
newParams := make(Params, len(*params), globalParamsCount)
|
||||
copy(newParams, *params)
|
||||
*params = newParams
|
||||
}
|
||||
|
||||
if value.params == nil {
|
||||
value.params = params
|
||||
}
|
||||
// Expand slice within preallocated capacity
|
||||
i := len(*value.params)
|
||||
*value.params = (*value.params)[:i+1]
|
||||
val := path[:end]
|
||||
if unescape {
|
||||
if v, err := url.QueryUnescape(val); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
(*value.params)[i] = Param{
|
||||
Key: n.path[1:],
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
|
||||
// we need to go deeper!
|
||||
if end < len(path) {
|
||||
if len(n.children) > 0 {
|
||||
path = path[end:]
|
||||
n = n.children[0]
|
||||
continue walk
|
||||
}
|
||||
|
||||
// ... but we can't
|
||||
value.tsr = len(path) == end+1
|
||||
return value
|
||||
}
|
||||
|
||||
if value.handlers = n.handlers; value.handlers != nil {
|
||||
value.fullPath = n.fullPath
|
||||
return value
|
||||
}
|
||||
if len(n.children) == 1 {
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for TSR recommendation
|
||||
n = n.children[0]
|
||||
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
|
||||
}
|
||||
return value
|
||||
|
||||
case catchAll:
|
||||
// Save param value
|
||||
if params != nil {
|
||||
// Preallocate capacity if necessary
|
||||
if cap(*params) < int(globalParamsCount) {
|
||||
newParams := make(Params, len(*params), globalParamsCount)
|
||||
copy(newParams, *params)
|
||||
*params = newParams
|
||||
}
|
||||
|
||||
if value.params == nil {
|
||||
value.params = params
|
||||
}
|
||||
// Expand slice within preallocated capacity
|
||||
i := len(*value.params)
|
||||
*value.params = (*value.params)[:i+1]
|
||||
val := path
|
||||
if unescape {
|
||||
if v, err := url.QueryUnescape(path); err == nil {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
(*value.params)[i] = Param{
|
||||
Key: n.path[2:],
|
||||
Value: val,
|
||||
}
|
||||
}
|
||||
|
||||
value.handlers = n.handlers
|
||||
value.fullPath = n.fullPath
|
||||
return value
|
||||
|
||||
default:
|
||||
panic("invalid node type")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if path == prefix {
|
||||
// If the current path does not equal '/' and the node does not have a registered handle and the most recently matched node has a child node
|
||||
// the current node needs to roll back to last valid skippedNode
|
||||
if n.handlers == nil && path != "/" {
|
||||
for length := len(*skippedNodes); length > 0; length-- {
|
||||
skippedNode := (*skippedNodes)[length-1]
|
||||
*skippedNodes = (*skippedNodes)[:length-1]
|
||||
if strings.HasSuffix(skippedNode.path, path) {
|
||||
path = skippedNode.path
|
||||
n = skippedNode.node
|
||||
if value.params != nil {
|
||||
*value.params = (*value.params)[:skippedNode.paramsCount]
|
||||
}
|
||||
globalParamsCount = skippedNode.paramsCount
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
// n = latestNode.children[len(latestNode.children)-1]
|
||||
}
|
||||
// We should have reached the node containing the handle.
|
||||
// Check if this node has a handle registered.
|
||||
if value.handlers = n.handlers; value.handlers != nil {
|
||||
value.fullPath = n.fullPath
|
||||
return value
|
||||
}
|
||||
|
||||
// If there is no handle for this route, but this route has a
|
||||
// wildcard child, there must be a handle for this path with an
|
||||
// additional trailing slash
|
||||
if path == "/" && n.wildChild && n.nType != root {
|
||||
value.tsr = true
|
||||
return value
|
||||
}
|
||||
|
||||
if path == "/" && n.nType == static {
|
||||
value.tsr = true
|
||||
return value
|
||||
}
|
||||
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists for trailing slash recommendation
|
||||
for i, c := range []byte(n.indices) {
|
||||
if c == '/' {
|
||||
n = n.children[i]
|
||||
value.tsr = (len(n.path) == 1 && n.handlers != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handlers != nil)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// Nothing found. We can recommend to redirect to the same URL with an
|
||||
// extra trailing slash if a leaf exists for that path
|
||||
value.tsr = path == "/" ||
|
||||
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&
|
||||
path == prefix[:len(prefix)-1] && n.handlers != nil)
|
||||
|
||||
// roll back to last valid skippedNode
|
||||
if !value.tsr && path != "/" {
|
||||
for length := len(*skippedNodes); length > 0; length-- {
|
||||
skippedNode := (*skippedNodes)[length-1]
|
||||
*skippedNodes = (*skippedNodes)[:length-1]
|
||||
if strings.HasSuffix(skippedNode.path, path) {
|
||||
path = skippedNode.path
|
||||
n = skippedNode.node
|
||||
if value.params != nil {
|
||||
*value.params = (*value.params)[:skippedNode.paramsCount]
|
||||
}
|
||||
globalParamsCount = skippedNode.paramsCount
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// Makes a case-insensitive lookup of the given path and tries to find a handler.
|
||||
// It can optionally also fix trailing slashes.
|
||||
// It returns the case-corrected path and a bool indicating whether the lookup
|
||||
// was successful.
|
||||
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
|
||||
const stackBufSize = 128
|
||||
|
||||
// Use a static sized buffer on the stack in the common case.
|
||||
// If the path is too long, allocate a buffer on the heap instead.
|
||||
buf := make([]byte, 0, stackBufSize)
|
||||
if length := len(path) + 1; length > stackBufSize {
|
||||
buf = make([]byte, 0, length)
|
||||
}
|
||||
|
||||
ciPath := n.findCaseInsensitivePathRec(
|
||||
path,
|
||||
buf, // Preallocate enough memory for new path
|
||||
[4]byte{}, // Empty rune buffer
|
||||
fixTrailingSlash,
|
||||
)
|
||||
|
||||
return ciPath, ciPath != nil
|
||||
}
|
||||
|
||||
// Shift bytes in array by n bytes left
|
||||
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
|
||||
switch n {
|
||||
case 0:
|
||||
return rb
|
||||
case 1:
|
||||
return [4]byte{rb[1], rb[2], rb[3], 0}
|
||||
case 2:
|
||||
return [4]byte{rb[2], rb[3]}
|
||||
case 3:
|
||||
return [4]byte{rb[3]}
|
||||
default:
|
||||
return [4]byte{}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath
|
||||
func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
|
||||
npLen := len(n.path)
|
||||
|
||||
walk: // Outer loop for walking the tree
|
||||
for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
|
||||
// Add common prefix to result
|
||||
oldPath := path
|
||||
path = path[npLen:]
|
||||
ciPath = append(ciPath, n.path...)
|
||||
|
||||
if len(path) == 0 {
|
||||
// We should have reached the node containing the handle.
|
||||
// Check if this node has a handle registered.
|
||||
if n.handlers != nil {
|
||||
return ciPath
|
||||
}
|
||||
|
||||
// No handle found.
|
||||
// Try to fix the path by adding a trailing slash
|
||||
if fixTrailingSlash {
|
||||
for i, c := range []byte(n.indices) {
|
||||
if c == '/' {
|
||||
n = n.children[i]
|
||||
if (len(n.path) == 1 && n.handlers != nil) ||
|
||||
(n.nType == catchAll && n.children[0].handlers != nil) {
|
||||
return append(ciPath, '/')
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *_node) travel(list *([]*_node)) {
|
||||
if n.pattern != "" {
|
||||
*list = append(*list, n)
|
||||
}
|
||||
for _, child := range n.children {
|
||||
child.travel(list)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *_node) matchChild(part string) *_node {
|
||||
for _, child := range n.children {
|
||||
if child.part == part || child.isWild {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *_node) matchChildren(part string) []*_node {
|
||||
nodes := make([]*_node, 0)
|
||||
for _, child := range n.children {
|
||||
if child.part == part || child.isWild {
|
||||
nodes = append(nodes, child)
|
||||
// If this node does not have a wildcard (param or catchAll) child,
|
||||
// we can just look up the next child node and continue to walk down
|
||||
// the tree
|
||||
if !n.wildChild {
|
||||
// Skip rune bytes already processed
|
||||
rb = shiftNRuneBytes(rb, npLen)
|
||||
|
||||
if rb[0] != 0 {
|
||||
// Old rune not finished
|
||||
idxc := rb[0]
|
||||
for i, c := range []byte(n.indices) {
|
||||
if c == idxc {
|
||||
// continue with child node
|
||||
n = n.children[i]
|
||||
npLen = len(n.path)
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
} else {
|
||||
// Process a new rune
|
||||
var rv rune
|
||||
|
||||
// Find rune start.
|
||||
// Runes are up to 4 byte long,
|
||||
// -4 would definitely be another rune.
|
||||
var off int
|
||||
for max := min(npLen, 3); off < max; off++ {
|
||||
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
|
||||
// read rune from cached path
|
||||
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate lowercase bytes of current rune
|
||||
lo := unicode.ToLower(rv)
|
||||
utf8.EncodeRune(rb[:], lo)
|
||||
|
||||
// Skip already processed bytes
|
||||
rb = shiftNRuneBytes(rb, off)
|
||||
|
||||
idxc := rb[0]
|
||||
for i, c := range []byte(n.indices) {
|
||||
// Lowercase matches
|
||||
if c == idxc {
|
||||
// must use a recursive approach since both the
|
||||
// uppercase byte and the lowercase byte might exist
|
||||
// as an index
|
||||
if out := n.children[i].findCaseInsensitivePathRec(
|
||||
path, ciPath, rb, fixTrailingSlash,
|
||||
); out != nil {
|
||||
return out
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If we found no match, the same for the uppercase rune,
|
||||
// if it differs
|
||||
if up := unicode.ToUpper(rv); up != lo {
|
||||
utf8.EncodeRune(rb[:], up)
|
||||
rb = shiftNRuneBytes(rb, off)
|
||||
|
||||
idxc := rb[0]
|
||||
for i, c := range []byte(n.indices) {
|
||||
// Uppercase matches
|
||||
if c == idxc {
|
||||
// Continue with child node
|
||||
n = n.children[i]
|
||||
npLen = len(n.path)
|
||||
continue walk
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found. We can recommend to redirect to the same URL
|
||||
// without a trailing slash if a leaf exists for that path
|
||||
if fixTrailingSlash && path == "/" && n.handlers != nil {
|
||||
return ciPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
n = n.children[0]
|
||||
switch n.nType {
|
||||
case param:
|
||||
// Find param end (either '/' or path end)
|
||||
end := 0
|
||||
for end < len(path) && path[end] != '/' {
|
||||
end++
|
||||
}
|
||||
|
||||
// Add param value to case insensitive path
|
||||
ciPath = append(ciPath, path[:end]...)
|
||||
|
||||
// We need to go deeper!
|
||||
if end < len(path) {
|
||||
if len(n.children) > 0 {
|
||||
// Continue with child node
|
||||
n = n.children[0]
|
||||
npLen = len(n.path)
|
||||
path = path[end:]
|
||||
continue
|
||||
}
|
||||
|
||||
// ... but we can't
|
||||
if fixTrailingSlash && len(path) == end+1 {
|
||||
return ciPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if n.handlers != nil {
|
||||
return ciPath
|
||||
}
|
||||
|
||||
if fixTrailingSlash && len(n.children) == 1 {
|
||||
// No handle found. Check if a handle for this path + a
|
||||
// trailing slash exists
|
||||
n = n.children[0]
|
||||
if n.path == "/" && n.handlers != nil {
|
||||
return append(ciPath, '/')
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
case catchAll:
|
||||
return append(ciPath, path...)
|
||||
|
||||
default:
|
||||
panic("invalid node type")
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing found.
|
||||
// Try to fix the path by adding / removing a trailing slash
|
||||
if fixTrailingSlash {
|
||||
if path == "/" {
|
||||
return ciPath
|
||||
}
|
||||
if len(path)+1 == npLen && n.path[len(path)] == '/' &&
|
||||
strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handlers != nil {
|
||||
return append(ciPath, n.path...)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
137
util.go
137
util.go
@ -65,3 +65,140 @@ func parseToStruct(aliasTag string, out interface{}, data map[string][]string) e
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func elsePanic(guard bool, text string) {
|
||||
if !guard {
|
||||
panic(text)
|
||||
}
|
||||
}
|
||||
|
||||
func cleanPath(p string) string {
|
||||
const stackBufSize = 128
|
||||
// Turn empty string into "/"
|
||||
if p == "" {
|
||||
return "/"
|
||||
}
|
||||
|
||||
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||
// If a larger buffer is required, it gets allocated dynamically.
|
||||
buf := make([]byte, 0, stackBufSize)
|
||||
|
||||
n := len(p)
|
||||
|
||||
// Invariants:
|
||||
// reading from path; r is index of next byte to process.
|
||||
// writing to buf; w is index of next byte to write.
|
||||
|
||||
// path must start with '/'
|
||||
r := 1
|
||||
w := 1
|
||||
|
||||
if p[0] != '/' {
|
||||
r = 0
|
||||
|
||||
if n+1 > stackBufSize {
|
||||
buf = make([]byte, n+1)
|
||||
} else {
|
||||
buf = buf[:n+1]
|
||||
}
|
||||
buf[0] = '/'
|
||||
}
|
||||
|
||||
trailing := n > 1 && p[n-1] == '/'
|
||||
|
||||
// A bit more clunky without a 'lazybuf' like the path package, but the loop
|
||||
// gets completely inlined (bufApp calls).
|
||||
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
|
||||
// calls (except make, if needed).
|
||||
|
||||
for r < n {
|
||||
switch {
|
||||
case p[r] == '/':
|
||||
// empty path element, trailing slash is added after the end
|
||||
r++
|
||||
|
||||
case p[r] == '.' && r+1 == n:
|
||||
trailing = true
|
||||
r++
|
||||
|
||||
case p[r] == '.' && p[r+1] == '/':
|
||||
// . element
|
||||
r += 2
|
||||
|
||||
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
|
||||
// .. element: remove to last /
|
||||
r += 3
|
||||
|
||||
if w > 1 {
|
||||
// can backtrack
|
||||
w--
|
||||
|
||||
if len(buf) == 0 {
|
||||
for w > 1 && p[w] != '/' {
|
||||
w--
|
||||
}
|
||||
} else {
|
||||
for w > 1 && buf[w] != '/' {
|
||||
w--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// Real path element.
|
||||
// Add slash if needed
|
||||
if w > 1 {
|
||||
bufApp(&buf, p, w, '/')
|
||||
w++
|
||||
}
|
||||
|
||||
// Copy element
|
||||
for r < n && p[r] != '/' {
|
||||
bufApp(&buf, p, w, p[r])
|
||||
w++
|
||||
r++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-append trailing slash
|
||||
if trailing && w > 1 {
|
||||
bufApp(&buf, p, w, '/')
|
||||
w++
|
||||
}
|
||||
|
||||
// If the original string was not modified (or only shortened at the end),
|
||||
// return the respective substring of the original string.
|
||||
// Otherwise return a new string from the buffer.
|
||||
if len(buf) == 0 {
|
||||
return p[:w]
|
||||
}
|
||||
return string(buf[:w])
|
||||
}
|
||||
|
||||
// Internal helper to lazily create a buffer if necessary.
|
||||
// Calls to this function get inlined.
|
||||
func bufApp(buf *[]byte, s string, w int, c byte) {
|
||||
b := *buf
|
||||
if len(b) == 0 {
|
||||
// No modification of the original string so far.
|
||||
// If the next character is the same as in the original string, we do
|
||||
// not yet have to allocate a buffer.
|
||||
if s[w] == c {
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise use either the stack buffer, if it is large enough, or
|
||||
// allocate a new buffer on the heap, and copy all previous characters.
|
||||
length := len(s)
|
||||
if length > cap(b) {
|
||||
*buf = make([]byte, length)
|
||||
} else {
|
||||
*buf = (*buf)[:length]
|
||||
}
|
||||
b = *buf
|
||||
|
||||
copy(b, s[:w])
|
||||
}
|
||||
b[w] = c
|
||||
}
|
||||
|
@ -1,49 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/loveuer/nf"
|
||||
"github.com/loveuer/nf/nft/resp"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := nf.New(nf.Config{EnableNotImplementHandler: true})
|
||||
|
||||
app.Get("/hello/:name", func(c *nf.Ctx) error {
|
||||
name := c.Param("name")
|
||||
return c.JSON(nf.Map{"status": 200, "data": "hello, " + name})
|
||||
})
|
||||
app.Get("/not_impl")
|
||||
app.Patch("/world", func(c *nf.Ctx) error {
|
||||
time.Sleep(5 * time.Second)
|
||||
c.Status(404)
|
||||
return c.JSON(nf.Map{"method": c.Method, "status": c.StatusCode})
|
||||
})
|
||||
app.Get("/error", func(c *nf.Ctx) error {
|
||||
return resp.RespError(c, resp.NewError(404, "not found", errors.New("NNNot Found"), nil))
|
||||
})
|
||||
app.Post("/data", func(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
var (
|
||||
err error
|
||||
req = new(Req)
|
||||
rm = make(map[string]any)
|
||||
)
|
||||
|
||||
if err = c.BodyParser(req); err != nil {
|
||||
return c.JSON(nf.Map{"status": 400, "msg": err.Error()})
|
||||
}
|
||||
|
||||
if err = c.BodyParser(&rm); err != nil {
|
||||
return c.JSON(nf.Map{"status": 400, "msg": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(nf.Map{"status": 200, "data": req, "map": rm})
|
||||
api := app.Group("/api")
|
||||
api.Get("/1", func(c *nf.Ctx) error {
|
||||
return c.SendString("nice")
|
||||
})
|
||||
|
||||
log.Fatal(app.Run(":80"))
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := nf.New(nf.Config{BodyLimit: -1})
|
||||
app := nf.New(nf.Config{BodyLimit: 30})
|
||||
|
||||
app.Post("/data", func(c *nf.Ctx) error {
|
||||
type Req struct {
|
||||
|
@ -11,19 +11,17 @@ func main() {
|
||||
app.Get("/hello", func(c *nf.Ctx) error {
|
||||
return c.SendString("world")
|
||||
})
|
||||
app.Get("/panic", func(c *nf.Ctx) error {
|
||||
panic("panic")
|
||||
})
|
||||
|
||||
app.Use(ml())
|
||||
|
||||
log.Fatal(app.Run(":7777"))
|
||||
log.Fatal(app.Run(":80"))
|
||||
}
|
||||
|
||||
func ml() nf.HandlerFunc {
|
||||
return func(c *nf.Ctx) error {
|
||||
log.Printf("[ML] [%s] - [%s]", c.Method, c.Path())
|
||||
index := []byte(`<h1>my not found</h1>`)
|
||||
c.Set("Content-Type", "text/html")
|
||||
c.Status(403)
|
||||
_, err := c.Write(index)
|
||||
return err
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ func main() {
|
||||
|
||||
app.Get("/nice", h1, h2)
|
||||
|
||||
log.Fatal(app.Run(":3333"))
|
||||
log.Fatal(app.Run(":80"))
|
||||
}
|
||||
|
||||
func h1(c *nf.Ctx) error {
|
||||
@ -19,7 +19,8 @@ func h1(c *nf.Ctx) error {
|
||||
return c.JSON(nf.Map{"status": 201, "msg": "nice to meet you"})
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
//return c.Next()
|
||||
return nil
|
||||
}
|
||||
|
||||
func h2(c *nf.Ctx) error {
|
||||
|
@ -7,7 +7,7 @@ import (
|
||||
|
||||
func main() {
|
||||
app := nf.New(nf.Config{
|
||||
DisableRecover: true,
|
||||
DisableRecover: false,
|
||||
})
|
||||
|
||||
app.Get("/hello/:name", func(c *nf.Ctx) error {
|
||||
|
Loading…
x
Reference in New Issue
Block a user