package fiber
import (
"fmt"
"html"
"sort"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/gofiber/fiber/v2/utils"
"github.com/valyala/fasthttp"
)
type Router interface {
Use (args ...interface {}) Router
Get (path string , handlers ...Handler ) Router
Head (path string , handlers ...Handler ) Router
Post (path string , handlers ...Handler ) Router
Put (path string , handlers ...Handler ) Router
Delete (path string , handlers ...Handler ) Router
Connect (path string , handlers ...Handler ) Router
Options (path string , handlers ...Handler ) Router
Trace (path string , handlers ...Handler ) Router
Patch (path string , handlers ...Handler ) Router
Add (method, path string , handlers ...Handler ) Router
Static (prefix, root string , config ...Static ) Router
All (path string , handlers ...Handler ) Router
Group (prefix string , handlers ...Handler ) Router
Route (prefix string , fn func (router Router ), name ...string ) Router
Mount (prefix string , fiber *App ) Router
Name (name string ) Router
}
type Route struct {
pos uint32
use bool
mount bool
star bool
root bool
path string
routeParser routeParser
group *Group
Method string `json:"method"`
Name string `json:"name"`
Path string `json:"path"`
Params []string `json:"params"`
Handlers []Handler `json:"-"`
}
func (r *Route ) match (detectionPath , path string , params *[maxParams ]string ) bool {
if r .root && detectionPath == "/" {
return true
} else if r .star {
if len (path ) > 1 {
params [0 ] = path [1 :]
} else {
params [0 ] = ""
}
return true
}
if len (r .Params ) > 0 {
if match := r .routeParser .getMatch (detectionPath , path , params , r .use ); match {
return match
}
}
if r .use {
if r .root || strings .HasPrefix (detectionPath , r .path ) {
return true
}
} else if len (r .path ) == len (detectionPath ) && r .path == detectionPath {
return true
}
return false
}
func (app *App ) next (c *Ctx ) (bool , error ) {
tree , ok := app .treeStack [c .methodINT ][c .treePath ]
if !ok {
tree = app .treeStack [c .methodINT ]["" ]
}
lenTree := len (tree ) - 1
for c .indexRoute < lenTree {
c .indexRoute ++
route := tree [c .indexRoute ]
var match bool
var err error
if route .mount {
continue
}
match = route .match (c .detectionPath , c .path , &c .values )
if !match {
continue
}
c .route = route
if !c .matched && !route .use {
c .matched = true
}
c .indexHandler = 0
if len (route .Handlers ) > 0 {
err = route .Handlers [0 ](c )
}
return match , err
}
err := NewError (StatusNotFound , "Cannot " +c .method +" " +html .EscapeString (c .pathOriginal ))
if !c .matched && app .methodExist (c ) {
err = ErrMethodNotAllowed
}
return false , err
}
func (app *App ) handler (rctx *fasthttp .RequestCtx ) {
c := app .AcquireCtx (rctx )
defer app .ReleaseCtx (c )
if c .methodINT == -1 {
_ = c .Status (StatusBadRequest ).SendString ("Invalid http method" )
return
}
match , err := app .next (c )
if err != nil {
if catch := c .app .ErrorHandler (c , err ); catch != nil {
_ = c .SendStatus (StatusInternalServerError )
}
}
if match && app .config .ETag {
setETag (c , false )
}
}
func (app *App ) addPrefixToRoute (prefix string , route *Route ) *Route {
prefixedPath := getGroupPath (prefix , route .Path )
prettyPath := prefixedPath
if !app .config .CaseSensitive {
prettyPath = utils .ToLower (prettyPath )
}
if !app .config .StrictRouting && len (prettyPath ) > 1 {
prettyPath = utils .TrimRight (prettyPath , '/' )
}
route .Path = prefixedPath
route .path = RemoveEscapeChar (prettyPath )
route .routeParser = parseRoute (prettyPath )
route .root = false
route .star = false
return route
}
func (*App ) copyRoute (route *Route ) *Route {
return &Route {
use : route .use ,
mount : route .mount ,
star : route .star ,
root : route .root ,
path : route .path ,
routeParser : route .routeParser ,
Params : route .Params ,
pos : route .pos ,
Path : route .Path ,
Method : route .Method ,
Handlers : route .Handlers ,
}
}
func (app *App ) register (method , pathRaw string , group *Group , handlers ...Handler ) {
method = utils .ToUpper (method )
if method != methodUse && app .methodInt (method ) == -1 {
panic (fmt .Sprintf ("add: invalid http method %s\n" , method ))
}
isMount := group != nil && group .app != app
if len (handlers ) == 0 && !isMount {
panic (fmt .Sprintf ("missing handler in route: %s\n" , pathRaw ))
}
if pathRaw == "" {
pathRaw = "/"
}
if pathRaw [0 ] != '/' {
pathRaw = "/" + pathRaw
}
pathPretty := pathRaw
if !app .config .CaseSensitive {
pathPretty = utils .ToLower (pathPretty )
}
if !app .config .StrictRouting && len (pathPretty ) > 1 {
pathPretty = utils .TrimRight (pathPretty , '/' )
}
isUse := method == methodUse
isStar := pathPretty == "/*"
isRoot := pathPretty == "/"
parsedRaw := parseRoute (pathRaw )
parsedPretty := parseRoute (pathPretty )
route := Route {
use : isUse ,
mount : isMount ,
star : isStar ,
root : isRoot ,
path : RemoveEscapeChar (pathPretty ),
routeParser : parsedPretty ,
Params : parsedRaw .params ,
group : group ,
Path : pathRaw ,
Method : method ,
Handlers : handlers ,
}
atomic .AddUint32 (&app .handlersCount , uint32 (len (handlers )))
if isUse {
for _ , m := range app .config .RequestMethods {
r := route
app .addRoute (m , &r , isMount )
}
} else {
app .addRoute (method , &route , isMount )
}
}
func (app *App ) registerStatic (prefix , root string , config ...Static ) {
if root == "" {
root = "."
}
if prefix == "" {
prefix = "/"
}
if prefix [0 ] != '/' {
prefix = "/" + prefix
}
if !app .config .CaseSensitive {
prefix = utils .ToLower (prefix )
}
if len (root ) > 0 && root [len (root )-1 ] == '/' {
root = root [:len (root )-1 ]
}
isStar := prefix == "/*"
isRoot := prefix == "/"
if strings .Contains (prefix , "*" ) {
isStar = true
prefix = strings .Split (prefix , "*" )[0 ]
}
prefixLen := len (prefix )
if prefixLen > 1 && prefix [prefixLen -1 :] == "/" {
prefixLen --
prefix = prefix [:prefixLen ]
}
const cacheDuration = 10 * time .Second
fs := &fasthttp .FS {
Root : root ,
AllowEmptyRoot : true ,
GenerateIndexPages : false ,
AcceptByteRange : false ,
Compress : false ,
CompressedFileSuffix : app .config .CompressedFileSuffix ,
CacheDuration : cacheDuration ,
IndexNames : []string {"index.html" },
PathRewrite : func (fctx *fasthttp .RequestCtx ) []byte {
path := fctx .Path ()
if len (path ) >= prefixLen {
if isStar && app .getString (path [0 :prefixLen ]) == prefix {
path = append (path [0 :0 ], '/' )
} else {
path = path [prefixLen :]
if len (path ) == 0 || path [len (path )-1 ] != '/' {
path = append (path , '/' )
}
}
}
if len (path ) > 0 && path [0 ] != '/' {
path = append ([]byte ("/" ), path ...)
}
return path
},
PathNotFound : func (fctx *fasthttp .RequestCtx ) {
fctx .Response .SetStatusCode (StatusNotFound )
},
}
var cacheControlValue string
var modifyResponse Handler
if len (config ) > 0 {
maxAge := config [0 ].MaxAge
if maxAge > 0 {
cacheControlValue = "public, max-age=" + strconv .Itoa (maxAge )
}
fs .CacheDuration = config [0 ].CacheDuration
fs .Compress = config [0 ].Compress
fs .AcceptByteRange = config [0 ].ByteRange
fs .GenerateIndexPages = config [0 ].Browse
if config [0 ].Index != "" {
fs .IndexNames = []string {config [0 ].Index }
}
modifyResponse = config [0 ].ModifyResponse
}
fileHandler := fs .NewRequestHandler ()
handler := func (c *Ctx ) error {
if len (config ) != 0 && config [0 ].Next != nil && config [0 ].Next (c ) {
return c .Next ()
}
fileHandler (c .fasthttp )
if len (config ) > 0 && config [0 ].Download {
c .Attachment ()
}
status := c .fasthttp .Response .StatusCode ()
if status != StatusNotFound && status != StatusForbidden {
if len (cacheControlValue ) > 0 {
c .fasthttp .Response .Header .Set (HeaderCacheControl , cacheControlValue )
}
if modifyResponse != nil {
return modifyResponse (c )
}
return nil
}
c .fasthttp .SetContentType ("" )
c .fasthttp .Response .SetStatusCode (StatusOK )
c .fasthttp .Response .SetBodyString ("" )
return c .Next ()
}
route := Route {
use : true ,
root : isRoot ,
path : prefix ,
Method : MethodGet ,
Path : prefix ,
Handlers : []Handler {handler },
}
atomic .AddUint32 (&app .handlersCount , 1 )
app .addRoute (MethodGet , &route )
app .addRoute (MethodHead , &route )
}
func (app *App ) addRoute (method string , route *Route , isMounted ...bool ) {
var mounted bool
if len (isMounted ) > 0 {
mounted = isMounted [0 ]
}
m := app .methodInt (method )
l := len (app .stack [m ])
if l > 0 && app .stack [m ][l -1 ].Path == route .Path && route .use == app .stack [m ][l -1 ].use && !route .mount && !app .stack [m ][l -1 ].mount {
preRoute := app .stack [m ][l -1 ]
preRoute .Handlers = append (preRoute .Handlers , route .Handlers ...)
} else {
route .pos = atomic .AddUint32 (&app .routesCount , 1 )
route .Method = method
app .stack [m ] = append (app .stack [m ], route )
app .routesRefreshed = true
}
if !mounted {
app .mutex .Lock ()
app .latestRoute = route
if err := app .hooks .executeOnRouteHooks (*route ); err != nil {
panic (err )
}
app .mutex .Unlock ()
}
}
func (app *App ) buildTree () *App {
if !app .routesRefreshed {
return app
}
for m := range app .config .RequestMethods {
tsMap := make (map [string ][]*Route )
for _ , route := range app .stack [m ] {
treePath := ""
if len (route .routeParser .segs ) > 0 && len (route .routeParser .segs [0 ].Const ) >= 3 {
treePath = route .routeParser .segs [0 ].Const [:3 ]
}
tsMap [treePath ] = append (tsMap [treePath ], route )
}
app .treeStack [m ] = tsMap
}
for m := range app .config .RequestMethods {
tsMap := app .treeStack [m ]
for treePart := range tsMap {
if treePart != "" {
tsMap [treePart ] = uniqueRouteStack (append (tsMap [treePart ], tsMap ["" ]...))
}
slc := tsMap [treePart ]
sort .Slice (slc , func (i , j int ) bool { return slc [i ].pos < slc [j ].pos })
}
}
app .routesRefreshed = false
return app
}
The pages are generated with Golds v0.6.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @Go100and1 (reachable from the left QR code) to get the latest news of Golds .