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"
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/loveuer/nf/internal/bytesconv"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"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 {
|
type App struct {
|
||||||
*RouterGroup
|
RouterGroup
|
||||||
config *Config
|
config *Config
|
||||||
router *router
|
|
||||||
groups []*RouterGroup
|
groups []*RouterGroup
|
||||||
server *http.Server
|
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) {
|
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 err = c.verify(); err != nil {
|
||||||
if strings.HasPrefix(request.URL.Path, group.prefix) {
|
if errors.As(err, nfe) {
|
||||||
c.handlers = append(c.handlers, group.middlewares...)
|
_ = c.Status(nfe.Status).SendString(nfe.Msg)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.router.handle(c); err != nil {
|
_ = c.Status(500).SendString(err.Error())
|
||||||
var ne = &Err{}
|
return
|
||||||
|
|
||||||
if errors.As(err, ne) {
|
|
||||||
writer.WriteHeader(ne.Status)
|
|
||||||
} else {
|
|
||||||
writer.WriteHeader(500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = writer.Write([]byte(err.Error()))
|
a.handleHTTPRequest(c)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) run(ln net.Listener) error {
|
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 {
|
func (a *App) Shutdown(ctx context.Context) error {
|
||||||
return a.server.Shutdown(ctx)
|
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 {
|
type Ctx struct {
|
||||||
|
writermem responseWriter
|
||||||
// origin objects
|
// origin objects
|
||||||
writer http.ResponseWriter
|
writer http.ResponseWriter
|
||||||
Request *http.Request
|
Request *http.Request
|
||||||
// request info
|
// request info
|
||||||
path string
|
path string
|
||||||
Method string
|
method string
|
||||||
// response info
|
// response info
|
||||||
StatusCode int
|
StatusCode int
|
||||||
|
|
||||||
app *App
|
app *App
|
||||||
params map[string]string
|
params *Params
|
||||||
index int
|
index int
|
||||||
handlers []HandlerFunc
|
handlers []HandlerFunc
|
||||||
locals map[string]interface{}
|
locals map[string]interface{}
|
||||||
|
skippedNodes *[]skippedNode
|
||||||
|
fullPath string
|
||||||
|
Params Params
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContext(app *App, writer http.ResponseWriter, request *http.Request) *Ctx {
|
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,
|
writer: writer,
|
||||||
|
writermem: responseWriter{},
|
||||||
Request: request,
|
Request: request,
|
||||||
path: request.URL.Path,
|
path: request.URL.Path,
|
||||||
Method: request.Method,
|
method: request.Method,
|
||||||
StatusCode: 200,
|
StatusCode: 200,
|
||||||
|
|
||||||
app: app,
|
app: app,
|
||||||
index: -1,
|
index: -1,
|
||||||
locals: map[string]interface{}{},
|
locals: map[string]interface{}{},
|
||||||
handlers: make([]HandlerFunc, 0),
|
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{} {
|
func (c *Ctx) Locals(key string, value ...interface{}) interface{} {
|
||||||
@ -52,6 +71,16 @@ func (c *Ctx) Locals(key string, value ...interface{}) interface{} {
|
|||||||
return data
|
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 {
|
func (c *Ctx) Path(overWrite ...string) string {
|
||||||
path := c.Request.URL.Path
|
path := c.Request.URL.Path
|
||||||
if len(overWrite) > 0 && overWrite[0] != "" {
|
if len(overWrite) > 0 && overWrite[0] != "" {
|
||||||
@ -83,11 +112,17 @@ func (c *Ctx) Next() error {
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if c.index < len(c.handlers) {
|
for c.index < len(c.handlers) {
|
||||||
err = c.handlers[c.index](c)
|
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 {
|
func (c *Ctx) Param(key string) string {
|
||||||
return c.params[key]
|
return c.Params.ByName(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctx) Form(key string) string {
|
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 {
|
func (c *Ctx) Status(code int) *Ctx {
|
||||||
c.StatusCode = code
|
c.writermem.WriteHeader(code)
|
||||||
c.writer.WriteHeader(code)
|
c.StatusCode = c.writermem.status
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctx) Set(key string, value string) {
|
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) {
|
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 {
|
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 {
|
func (c *Ctx) JSON(data interface{}) error {
|
||||||
c.SetHeader("Content-Type", MIMEApplicationJSON)
|
c.SetHeader("Content-Type", MIMEApplicationJSON)
|
||||||
|
|
||||||
encoder := json.NewEncoder(c.writer)
|
encoder := json.NewEncoder(&c.writermem)
|
||||||
|
|
||||||
if err := encoder.Encode(data); err != nil {
|
if err := encoder.Encode(data); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -237,7 +272,7 @@ func (c *Ctx) RawWriter() http.ResponseWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ctx) Write(data []byte) (int, error) {
|
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 {
|
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
|
type HandlerFunc func(*Ctx) error
|
||||||
|
|
||||||
func ToDoHandler(c *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 {
|
} else {
|
||||||
os.Stderr.WriteString(fmt.Sprintf("recovered from panic: %v\n", r))
|
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))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -44,10 +47,10 @@ func NewLogger() HandlerFunc {
|
|||||||
|
|
||||||
if num > 1000 {
|
if num > 1000 {
|
||||||
num = num / 1000
|
num = num / 1000
|
||||||
unit = " s"
|
unit = "s"
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%v %s", num, unit)
|
return fmt.Sprintf("%3d %2s", num, unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(c *Ctx) error {
|
return func(c *Ctx) error {
|
||||||
|
32
nf.go
32
nf.go
@ -3,11 +3,14 @@ package nf
|
|||||||
const (
|
const (
|
||||||
banner = " _ _ _ ___ _ \n | \\| |___| |_ | __|__ _ _ _ _ __| |\n | .` / _ \\ _| | _/ _ \\ || | ' \\/ _` |\n |_|\\_\\___/\\__| |_|\\___/\\_,_|_||_\\__,_|\n "
|
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>"
|
_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 Map map[string]interface{}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
DisableMessagePrint bool `json:"-"`
|
||||||
// Default: 4 * 1024 * 1024
|
// Default: 4 * 1024 * 1024
|
||||||
BodyLimit int64 `json:"-"`
|
BodyLimit int64 `json:"-"`
|
||||||
|
|
||||||
@ -19,8 +22,9 @@ type Config struct {
|
|||||||
DisableRecover bool `json:"-"`
|
DisableRecover bool `json:"-"`
|
||||||
DisableHttpErrorLog bool `json:"-"`
|
DisableHttpErrorLog bool `json:"-"`
|
||||||
|
|
||||||
EnableNotImplementHandler bool `json:"-"`
|
//EnableNotImplementHandler bool `json:"-"`
|
||||||
NotFoundHandler HandlerFunc `json:"-"`
|
NotFoundHandler HandlerFunc `json:"-"`
|
||||||
|
MethodNotAllowedHandler HandlerFunc `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -31,16 +35,33 @@ var (
|
|||||||
_, err := c.Status(404).Write([]byte(_404))
|
_, err := c.Status(404).Write([]byte(_404))
|
||||||
return err
|
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 {
|
func New(config ...Config) *App {
|
||||||
app := &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 {
|
if len(config) > 0 {
|
||||||
app.config = &config[0]
|
app.config = &config[0]
|
||||||
|
|
||||||
if app.config.BodyLimit == 0 {
|
if app.config.BodyLimit == 0 {
|
||||||
app.config.BodyLimit = defaultConfig.BodyLimit
|
app.config.BodyLimit = defaultConfig.BodyLimit
|
||||||
}
|
}
|
||||||
@ -49,12 +70,15 @@ func New(config ...Config) *App {
|
|||||||
app.config.NotFoundHandler = defaultConfig.NotFoundHandler
|
app.config.NotFoundHandler = defaultConfig.NotFoundHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if app.config.MethodNotAllowedHandler == nil {
|
||||||
|
app.config.MethodNotAllowedHandler = defaultConfig.MethodNotAllowedHandler
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
app.config = defaultConfig
|
app.config = defaultConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
app.RouterGroup = &RouterGroup{app: app, prefix: ""}
|
app.RouterGroup.app = app
|
||||||
app.groups = []*RouterGroup{app.RouterGroup}
|
|
||||||
|
|
||||||
if !app.config.DisableLogger {
|
if !app.config.DisableLogger {
|
||||||
app.Use(NewLogger())
|
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
|
||||||
|
}
|
909
tree.go
909
tree.go
@ -1,76 +1,891 @@
|
|||||||
package nf
|
package nf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/loveuer/nf/internal/bytesconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type _node struct {
|
var (
|
||||||
pattern string
|
strColon = []byte(":")
|
||||||
part string
|
strStar = []byte("*")
|
||||||
children []*_node
|
strSlash = []byte("/")
|
||||||
isWild bool
|
)
|
||||||
|
|
||||||
|
// 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) {
|
// Params is a Param-slice, as returned by the router.
|
||||||
if len(parts) == height {
|
// The slice is ordered, the first URL parameter is also the first slice value.
|
||||||
n.pattern = pattern
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
type methodTree struct {
|
||||||
|
method string
|
||||||
|
root *node
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func countSections(path string) uint16 {
|
||||||
|
s := bytesconv.StringToBytes(path)
|
||||||
|
return uint16(bytes.Count(s, strSlash))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
part := parts[height]
|
parentFullPathIndex := 0
|
||||||
child := n.matchChild(part)
|
|
||||||
if child == nil {
|
walk:
|
||||||
child = &_node{part: part, isWild: part[0] == ':' || part[0] == '*'}
|
for {
|
||||||
n.children = append(n.children, child)
|
// 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
|
||||||
}
|
}
|
||||||
child.insert(pattern, parts, height+1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *_node) search(parts []string, height int) *_node {
|
// Search for a wildcard segment and check the name for invalid characters.
|
||||||
if len(parts) == height || strings.HasPrefix(n.part, "*") {
|
// Returns -1 as index, if no wildcard was found.
|
||||||
if n.pattern == "" {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
return n
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
part := parts[height]
|
// If this node does not have a wildcard (param or catchAll) child,
|
||||||
children := n.matchChildren(part)
|
// 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)
|
||||||
|
|
||||||
for _, child := range children {
|
if rb[0] != 0 {
|
||||||
result := child.search(parts, height+1)
|
// Old rune not finished
|
||||||
if result != nil {
|
idxc := rb[0]
|
||||||
return result
|
for i, c := range []byte(n.indices) {
|
||||||
|
if c == idxc {
|
||||||
|
// continue with child node
|
||||||
|
n = n.children[i]
|
||||||
|
npLen = len(n.path)
|
||||||
|
continue walk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
func (n *_node) travel(list *([]*_node)) {
|
case catchAll:
|
||||||
if n.pattern != "" {
|
return append(ciPath, path...)
|
||||||
*list = append(*list, n)
|
|
||||||
}
|
|
||||||
for _, child := range n.children {
|
|
||||||
child.travel(list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *_node) matchChild(part string) *_node {
|
default:
|
||||||
for _, child := range n.children {
|
panic("invalid node type")
|
||||||
if child.part == part || child.isWild {
|
}
|
||||||
return child
|
}
|
||||||
|
|
||||||
|
// 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
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
137
util.go
137
util.go
@ -65,3 +65,140 @@ func parseToStruct(aliasTag string, out interface{}, data map[string][]string) e
|
|||||||
|
|
||||||
return nil
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"github.com/loveuer/nf"
|
"github.com/loveuer/nf"
|
||||||
"github.com/loveuer/nf/nft/resp"
|
|
||||||
"log"
|
"log"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := nf.New(nf.Config{EnableNotImplementHandler: true})
|
app := nf.New(nf.Config{EnableNotImplementHandler: true})
|
||||||
|
|
||||||
app.Get("/hello/:name", func(c *nf.Ctx) error {
|
api := app.Group("/api")
|
||||||
name := c.Param("name")
|
api.Get("/1", func(c *nf.Ctx) error {
|
||||||
return c.JSON(nf.Map{"status": 200, "data": "hello, " + name})
|
return c.SendString("nice")
|
||||||
})
|
|
||||||
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})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Fatal(app.Run(":80"))
|
log.Fatal(app.Run(":80"))
|
||||||
|
@ -6,7 +6,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := nf.New(nf.Config{BodyLimit: -1})
|
app := nf.New(nf.Config{BodyLimit: 30})
|
||||||
|
|
||||||
app.Post("/data", func(c *nf.Ctx) error {
|
app.Post("/data", func(c *nf.Ctx) error {
|
||||||
type Req struct {
|
type Req struct {
|
||||||
|
@ -11,19 +11,17 @@ func main() {
|
|||||||
app.Get("/hello", func(c *nf.Ctx) error {
|
app.Get("/hello", func(c *nf.Ctx) error {
|
||||||
return c.SendString("world")
|
return c.SendString("world")
|
||||||
})
|
})
|
||||||
app.Get("/panic", func(c *nf.Ctx) error {
|
|
||||||
panic("panic")
|
|
||||||
})
|
|
||||||
app.Use(ml())
|
app.Use(ml())
|
||||||
|
|
||||||
log.Fatal(app.Run(":7777"))
|
log.Fatal(app.Run(":80"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ml() nf.HandlerFunc {
|
func ml() nf.HandlerFunc {
|
||||||
return func(c *nf.Ctx) error {
|
return func(c *nf.Ctx) error {
|
||||||
log.Printf("[ML] [%s] - [%s]", c.Method, c.Path())
|
|
||||||
index := []byte(`<h1>my not found</h1>`)
|
index := []byte(`<h1>my not found</h1>`)
|
||||||
c.Set("Content-Type", "text/html")
|
c.Set("Content-Type", "text/html")
|
||||||
|
c.Status(403)
|
||||||
_, err := c.Write(index)
|
_, err := c.Write(index)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ func main() {
|
|||||||
|
|
||||||
app.Get("/nice", h1, h2)
|
app.Get("/nice", h1, h2)
|
||||||
|
|
||||||
log.Fatal(app.Run(":3333"))
|
log.Fatal(app.Run(":80"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func h1(c *nf.Ctx) error {
|
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.JSON(nf.Map{"status": 201, "msg": "nice to meet you"})
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Next()
|
//return c.Next()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func h2(c *nf.Ctx) error {
|
func h2(c *nf.Ctx) error {
|
||||||
|
@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := nf.New(nf.Config{
|
app := nf.New(nf.Config{
|
||||||
DisableRecover: true,
|
DisableRecover: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
app.Get("/hello/:name", func(c *nf.Ctx) error {
|
app.Get("/hello/:name", func(c *nf.Ctx) error {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user