From 07a3961941779068491f54fbbab47651d084bbcd Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Oct 2014 21:37:26 +0200 Subject: [PATCH] General refactoring --- context.go | 62 ++++++++++------- context_test.go | 6 +- gin.go | 175 +++++++---------------------------------------- mode.go | 4 ++ recovery_test.go | 2 +- routergroup.go | 137 +++++++++++++++++++++++++++++++++++++ utils.go | 17 ++++- 7 files changed, 223 insertions(+), 180 deletions(-) create mode 100644 routergroup.go diff --git a/context.go b/context.go index 178379b..7fcdd93 100644 --- a/context.go +++ b/context.go @@ -71,11 +71,11 @@ type Context struct { } /************************************/ -/********** ROUTES GROUPING *********/ +/********** CONTEXT CREATION ********/ /************************************/ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { - c := engine.cache.Get().(*Context) + c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.Params = params @@ -87,9 +87,9 @@ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, pa return c } -/************************************/ -/****** FLOW AND ERROR MANAGEMENT****/ -/************************************/ +func (engine *Engine) reuseContext(c *Context) { + engine.pool.Put(c) +} func (c *Context) Copy() *Context { var cp Context = *c @@ -98,6 +98,10 @@ func (c *Context) Copy() *Context { return &cp } +/************************************/ +/*************** FLOW ***************/ +/************************************/ + // Next should be used only in the middlewares. // It executes the pending handlers in the chain inside the calling handler. // See example in github. @@ -109,25 +113,31 @@ func (c *Context) Next() { } } -// Forces the system to do not continue calling the pending handlers. -// For example, the first handler checks if the request is authorized. If it's not, context.Abort(401) should be called. -// The rest of pending handlers would never be called for that request. -func (c *Context) Abort(code int) { - if code >= 0 { - c.Writer.WriteHeader(code) - } +// Forces the system to do not continue calling the pending handlers in the chain. +func (c *Context) Abort() { c.index = AbortIndex } +// Same than AbortWithStatus() but also writes the specified response status code. +// For example, the first handler checks if the request is authorized. If it's not, context.AbortWithStatus(401) should be called. +func (c *Context) AbortWithStatus(code int) { + c.Writer.WriteHeader(code) + c.Abort() +} + +/************************************/ +/********* ERROR MANAGEMENT *********/ +/************************************/ + // Fail is the same as Abort plus an error message. // Calling `context.Fail(500, err)` is equivalent to: // ``` // context.Error("Operation aborted", err) -// context.Abort(500) +// context.AbortWithStatus(500) // ``` func (c *Context) Fail(code int, err error) { c.Error(err, "Operation aborted") - c.Abort(code) + c.AbortWithStatus(code) } func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) { @@ -146,9 +156,9 @@ func (c *Context) Error(err error, meta interface{}) { } func (c *Context) LastError() error { - s := len(c.Errors) - if s > 0 { - return errors.New(c.Errors[s-1].Err) + nuErrors := len(c.Errors) + if nuErrors > 0 { + return errors.New(c.Errors[nuErrors-1].Err) } else { return nil } @@ -170,9 +180,9 @@ func (c *Context) Set(key string, item interface{}) { // Get returns the value for the given key or an error if the key does not exist. func (c *Context) Get(key string) (interface{}, error) { if c.Keys != nil { - item, ok := c.Keys[key] + value, ok := c.Keys[key] if ok { - return item, nil + return value, nil } } return nil, errors.New("Key does not exist.") @@ -182,13 +192,13 @@ func (c *Context) Get(key string) (interface{}, error) { func (c *Context) MustGet(key string) interface{} { value, err := c.Get(key) if err != nil || value == nil { - log.Panicf("Key %s doesn't exist", key) + log.Panicf("Key %s doesn't exist", value) } return value } /************************************/ -/******** ENCOGING MANAGEMENT********/ +/********* PARSING REQUEST **********/ /************************************/ // This function checks the Content-Type to select a binding engine automatically, @@ -222,10 +232,14 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { return true } +/************************************/ +/******** RESPONSE RENDERING ********/ +/************************************/ + func (c *Context) Render(code int, render render.Render, obj ...interface{}) { if err := render.Render(c.Writer, code, obj...); err != nil { c.ErrorTyped(err, ErrorTypeInternal, obj) - c.Abort(500) + c.AbortWithStatus(500) } } @@ -267,9 +281,7 @@ func (c *Context) Data(code int, contentType string, data []byte) { if len(contentType) > 0 { c.Writer.Header().Set("Content-Type", contentType) } - if code >= 0 { - c.Writer.WriteHeader(code) - } + c.Writer.WriteHeader(code) c.Writer.Write(data) } diff --git a/context_test.go b/context_test.go index 6df824c..8435ac5 100644 --- a/context_test.go +++ b/context_test.go @@ -232,13 +232,13 @@ func TestBadAbortHandlersChain(t *testing.T) { c.Next() stepsPassed += 1 // after check and abort - c.Abort(409) + c.AbortWithStatus(409) }) r.Use(func(c *Context) { stepsPassed += 1 c.Next() stepsPassed += 1 - c.Abort(403) + c.AbortWithStatus(403) }) // RUN @@ -260,7 +260,7 @@ func TestAbortHandlersChain(t *testing.T) { r := New() r.Use(func(context *Context) { stepsPassed += 1 - context.Abort(409) + context.AbortWithStatus(409) }) r.Use(func(context *Context) { stepsPassed += 1 diff --git a/gin.go b/gin.go index 1ff2acf..fcdd0a2 100644 --- a/gin.go +++ b/gin.go @@ -5,13 +5,11 @@ package gin import ( - "fmt" "github.com/gin-gonic/gin/render" "github.com/julienschmidt/httprouter" "html/template" "math" "net/http" - "path" "sync" ) @@ -28,28 +26,19 @@ const ( type ( HandlerFunc func(*Context) - // Used internally to configure router, a RouterGroup is associated with a prefix - // and an array of handlers (middlewares) - RouterGroup struct { - Handlers []HandlerFunc - prefix string - parent *RouterGroup - engine *Engine - } - // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { *RouterGroup - HTMLRender render.Render - cache sync.Pool - finalNoRoute []HandlerFunc - noRoute []HandlerFunc - router *httprouter.Router + HTMLRender render.Render + pool sync.Pool + allNoRoute []HandlerFunc + noRoute []HandlerFunc + router *httprouter.Router } ) func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.finalNoRoute) + c := engine.createContext(w, req, nil, engine.allNoRoute) // set 404 by default, useful for logging c.Writer.WriteHeader(404) c.Next() @@ -60,17 +49,21 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { c.Writer.WriteHeaderNow() } } - engine.cache.Put(c) + engine.reuseContext(c) } // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { engine := &Engine{} - engine.RouterGroup = &RouterGroup{nil, "/", nil, engine} + engine.RouterGroup = &RouterGroup{ + Handlers: nil, + absolutePath: "/", + engine: engine, + } engine.router = httprouter.New() engine.router.NotFound = engine.handle404 - engine.cache.New = func() interface{} { + engine.pool.New = func() interface{} { c := &Context{Engine: engine} c.Writer = &c.writermem return c @@ -86,7 +79,7 @@ func Default() *Engine { } func (engine *Engine) LoadHTMLGlob(pattern string) { - if gin_mode == debugCode { + if IsDebugging() { render.HTMLDebug.AddGlob(pattern) engine.HTMLRender = render.HTMLDebug } else { @@ -96,7 +89,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { } func (engine *Engine) LoadHTMLFiles(files ...string) { - if gin_mode == debugCode { + if IsDebugging() { render.HTMLDebug.AddFiles(files...) engine.HTMLRender = render.HTMLDebug } else { @@ -114,151 +107,33 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) { // Adds handlers for NoRoute. It return a 404 code by default. func (engine *Engine) NoRoute(handlers ...HandlerFunc) { engine.noRoute = handlers - engine.finalNoRoute = engine.combineHandlers(engine.noRoute) + engine.rebuild404Handlers() } func (engine *Engine) Use(middlewares ...HandlerFunc) { engine.RouterGroup.Use(middlewares...) - engine.finalNoRoute = engine.combineHandlers(engine.noRoute) + engine.rebuild404Handlers() +} + +func (engine *Engine) rebuild404Handlers() { + engine.allNoRoute = engine.combineHandlers(engine.noRoute) } // ServeHTTP makes the router implement the http.Handler interface. -func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { - engine.router.ServeHTTP(w, req) +func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + engine.router.ServeHTTP(writer, request) } func (engine *Engine) Run(addr string) { - if gin_mode == debugCode { - fmt.Println("[GIN-debug] Listening and serving HTTP on " + addr) - } + debugPrint("Listening and serving HTTP on %s", addr) if err := http.ListenAndServe(addr, engine); err != nil { panic(err) } } func (engine *Engine) RunTLS(addr string, cert string, key string) { - if gin_mode == debugCode { - fmt.Println("[GIN-debug] Listening and serving HTTPS on " + addr) - } + debugPrint("Listening and serving HTTPS on %s", addr) if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { panic(err) } } - -/************************************/ -/********** ROUTES GROUPING *********/ -/************************************/ - -// Adds middlewares to the group, see example code in github. -func (group *RouterGroup) Use(middlewares ...HandlerFunc) { - group.Handlers = append(group.Handlers, middlewares...) -} - -// Creates a new router group. You should add all the routes that have common middlwares or the same path prefix. -// For example, all the routes that use a common middlware for authorization could be grouped. -func (group *RouterGroup) Group(component string, handlers ...HandlerFunc) *RouterGroup { - prefix := group.pathFor(component) - - return &RouterGroup{ - Handlers: group.combineHandlers(handlers), - parent: group, - prefix: prefix, - engine: group.engine, - } -} - -func (group *RouterGroup) pathFor(p string) string { - joined := path.Join(group.prefix, p) - // Append a '/' if the last component had one, but only if it's not there already - if len(p) > 0 && p[len(p)-1] == '/' && joined[len(joined)-1] != '/' { - return joined + "/" - } - return joined -} - -// Handle registers a new request handle and middlewares with the given path and method. -// The last handler should be the real handler, the other ones should be middlewares that can and should be shared among different routes. -// See the example code in github. -// -// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut -// functions can be used. -// -// This function is intended for bulk loading and to allow the usage of less -// frequently used, non-standardized or custom methods (e.g. for internal -// communication with a proxy). -func (group *RouterGroup) Handle(method, p string, handlers []HandlerFunc) { - p = group.pathFor(p) - handlers = group.combineHandlers(handlers) - if gin_mode == debugCode { - nuHandlers := len(handlers) - name := funcName(handlers[nuHandlers-1]) - fmt.Printf("[GIN-debug] %-5s %-25s --> %s (%d handlers)\n", method, p, name, nuHandlers) - } - group.engine.router.Handle(method, p, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { - c := group.engine.createContext(w, req, params, handlers) - c.Next() - c.Writer.WriteHeaderNow() - group.engine.cache.Put(c) - }) -} - -// POST is a shortcut for router.Handle("POST", path, handle) -func (group *RouterGroup) POST(path string, handlers ...HandlerFunc) { - group.Handle("POST", path, handlers) -} - -// GET is a shortcut for router.Handle("GET", path, handle) -func (group *RouterGroup) GET(path string, handlers ...HandlerFunc) { - group.Handle("GET", path, handlers) -} - -// DELETE is a shortcut for router.Handle("DELETE", path, handle) -func (group *RouterGroup) DELETE(path string, handlers ...HandlerFunc) { - group.Handle("DELETE", path, handlers) -} - -// PATCH is a shortcut for router.Handle("PATCH", path, handle) -func (group *RouterGroup) PATCH(path string, handlers ...HandlerFunc) { - group.Handle("PATCH", path, handlers) -} - -// PUT is a shortcut for router.Handle("PUT", path, handle) -func (group *RouterGroup) PUT(path string, handlers ...HandlerFunc) { - group.Handle("PUT", path, handlers) -} - -// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) -func (group *RouterGroup) OPTIONS(path string, handlers ...HandlerFunc) { - group.Handle("OPTIONS", path, handlers) -} - -// HEAD is a shortcut for router.Handle("HEAD", path, handle) -func (group *RouterGroup) HEAD(path string, handlers ...HandlerFunc) { - group.Handle("HEAD", path, handlers) -} - -// Static serves files from the given file system root. -// Internally a http.FileServer is used, therefore http.NotFound is used instead -// of the Router's NotFound handler. -// To use the operating system's file system implementation, -// use : -// router.Static("/static", "/var/www") -func (group *RouterGroup) Static(p, root string) { - prefix := group.pathFor(p) - p = path.Join(p, "/*filepath") - fileServer := http.StripPrefix(prefix, http.FileServer(http.Dir(root))) - group.GET(p, func(c *Context) { - fileServer.ServeHTTP(c.Writer, c.Request) - }) - group.HEAD(p, func(c *Context) { - fileServer.ServeHTTP(c.Writer, c.Request) - }) -} - -func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { - s := len(group.Handlers) + len(handlers) - h := make([]HandlerFunc, 0, s) - h = append(h, group.Handlers...) - h = append(h, handlers...) - return h -} diff --git a/mode.go b/mode.go index 9856789..20abd51 100644 --- a/mode.go +++ b/mode.go @@ -42,6 +42,10 @@ func Mode() string { return mode_name } +func IsDebugging() bool { + return gin_mode == debugCode +} + func init() { value := os.Getenv(GIN_MODE) if len(value) == 0 { diff --git a/recovery_test.go b/recovery_test.go index 756c7c2..f9047e2 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -39,7 +39,7 @@ func TestPanicWithAbort(t *testing.T) { r := New() r.Use(Recovery()) r.GET("/recovery", func(c *Context) { - c.Abort(400) + c.AbortWithStatus(400) panic("Oupps, Houston, we have a problem") }) diff --git a/routergroup.go b/routergroup.go new file mode 100644 index 0000000..651bf93 --- /dev/null +++ b/routergroup.go @@ -0,0 +1,137 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "github.com/julienschmidt/httprouter" + "net/http" + "path" +) + +// Used internally to configure router, a RouterGroup is associated with a prefix +// and an array of handlers (middlewares) +type RouterGroup struct { + Handlers []HandlerFunc + absolutePath string + engine *Engine +} + +// Adds middlewares to the group, see example code in github. +func (group *RouterGroup) Use(middlewares ...HandlerFunc) { + group.Handlers = append(group.Handlers, middlewares...) +} + +// Creates a new router group. You should add all the routes that have common middlwares or the same path prefix. +// For example, all the routes that use a common middlware for authorization could be grouped. +func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { + return &RouterGroup{ + Handlers: group.combineHandlers(handlers), + absolutePath: group.calculateAbsolutePath(relativePath), + engine: group.engine, + } +} + +// Handle registers a new request handle and middlewares with the given path and method. +// The last handler should be the real handler, the other ones should be middlewares that can and should be shared among different routes. +// See the example code in github. +// +// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut +// functions can be used. +// +// This function is intended for bulk loading and to allow the usage of less +// frequently used, non-standardized or custom methods (e.g. for internal +// communication with a proxy). +func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []HandlerFunc) { + absolutePath := group.calculateAbsolutePath(relativePath) + handlers = group.combineHandlers(handlers) + if IsDebugging() { + nuHandlers := len(handlers) + handlerName := nameOfFuncion(handlers[nuHandlers-1]) + debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) + } + + group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { + context := group.engine.createContext(w, req, params, handlers) + context.Next() + context.Writer.WriteHeaderNow() + group.engine.reuseContext(context) + }) +} + +// POST is a shortcut for router.Handle("POST", path, handle) +func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) { + group.Handle("POST", relativePath, handlers) +} + +// GET is a shortcut for router.Handle("GET", path, handle) +func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) { + group.Handle("GET", relativePath, handlers) +} + +// DELETE is a shortcut for router.Handle("DELETE", path, handle) +func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) { + group.Handle("DELETE", relativePath, handlers) +} + +// PATCH is a shortcut for router.Handle("PATCH", path, handle) +func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) { + group.Handle("PATCH", relativePath, handlers) +} + +// PUT is a shortcut for router.Handle("PUT", path, handle) +func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) { + group.Handle("PUT", relativePath, handlers) +} + +// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) +func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) { + group.Handle("OPTIONS", relativePath, handlers) +} + +// HEAD is a shortcut for router.Handle("HEAD", path, handle) +func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) { + group.Handle("HEAD", relativePath, handlers) +} + +// Static serves files from the given file system root. +// Internally a http.FileServer is used, therefore http.NotFound is used instead +// of the Router's NotFound handler. +// To use the operating system's file system implementation, +// use : +// router.Static("/static", "/var/www") +func (group *RouterGroup) Static(relativePath, root string) { + handler := group.createStaticHandler(relativePath, root) + group.GET(relativePath, handler) + group.HEAD(relativePath, handler) +} + +func (group *RouterGroup) createStaticHandler(relativePath, root string) func(*Context) { + absolutePath := group.calculateAbsolutePath(relativePath) + absolutePath = path.Join(absolutePath, "/*filepath") + fileServer := http.StripPrefix(absolutePath, http.FileServer(http.Dir(root))) + return func(c *Context) { + fileServer.ServeHTTP(c.Writer, c.Request) + } +} + +func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { + finalSize := len(group.Handlers) + len(handlers) + mergedHandlers := make([]HandlerFunc, 0, finalSize) + mergedHandlers = append(mergedHandlers, group.Handlers...) + mergedHandlers = append(mergedHandlers, handlers...) + return mergedHandlers +} + +func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { + if len(relativePath) == 0 { + return group.absolutePath + } + absolutePath := path.Join(group.absolutePath, relativePath) + appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/' + if appendSlash { + return absolutePath + "/" + } + return absolutePath +} diff --git a/utils.go b/utils.go index fee5728..69ad8fa 100644 --- a/utils.go +++ b/utils.go @@ -6,6 +6,7 @@ package gin import ( "encoding/xml" + "fmt" "reflect" "runtime" "strings" @@ -46,6 +47,12 @@ func filterFlags(content string) string { return content } +func debugPrint(format string, values ...interface{}) { + if IsDebugging() { + fmt.Printf("[GIN-debug] "+format, values) + } +} + func chooseData(custom, wildcard interface{}) interface{} { if custom == nil { if wildcard == nil { @@ -69,6 +76,14 @@ func parseAccept(accept string) []string { return parts } -func funcName(f interface{}) string { +func lastChar(str string) uint8 { + size := len(str) + if size == 0 { + panic("The length of the string can't be 0") + } + return str[size-1] +} + +func nameOfFuncion(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() }