From 15216a0883d113fadc33198d24850974eae0f841 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 18 Jun 2014 01:42:34 +0200 Subject: [PATCH] Initial commit --- README.md | 277 ++++++++++++++++++++++++++++ auth.go | 83 +++++++++ examples/example_basic.go | 55 ++++++ gin.go | 377 ++++++++++++++++++++++++++++++++++++++ logger.go | 20 ++ recovery.go | 97 ++++++++++ validation.go | 56 ++++++ 7 files changed, 965 insertions(+) create mode 100644 README.md create mode 100644 auth.go create mode 100644 examples/example_basic.go create mode 100644 gin.go create mode 100644 logger.go create mode 100644 recovery.go create mode 100644 validation.go diff --git a/README.md b/README.md new file mode 100644 index 00000000..f910801f --- /dev/null +++ b/README.md @@ -0,0 +1,277 @@ +#Gin Web Framework +Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster. If you need performance and good productivity, you will love Gin. +[Check out the official web site](http://gingonic.github.com) + +## Start using it +Run: + +``` +go get github.com/gin-gonic/gin +``` +Then import it in your Golang code: + +``` +import "github.com/gin-gonic/gin" +``` + + +##API Examples + +#### Create most basic PING/PONG HTTP endpoint +``` +func main() { + // Creates a gin router + logger and recovery (crash-free) middlwares + r := gin.Default() + r.GET("/ping", func(c *gin.Context){ + c.String("pong") + }) + + r.POST("/somePost", posting) + r.PUT("/somePut", putting) + r.DELETE("/someDelete", deleting) + r.PATCH("/somePATCH", patching) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +#### Parameters in path + +``` +func main() { + r := gin.Default() + + r.GET("/user/:name", func(c *gin.Context) { + name := c.Params.ByName("name") + message := "Hello "+name + c.String(200, message) + }) +} +``` + + +#### Grouping routes +``` +func main() { + r := gin.Default() + + // Simple group: v1 + v1 := r.Group("/v1") + { + v1.POST("/login", loginEndpoint) + v1.POST("/submit", submitEndpoint) + v1.POST("/read", readEndpoint) + } + + // Simple group: v1 + v2 := r.Group("/v2") + { + v2.POST("/login", loginEndpoint) + v2.POST("/submit"", submitEndpoint) + v2.POST("/read"", readEndpoint) + } + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") +} +``` + + +#### Blank Gin without middlewares by default + +Use + +``` +r := gin.New() +``` +instead of + +``` +r := gin.Default() +``` + + +#### Using middlewares +``` +func main() { + // Creates a router without any middlware by default + r := gin.New() + + // Global middlwares + r.Use(gin.Logger()) + r.Use(gin.Recovery()) + + // Per route middlwares, you can add as many as you desire. + r.GET("/benchmark", MyBenchLogger(), benchEndpoint) + + // Authorization group + // authorized := r.Group("/", AuthRequired()) + // exactly the same than: + authorized := r.Group("/") + // per group middlwares! in this case we use the custom created + // AuthRequired() middlware just in the "authorized" group. + authorized.Use(AuthRequired()) + { + authorized.Use.POST("/login", loginEndpoint) + authorized.Use.POST("/submit", submitEndpoint) + authorized.Use.POST("/read", readEndpoint) + + // nested group + testing := authorized.Group("testing") + testing.GET("/analytics", analyticsEndpoint) + } + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") +} +``` + + +#### JSON parsing and validation +``` + +type LoginJSON struct { + User string `json:"user" binding:"required"` + Password string `json:"password" binding:"required"` +} + +func main() { + r := gin.Default() + + r.POST("/login", func(c *gin.Context) { + var json LoginJSON + + // If EnsureBody returns false, it will write automatically the error + // in the HTTP stream and return a 400 error. If you want custom error + // handling you should use: c.ParseBody(interface{}) error + if c.EnsureBody(&json) { + if json.User=="manu" && json.Password=="123" { + c.JSON(200, gin.H{"status": "you are logged in"}) + }else{ + c.JSON(401, gin.H{"status": "unauthorized"}) + } + } + }) +} +``` + +#### XML, and JSON rendering + +``` +func main() { + r := gin.Default() + + // gin.H is a shortcup for map[string]interface{} + r.GET("/someJSON", func(c *gin.Context) { + c.JSON(200, gin.H{"message": "hey", "status": 200}) + }) + + r.GET("/moreJSON", func(c *gin.Context) { + // You also can use a struct + var msg struct { + Message string + Status int + } + msg.Message = "hey" + msg.Status = 200 + c.JSON(200, msg.Status) + }) + + r.GET("/someXML", func(c *gin.Context) { + c.XML(200, gin.H{"message": "hey", "status": 200}) + }) +} +``` + + +####HTML rendering + +Using LoadHTMLTemplates() + +``` +func main() { + r := gin.Default() + r.LoadHTMLTemplates("templates/*") + r.GET("index", func(c *gin.Context) { + obj := gin.h{"title": "Main website"} + c.HTML(200, "templates/index.tmpl", obj) + }) +} +``` + +You can also use your own html template render + +``` +import "html/template" +func main() { + r := gin.Default() + html := template.ParseFiles("file1", "file2") + r.HTMLTemplates = html +} +``` + + +#### Custom Middlewares + +``` +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + t : time.Now() + + // Set example variable + c.Set("example", "12345") + + // before request + + c.Next() + + // after request + latency := time.Since(t) + log.Print(latency) + } +} + +func main() { + r := gin.New() + r.Use(Logger()) + + r.GET("test", func(c *gin.Context){ + example := r.Get("example").(string) + + // it would print: "12345" + log.Println(example) + }) +``` + + + + +#### Custom HTTP configuration + +Do not use the `Run()` method, instead use this: + +``` +func main() { + router := gin.Default() + http.ListenAndServe(":8080", router) +} +``` +or + +``` +func main() { + router := gin.Default() + + s := &http.Server{ + Addr: ":8080", + Handler: router, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + s.ListenAndServe() +} +``` + + diff --git a/auth.go b/auth.go new file mode 100644 index 00000000..c6a5291a --- /dev/null +++ b/auth.go @@ -0,0 +1,83 @@ +package gin + +import ( + "crypto/subtle" + "encoding/base64" + "errors" + "sort" +) + +type ( + BasicAuthPair struct { + Code string + User string + } + Account struct { + User string + Password string + } + + Accounts []Account + Pairs []BasicAuthPair +) + +func (a Pairs) Len() int { return len(a) } +func (a Pairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a Pairs) Less(i, j int) bool { return a[i].Code < a[j].Code } + +func processCredentials(accounts Accounts) (Pairs, error) { + if len(accounts) == 0 { + return nil, errors.New("Empty list of authorized credentials.") + } + pairs := make(Pairs, 0, len(accounts)) + for _, account := range accounts { + if len(account.User) == 0 || len(account.Password) == 0 { + return nil, errors.New("User or password is empty") + } + base := account.User + ":" + account.Password + code := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) + pairs = append(pairs, BasicAuthPair{code, account.User}) + } + // We have to sort the credentials in order to use bsearch later. + sort.Sort(pairs) + return pairs, nil +} + +func searchCredential(pairs Pairs, auth string) string { + if len(auth) == 0 { + return "" + } + // Search user in the slice of allowed credentials + r := sort.Search(len(pairs), func(i int) bool { return pairs[i].Code >= auth }) + + if r < len(pairs) && subtle.ConstantTimeCompare([]byte(pairs[r].Code), []byte(auth)) == 1 { + // user is allowed, set UserId to key "user" in this context, the userId can be read later using + // c.Get("user" + return pairs[r].User + } else { + return "" + } +} + +// Implements a basic Basic HTTP Authorization. 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 { + + pairs, err := processCredentials(accounts) + if err != nil { + panic(err) + } + return func(c *Context) { + // Search user in the slice of allowed credentials + user := searchCredential(pairs, c.Req.Header.Get("Authorization")) + if len(user) == 0 { + // Credentials doesn't match, we return 401 Unauthorized and abort request. + c.Writer.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"") + c.Fail(401, errors.New("Unauthorized")) + } else { + // user is allowed, set UserId to key "user" in this context, the userId can be read later using + // c.Get("user") + c.Set("user", user) + } + } +} diff --git a/examples/example_basic.go b/examples/example_basic.go new file mode 100644 index 00000000..575bd89f --- /dev/null +++ b/examples/example_basic.go @@ -0,0 +1,55 @@ +package main + +import ( + "github.com/gin-gonic/gin" +) + +var DB = make(map[string]string) + +func main() { + r := gin.Default() + + // Ping test + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + // Get user value + r.GET("/user/:name", func(c *gin.Context) { + user := c.Params.ByName("name") + value, ok := DB[user] + if ok { + c.JSON(200, gin.H{"user": user, "value": value}) + } else { + c.JSON(200, gin.H{"user": user, "status": "no value"}) + } + }) + + // Authorized group (uses gin.BasicAuth() middleware) + // Same than: + // authorized := r.Group("/") + // authorized.Use(gin.BasicAuth(gin.Credentials{ + // "foo": "bar", + // "manu": "123", + //})) + authorized := r.Group("/", gin.BasicAuth(gin.Accounts{ + {"foo", "bar"}, //1. user:foo password:bar + {"manu", "123"}, //2. user:manu password:123 + })) + + authorized.POST("admin", func(c *gin.Context) { + user := c.Get("user").(string) + + // Parse JSON + var json struct { + Value string `json:"value" binding:"required"` + } + if c.EnsureBody(&json) { + DB[user] = json.Value + c.JSON(200, gin.H{"status": "ok"}) + } + }) + + // Listen and Server in 0.0.0.0:8080 + r.Run(":8081") +} diff --git a/gin.go b/gin.go new file mode 100644 index 00000000..1674f2ef --- /dev/null +++ b/gin.go @@ -0,0 +1,377 @@ +package gin + +import ( + "encoding/json" + "encoding/xml" + "github.com/julienschmidt/httprouter" + "html/template" + "log" + "math" + "net/http" + "path" +) + +const ( + AbortIndex = math.MaxInt8 / 2 +) + +type ( + HandlerFunc func(*Context) + + H map[string]interface{} + + // Used internally to collect a error ocurred during a http request. + ErrorMsg struct { + Message string `json:"msg"` + Meta interface{} `json:"meta"` + } + + ResponseWriter interface { + http.ResponseWriter + Status() int + Written() bool + } + + responseWriter struct { + http.ResponseWriter + status int + } + + // 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. + Context struct { + Req *http.Request + Writer ResponseWriter + Keys map[string]interface{} + Errors []ErrorMsg + Params httprouter.Params + handlers []HandlerFunc + engine *Engine + index int8 + } + + // 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 wrappers the blazing fast httprouter multiplexer and a list of global middlewares. + Engine struct { + *RouterGroup + handlers404 []HandlerFunc + router *httprouter.Router + HTMLTemplates *template.Template + } +) + +func (rw *responseWriter) WriteHeader(s int) { + rw.ResponseWriter.WriteHeader(s) + rw.status = s +} + +func (rw *responseWriter) Write(b []byte) (int, error) { + return rw.ResponseWriter.Write(b) +} + +func (rw *responseWriter) Status() int { + return rw.status +} + +func (rw *responseWriter) Written() bool { + return rw.status != 0 +} + +// 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.router = httprouter.New() + engine.router.NotFound = engine.handle404 + return engine +} + +// Returns a Engine instance with the Logger and Recovery already attached. +func Default() *Engine { + engine := New() + engine.Use(Recovery(), Logger()) + return engine +} + +func (engine *Engine) LoadHTMLTemplates(pattern string) { + engine.HTMLTemplates = template.Must(template.ParseGlob(pattern)) +} + +// Adds handlers for NotFound. It return a 404 code by default. +func (engine *Engine) NotFound404(handlers ...HandlerFunc) { + engine.handlers404 = handlers +} + +func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { + + handlers := engine.allHandlers(engine.handlers404) + c := engine.createContext(w, req, nil, handlers) + c.Next() + if !c.Writer.Written() { + http.NotFound(c.Writer, c.Req) + } +} + +// ServeFiles serves files from the given file system root. +// The path must end with "/*filepath", files are then served from the local +// path /defined/root/dir/*filepath. +// For example if root is "/etc" and *filepath is "passwd", the local file +// "/etc/passwd" would be served. +// 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 http.Dir: +// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) +func (engine *Engine) ServeFiles(path string, root http.FileSystem) { + engine.router.ServeFiles(path, root) +} + +// 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) Run(addr string) { + http.ListenAndServe(addr, engine) +} + +/************************************/ +/********** ROUTES GROUPING *********/ +/************************************/ + +func (group *RouterGroup) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { + return &Context{ + Writer: &responseWriter{w, 0}, + Req: req, + index: -1, + engine: group.engine, + Params: params, + handlers: handlers, + } +} + +// Adds middlewares to the group, see example code in github. +func (group *RouterGroup) Use(middlewares ...HandlerFunc) { + group.Handlers = append(group.Handlers, middlewares...) +} + +// Greates a new router group. You should create add all the routes that share that have common middlwares or 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 := path.Join(group.prefix, component) + return &RouterGroup{ + Handlers: handlers, + parent: group, + prefix: prefix, + 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(method, p string, handlers []HandlerFunc) { + p = path.Join(group.prefix, p) + handlers = group.allHandlers(handlers) + group.engine.router.Handle(method, p, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { + group.createContext(w, req, params, handlers).Next() + }) +} + +// 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) +} + +func (group *RouterGroup) allHandlers(handlers []HandlerFunc) []HandlerFunc { + local := append(group.Handlers, handlers...) + if group.parent != nil { + return group.parent.allHandlers(local) + } else { + return local + } +} + +/************************************/ +/****** FLOW AND ERROR MANAGEMENT****/ +/************************************/ + +// Next should be used only in the middlewares. +// It executes the pending handlers in the chain inside the calling handler. +// See example in github. +func (c *Context) Next() { + c.index++ + s := int8(len(c.handlers)) + for ; c.index < s; c.index++ { + c.handlers[c.index](c) + } +} + +// 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) { + c.Writer.WriteHeader(code) + c.index = AbortIndex +} + +// Fail is the same than Abort plus an error message. +// Calling `context.Fail(500, err)` is equivalent to: +// ``` +// context.Error("Operation aborted", err) +// context.Abort(500) +// ``` +func (c *Context) Fail(code int, err error) { + c.Error(err, "Operation aborted") + c.Abort(code) +} + +// Attachs an error to the current context. The error is pushed to a list of errors. +// It's a gooc idea to call Error for each error ocurred during the resolution of a request. +// A middleware can be used to collect all the errors and push them to a database together, print a log, or append it in the HTTP response. +func (c *Context) Error(err error, meta interface{}) { + c.Errors = append(c.Errors, ErrorMsg{ + Message: err.Error(), + Meta: meta, + }) +} + +/************************************/ +/******** METADATA MANAGEMENT********/ +/************************************/ + +// Sets a new pair key/value just for the specefied context. +// It also lazy initializes the hashmap +func (c *Context) Set(key string, item interface{}) { + if c.Keys == nil { + c.Keys = make(map[string]interface{}) + } + c.Keys[key] = item +} + +// Returns the value for the given key. +// It panics if the value doesn't exist. +func (c *Context) Get(key string) interface{} { + var ok bool + var item interface{} + if c.Keys != nil { + item, ok = c.Keys[key] + } else { + item, ok = nil, false + } + if !ok || item == nil { + log.Panicf("Key %s doesn't exist", key) + } + return item +} + +/************************************/ +/******** ENCOGING MANAGEMENT********/ +/************************************/ + +// Like ParseBody() but this method also writes a 400 error if the json is not valid. +func (c *Context) EnsureBody(item interface{}) bool { + if err := c.ParseBody(item); err != nil { + c.Fail(400, err) + return false + } + return true +} + +// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer. +func (c *Context) ParseBody(item interface{}) error { + decoder := json.NewDecoder(c.Req.Body) + if err := decoder.Decode(&item); err == nil { + return Validate(c, item) + } else { + return err + } +} + +// Serializes the given struct as a JSON into the response body in a fast and efficient way. +// It also sets the Content-Type as "application/json" +func (c *Context) JSON(code int, obj interface{}) { + c.Writer.WriteHeader(code) + c.Writer.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(c.Writer) + if err := encoder.Encode(obj); err != nil { + c.Error(err, obj) + http.Error(c.Writer, err.Error(), 500) + } +} + +// Serializes the given struct as a XML into the response body in a fast and efficient way. +// It also sets the Content-Type as "application/xml" +func (c *Context) XML(code int, obj interface{}) { + c.Writer.WriteHeader(code) + c.Writer.Header().Set("Content-Type", "application/xml") + encoder := xml.NewEncoder(c.Writer) + if err := encoder.Encode(obj); err != nil { + c.Error(err, obj) + http.Error(c.Writer, err.Error(), 500) + } +} + +// Renders the HTTP template specified by his file name. +// It also update the HTTP code and sets the Content-Type as "text/html". +// See http://golang.org/doc/articles/wiki/ +func (c *Context) HTML(code int, name string, data interface{}) { + c.Writer.WriteHeader(code) + c.Writer.Header().Set("Content-Type", "text/html") + if err := c.engine.HTMLTemplates.ExecuteTemplate(c.Writer, name, data); err != nil { + c.Error(err, map[string]interface{}{ + "name": name, + "data": data, + }) + http.Error(c.Writer, err.Error(), 500) + } +} + +// Writes the given string into the response body and sets the Content-Type to "text/plain" +func (c *Context) String(code int, msg string) { + c.Writer.Header().Set("Content-Type", "text/plain") + c.Writer.WriteHeader(code) + c.Writer.Write([]byte(msg)) +} + +// Writes some data into the body stream and updates the HTTP code +func (c *Context) Data(code int, data []byte) { + c.Writer.WriteHeader(code) + c.Writer.Write(data) +} diff --git a/logger.go b/logger.go new file mode 100644 index 00000000..48f69a50 --- /dev/null +++ b/logger.go @@ -0,0 +1,20 @@ +package gin + +import ( + "log" + "time" +) + +func Logger() HandlerFunc { + return func(c *Context) { + + // Start timer + t := time.Now() + + // Process request + c.Next() + + // Calculate resolution time + log.Printf("[%d] %s in %v", c.Writer.Status(), c.Req.RequestURI, time.Since(t)) + } +} diff --git a/recovery.go b/recovery.go new file mode 100644 index 00000000..cbde0e8a --- /dev/null +++ b/recovery.go @@ -0,0 +1,97 @@ +package gin + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "net/http" + "runtime" +) + +var ( + dunno = []byte("???") + centerDot = []byte("·") + dot = []byte(".") + slash = []byte("/") +) + +// stack returns a nicely formated stack frame, skipping skip frames +func stack(skip int) []byte { + buf := new(bytes.Buffer) // the returned data + // As we loop, we open files and read them. These variables record the currently + // loaded file. + var lines [][]byte + var lastFile string + for i := skip; ; i++ { // Skip the expected number of frames + pc, file, line, ok := runtime.Caller(i) + if !ok { + break + } + // Print this much at least. If we can't find the source, it won't show. + fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc) + if file != lastFile { + data, err := ioutil.ReadFile(file) + if err != nil { + continue + } + lines = bytes.Split(data, []byte{'\n'}) + lastFile = file + } + fmt.Fprintf(buf, "\t%s: %s\n", function(pc), source(lines, line)) + } + return buf.Bytes() +} + +// source returns a space-trimmed slice of the n'th line. +func source(lines [][]byte, n int) []byte { + n-- // in stack trace, lines are 1-indexed but our array is 0-indexed + if n < 0 || n >= len(lines) { + return dunno + } + return bytes.TrimSpace(lines[n]) +} + +// function returns, if possible, the name of the function containing the PC. +func function(pc uintptr) []byte { + fn := runtime.FuncForPC(pc) + if fn == nil { + return dunno + } + name := []byte(fn.Name()) + // The name includes the path name to the package, which is unnecessary + // since the file name is already included. Plus, it has center dots. + // That is, we see + // runtime/debug.*T·ptrmethod + // and want + // *T.ptrmethod + // Also the package path might contains dot (e.g. code.google.com/...), + // so first eliminate the path prefix + if lastslash := bytes.LastIndex(name, slash); lastslash >= 0 { + name = name[lastslash+1:] + } + if period := bytes.Index(name, dot); period >= 0 { + name = name[period+1:] + } + name = bytes.Replace(name, centerDot, dot, -1) + return name +} + +// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. +// While Martini is in development mode, Recovery will also output the panic as HTML. +func Recovery() HandlerFunc { + return func(c *Context) { + defer func() { + if len(c.Errors) > 0 { + log.Println(c.Errors) + } + if err := recover(); err != nil { + stack := stack(3) + log.Printf("PANIC: %s\n%s", err, stack) + c.Writer.WriteHeader(http.StatusInternalServerError) + } + }() + + c.Next() + } +} diff --git a/validation.go b/validation.go new file mode 100644 index 00000000..610d10ac --- /dev/null +++ b/validation.go @@ -0,0 +1,56 @@ +package gin + +import ( + "errors" + "reflect" + "strings" +) + +func (c *Context) ErrorRender() HandlerFunc { + return func(c *Context) { + defer func() { + if len(c.Errors) > 0 { + c.JSON(-1, c.Errors) + } + }() + c.Next() + } +} + +func Validate(c *Context, obj interface{}) error { + + var err error + typ := reflect.TypeOf(obj) + val := reflect.ValueOf(obj) + + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + fieldValue := val.Field(i).Interface() + zero := reflect.Zero(field.Type).Interface() + + // Validate nested and embedded structs (if pointer, only do so if not nil) + if field.Type.Kind() == reflect.Struct || + (field.Type.Kind() == reflect.Ptr && !reflect.DeepEqual(zero, fieldValue)) { + err = Validate(c, fieldValue) + } + + if strings.Index(field.Tag.Get("binding"), "required") > -1 { + if reflect.DeepEqual(zero, fieldValue) { + name := field.Name + if j := field.Tag.Get("json"); j != "" { + name = j + } else if f := field.Tag.Get("form"); f != "" { + name = f + } + err = errors.New("Required " + name) + c.Error(err, "json validation") + } + } + } + return err +}