From 8f3047814e86442c8efbd91ef7007905d47cf6c9 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 2 Jul 2015 20:24:54 +0200 Subject: [PATCH] Comments + IRoutes + IRouter + unexported AbortIndex --- auth.go | 16 ++++---- context.go | 26 +++++++------ context_test.go | 8 ++-- debug.go | 7 +++- gin.go | 33 ++++++++++------ response_writer.go | 10 +++++ routergroup.go | 94 +++++++++++++++++++++++---------------------- routergroup_test.go | 2 +- 8 files changed, 112 insertions(+), 84 deletions(-) diff --git a/auth.go b/auth.go index 33f8e9a..ab4e35d 100644 --- a/auth.go +++ b/auth.go @@ -10,9 +10,7 @@ import ( "strconv" ) -const ( - AuthUserKey = "user" -) +const AuthUserKey = "user" type ( Accounts map[string]string @@ -35,8 +33,9 @@ func (a authPairs) searchCredential(authValue string) (string, bool) { return "", false } -// Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where -// the key is the user name and the value is the password, as well as the name of the Realm +// BasicAuthForRealm returns a Basic HTTP Authorization middleware. It takes as arguments a map[string]string where +// the key is the user name and the value is the password, as well as the name of the Realm. +// If the realm is empty, "Authorization Required" will be used by default. // (see http://tools.ietf.org/html/rfc2617#section-1.2) func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { if realm == "" { @@ -59,7 +58,7 @@ func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { } } -// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where +// BasicAuth returns a Basic HTTP Authorization middleware. It takes as argument a map[string]string where // the key is the user name and the value is the password. func BasicAuth(accounts Accounts) HandlerFunc { return BasicAuthForRealm(accounts, "") @@ -91,8 +90,7 @@ func authorizationHeader(user, password string) string { func secureCompare(given, actual string) bool { if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 { return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1 - } else { - /* Securely compare actual to itself to keep constant time, but always return false */ - return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false } + /* Securely compare actual to itself to keep constant time, but always return false */ + return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false } diff --git a/context.go b/context.go index f5c28aa..8c83315 100644 --- a/context.go +++ b/context.go @@ -18,6 +18,7 @@ import ( "golang.org/x/net/context" ) +// Content-Type MIME of the most common data formats const ( MIMEJSON = binding.MIMEJSON MIMEHTML = binding.MIMEHTML @@ -28,7 +29,7 @@ const ( MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm ) -const AbortIndex int8 = math.MaxInt8 / 2 +const abortIndex int8 = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. @@ -63,16 +64,18 @@ func (c *Context) reset() { c.Accepted = nil } +// Copy returns a copy of the current context that can be safely used outside the request's scope. +// This have to be used then the context has to be passed to a goroutine. func (c *Context) Copy() *Context { var cp Context = *c cp.writermem.ResponseWriter = nil cp.Writer = &cp.writermem - cp.index = AbortIndex + cp.index = abortIndex cp.handlers = nil return &cp } -// Returns the main handle's name. For example if the handler is "handleGetUsers()", this +// HandlerName returns the main handle's name. For example if the handler is "handleGetUsers()", this // function will return "main.handleGetUsers" func (c *Context) HandlerName() string { return nameOfFunction(c.handlers.Last()) @@ -93,27 +96,27 @@ func (c *Context) Next() { } } -// Returns if the currect context was aborted. +// IsAborted returns true if the currect context was aborted. func (c *Context) IsAborted() bool { - return c.index >= AbortIndex + return c.index >= abortIndex } -// Stops the system to continue calling the pending handlers in the chain. +// Abort stops the system to continue calling the pending handlers in the chain. // Let's say you have an authorization middleware that validates if the request is authorized // if the authorization fails (the password does not match). This method (Abort()) should be called // in order to stop the execution of the actual handler. func (c *Context) Abort() { - c.index = AbortIndex + c.index = abortIndex } -// It calls Abort() and writes the headers with the specified status code. +// AbortWithStatus calls `Abort()` and writes the headers with the specified status code. // For example, a failed attempt to authentificate a request could use: context.AbortWithStatus(401). func (c *Context) AbortWithStatus(code int) { c.Writer.WriteHeader(code) c.Abort() } -// It calls AbortWithStatus() and Error() internally. This method stops the chain, writes the status code and +// AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and // pushes the specified error to `c.Errors`. // See Context.Error() for more details. func (c *Context) AbortWithError(code int, err error) *Error { @@ -371,7 +374,7 @@ func (c *Context) Redirect(code int, location string) { }) } -// Writes some data into the body stream and updates the HTTP code. +// Data writes some data into the body stream and updates the HTTP code. func (c *Context) Data(code int, contentType string, data []byte) { c.Render(code, render.Data{ ContentType: contentType, @@ -379,11 +382,12 @@ func (c *Context) Data(code int, contentType string, data []byte) { }) } -// Writes the specified file into the body stream in a efficient way. +// File writes the specified file into the body stream in a efficient way. func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } +// SSEvent writes a Server-Sent Event into the body stream. func (c *Context) SSEvent(name string, message interface{}) { c.Render(-1, sse.Event{ Event: name, diff --git a/context_test.go b/context_test.go index a875e33..84ef8b4 100644 --- a/context_test.go +++ b/context_test.go @@ -129,7 +129,7 @@ func TestContextCopy(t *testing.T) { assert.Nil(t, cp.writermem.ResponseWriter) assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter)) assert.Equal(t, cp.Request, c.Request) - assert.Equal(t, cp.index, AbortIndex) + assert.Equal(t, cp.index, abortIndex) assert.Equal(t, cp.Keys, c.Keys) assert.Equal(t, cp.engine, c.engine) assert.Equal(t, cp.Params, c.Params) @@ -418,7 +418,7 @@ func TestContextIsAborted(t *testing.T) { c.Next() assert.True(t, c.IsAborted()) - c.Next() + c.index++ assert.True(t, c.IsAborted()) } @@ -430,7 +430,7 @@ func TestContextAbortWithStatus(t *testing.T) { c.AbortWithStatus(401) c.Writer.WriteHeaderNow() - assert.Equal(t, c.index, AbortIndex) + assert.Equal(t, c.index, abortIndex) assert.Equal(t, c.Writer.Status(), 401) assert.Equal(t, w.Code, 401) assert.True(t, c.IsAborted()) @@ -482,7 +482,7 @@ func TestContextAbortWithError(t *testing.T) { c.Writer.WriteHeaderNow() assert.Equal(t, w.Code, 401) - assert.Equal(t, c.index, AbortIndex) + assert.Equal(t, c.index, abortIndex) assert.True(t, c.IsAborted()) } diff --git a/debug.go b/debug.go index 2f4b101..bff86e1 100644 --- a/debug.go +++ b/debug.go @@ -9,6 +9,9 @@ import "log" func init() { log.SetFlags(0) } + +// IsDebugging returns true if the framework is running in debug mode. +// Use SetMode(gin.Release) to switch to disable the debug mode. func IsDebugging() bool { return ginMode == debugCode } @@ -27,7 +30,7 @@ func debugPrint(format string, values ...interface{}) { } } -func debugPrintWARNING_New() { +func debugPrintWARNINGNew() { debugPrint(`[WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) @@ -35,7 +38,7 @@ func debugPrintWARNING_New() { `) } -func debugPrintWARNING_SetHTMLTemplate() { +func debugPrintWARNINGSetHTMLTemplate() { debugPrint(`[WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called at initialization. ie. before any route is registered or the router is listening in a socket: diff --git a/gin.go b/gin.go index e06792b..6625421 100644 --- a/gin.go +++ b/gin.go @@ -14,6 +14,7 @@ import ( "github.com/gin-gonic/gin/render" ) +// Framework's version const Version = "v1.0rc2" var default404Body = []byte("404 page not found") @@ -22,6 +23,7 @@ var default405Body = []byte("405 method not allowed") type HandlerFunc func(*Context) type HandlersChain []HandlerFunc +// Last returns the last handler in the chain. ie. the last handler is the main own. func (c HandlersChain) Last() HandlerFunc { length := len(c) if length > 0 { @@ -38,7 +40,8 @@ type ( Handler string } - // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. + // Engine is the framework's instance, it contains the muxer, middlewares and configuration settings. + // Create an instance of Engine, by using New() or Default() Engine struct { RouterGroup HTMLRender render.HTMLRender @@ -78,12 +81,16 @@ type ( } ) -var _ RoutesInterface = &Engine{} +var _ IRouter = &Engine{} -// Returns a new blank Engine instance without any middleware attached. -// The most basic configuration +// New returns a new blank Engine instance without any middleware attached. +// By default the configuration is: +// - RedirectTrailingSlash: true +// - RedirectFixedPath: false +// - HandleMethodNotAllowed: false +// - ForwardedByClientIP: true func New() *Engine { - debugPrintWARNING_New() + debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ Handlers: nil, @@ -103,7 +110,7 @@ func New() *Engine { return engine } -// Returns a Engine instance with the Logger and Recovery already attached. +// Default returns an Engine instance with the Logger and Recovery middleware already attached. func Default() *Engine { engine := New() engine.Use(Recovery(), Logger()) @@ -134,7 +141,7 @@ func (engine *Engine) LoadHTMLFiles(files ...string) { func (engine *Engine) SetHTMLTemplate(templ *template.Template) { if len(engine.trees) > 0 { - debugPrintWARNING_SetHTMLTemplate() + debugPrintWARNINGSetHTMLTemplate() } engine.HTMLRender = render.HTMLProduction{Template: templ} } @@ -154,7 +161,7 @@ func (engine *Engine) NoMethod(handlers ...HandlerFunc) { // Attachs a global middleware to the router. ie. the middlewares attached though Use() will be // included in the handlers chain for every single request. Even 404, 405, static files... // For example, this is the right place for a logger or error management middleware. -func (engine *Engine) Use(middlewares ...HandlerFunc) routesInterface { +func (engine *Engine) Use(middlewares ...HandlerFunc) IRoutes { engine.RouterGroup.Use(middlewares...) engine.rebuild404Handlers() engine.rebuild405Handlers() @@ -193,6 +200,8 @@ func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { root.addRoute(path, handlers) } +// Routes returns a slice of registered routes, including some useful information, such as: +// the http method, path and the handler name. func (engine *Engine) Routes() (routes RoutesInfo) { for _, tree := range engine.trees { routes = iterate("", tree.method, routes, tree.root) @@ -215,7 +224,7 @@ func iterate(path, method string, routes RoutesInfo, root *node) RoutesInfo { return routes } -// The router is attached to a http.Server and starts listening and serving HTTP requests. +// Run attaches the router to a http.Server and starts listening and serving HTTP requests. // It is a shortcut for http.ListenAndServe(addr, router) // Note: this method will block the calling goroutine undefinitelly unless an error happens. func (engine *Engine) Run(addr string) (err error) { @@ -226,7 +235,7 @@ func (engine *Engine) Run(addr string) (err error) { return } -// The router is attached to a http.Server and starts listening and serving HTTPS requests. +// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests. // It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) // Note: this method will block the calling goroutine undefinitelly unless an error happens. func (engine *Engine) RunTLS(addr string, certFile string, keyFile string) (err error) { @@ -237,8 +246,8 @@ func (engine *Engine) RunTLS(addr string, certFile string, keyFile string) (err return } -// The router is attached to a http.Server and starts listening and serving HTTP requests -// through the specified unix socket (ie. a file) +// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests +// through the specified unix socket (ie. a file). // Note: this method will block the calling goroutine undefinitelly unless an error happens. func (engine *Engine) RunUnix(file string) (err error) { debugPrint("Listening and serving HTTP on unix:/%s", file) diff --git a/response_writer.go b/response_writer.go index 5a75335..fcbe230 100644 --- a/response_writer.go +++ b/response_writer.go @@ -23,10 +23,20 @@ type ( http.Flusher http.CloseNotifier + // Returns the HTTP response status code of the current request. Status() int + + // Returns the number of bytes already written into the response http body. + // See Written() Size() int + + // Writes the string into the response body. WriteString(string) (int, error) + + // Returns true if the response body was already written. Written() bool + + // Forces to write the http header (status code + headers). WriteHeaderNow() } diff --git a/routergroup.go b/routergroup.go index b77a55b..3fa3b8c 100644 --- a/routergroup.go +++ b/routergroup.go @@ -12,30 +12,30 @@ import ( ) type ( - RoutesInterface interface { - routesInterface + IRouter interface { + IRoutes Group(string, ...HandlerFunc) *RouterGroup } - routesInterface interface { - Use(...HandlerFunc) routesInterface + IRoutes interface { + Use(...HandlerFunc) IRoutes - Handle(string, string, ...HandlerFunc) routesInterface - Any(string, ...HandlerFunc) routesInterface - GET(string, ...HandlerFunc) routesInterface - POST(string, ...HandlerFunc) routesInterface - DELETE(string, ...HandlerFunc) routesInterface - PATCH(string, ...HandlerFunc) routesInterface - PUT(string, ...HandlerFunc) routesInterface - OPTIONS(string, ...HandlerFunc) routesInterface - HEAD(string, ...HandlerFunc) routesInterface + 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 - StaticFile(string, string) routesInterface - Static(string, string) routesInterface - StaticFS(string, http.FileSystem) routesInterface + StaticFile(string, string) IRoutes + Static(string, string) IRoutes + StaticFS(string, http.FileSystem) IRoutes } - // Used internally to configure router, a RouterGroup is associated with a prefix + // RouterGroup is used internally to configure router, a RouterGroup is associated with a prefix // and an array of handlers (middlewares) RouterGroup struct { Handlers HandlersChain @@ -45,15 +45,15 @@ type ( } ) -var _ RoutesInterface = &RouterGroup{} +var _ IRouter = &RouterGroup{} -// Adds middlewares to the group, see example code in github. -func (group *RouterGroup) Use(middlewares ...HandlerFunc) routesInterface { +// Use adds middlewares to the group, see example code in github. +func (group *RouterGroup) Use(middlewares ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middlewares...) return group.returnObj() } -// Creates a new router group. You should add all the routes that have common middlwares or the same path prefix. +// Group 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{ @@ -63,6 +63,13 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R } } +func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { + absolutePath := group.calculateAbsolutePath(relativePath) + handlers = group.combineHandlers(handlers) + group.engine.addRoute(httpMethod, absolutePath, handlers) + return group.returnObj() +} + // 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. @@ -73,14 +80,7 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R // 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 HandlersChain) routesInterface { - absolutePath := group.calculateAbsolutePath(relativePath) - handlers = group.combineHandlers(handlers) - group.engine.addRoute(httpMethod, absolutePath, handlers) - return group.returnObj() -} - -func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil { panic("http method " + httpMethod + " is not valid") } @@ -88,42 +88,43 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...Ha } // POST is a shortcut for router.Handle("POST", path, handle) -func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("POST", relativePath, handlers) } // GET is a shortcut for router.Handle("GET", path, handle) -func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("GET", relativePath, handlers) } // DELETE is a shortcut for router.Handle("DELETE", path, handle) -func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("DELETE", relativePath, handlers) } // PATCH is a shortcut for router.Handle("PATCH", path, handle) -func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("PATCH", relativePath, handlers) } // PUT is a shortcut for router.Handle("PUT", path, handle) -func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("PUT", relativePath, handlers) } // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) -func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("OPTIONS", relativePath, handlers) } // HEAD is a shortcut for router.Handle("HEAD", path, handle) -func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) routesInterface { +func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle("HEAD", relativePath, handlers) } -func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) routesInterface { - // GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE +// 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 { group.handle("GET", relativePath, handlers) group.handle("POST", relativePath, handlers) group.handle("PUT", relativePath, handlers) @@ -136,7 +137,9 @@ func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) rout return group.returnObj() } -func (group *RouterGroup) StaticFile(relativePath, filepath string) routesInterface { +// StaticFile registers a single route in order to server a single file of the local filesystem. +// router.StaticFile("favicon.ico", "./resources/favicon.ico") +func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { panic("URL parameters can not be used when serving a static file") } @@ -154,11 +157,13 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) routesInterf // To use the operating system's file system implementation, // use : // router.Static("/static", "/var/www") -func (group *RouterGroup) Static(relativePath, root string) routesInterface { +func (group *RouterGroup) Static(relativePath, root string) IRoutes { return group.StaticFS(relativePath, Dir(root, false)) } -func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) routesInterface { +// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead. +// Gin by default user: gin.Dir() +func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { panic("URL parameters can not be used when serving a static folder") } @@ -185,7 +190,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) - if finalSize >= int(AbortIndex) { + if finalSize >= int(abortIndex) { panic("too many handlers") } mergedHandlers := make(HandlersChain, finalSize) @@ -198,10 +203,9 @@ func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { return joinPaths(group.BasePath, relativePath) } -func (group *RouterGroup) returnObj() routesInterface { +func (group *RouterGroup) returnObj() IRoutes { if group.root { return group.engine - } else { - return group } + return group } diff --git a/routergroup_test.go b/routergroup_test.go index 5f87b00..39bdb80 100644 --- a/routergroup_test.go +++ b/routergroup_test.go @@ -157,7 +157,7 @@ func TestRouterGroupPipeline(t *testing.T) { testRoutesInterface(t, v1) } -func testRoutesInterface(t *testing.T, r RoutesInterface) { +func testRoutesInterface(t *testing.T, r IRoutes) { handler := func(c *Context) {} assert.Equal(t, r, r.Use(handler))