Compare commits
	
		
			16 Commits
		
	
	
		
			0ed626e351
			...
			my-framewo
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c5ab1debdb | |||
| c0fea38790 | |||
| b98513ad36 | |||
| 0786a97a77 | |||
| cb1ea9f701 | |||
| 8f4b378fdd | |||
| 099d1aeb0f | |||
| 3b16d6b16a | |||
| fc4103cff3 | |||
| e770731643 | |||
| 3bf14b9c04 | |||
| 1aa9b78bdc | |||
| e8c4cbaa89 | |||
| 14af16c71e | |||
| 9f83bc7b58 | |||
| 0a5e329aa4 | 
@ -2,12 +2,7 @@ package framework
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
@ -19,7 +14,12 @@ type Context struct {
 | 
			
		||||
	ctx            context.Context
 | 
			
		||||
	request        *http.Request
 | 
			
		||||
	responseWriter http.ResponseWriter
 | 
			
		||||
	handler        ControllerHandler
 | 
			
		||||
 | 
			
		||||
	handlers []ControllerHandler
 | 
			
		||||
	// current handler index
 | 
			
		||||
	index int
 | 
			
		||||
 | 
			
		||||
	params map[string]string
 | 
			
		||||
 | 
			
		||||
	hasTimeout bool
 | 
			
		||||
	writerMux  *sync.Mutex
 | 
			
		||||
@ -32,6 +32,7 @@ func NewContext(w http.ResponseWriter, r *http.Request) *Context {
 | 
			
		||||
		request:        r,
 | 
			
		||||
		responseWriter: w,
 | 
			
		||||
		writerMux:      &sync.Mutex{},
 | 
			
		||||
		index:          -1, // will be set to 0 when at the beginning
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -97,172 +98,27 @@ func (ctx *Context) Value(key any) any {
 | 
			
		||||
	return ctx.BaseContext().Value(key)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
// {{{ Implements request functions
 | 
			
		||||
 | 
			
		||||
// {{{ Request URI
 | 
			
		||||
 | 
			
		||||
// QueryInt gets an int value from the query request
 | 
			
		||||
func (ctx *Context) QueryInt(key string, defval int) (int, error) {
 | 
			
		||||
	params, err := ctx.QueryAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return defval, err
 | 
			
		||||
	}
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		len := len(vals)
 | 
			
		||||
		if len > 0 {
 | 
			
		||||
			intval, err := strconv.Atoi(vals[len-1]) // return the last elem
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return defval, err
 | 
			
		||||
			}
 | 
			
		||||
			return intval, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, errors.New("key not found")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// QueryString gets a string value from the query request
 | 
			
		||||
func (ctx *Context) QueryString(key string, defval string) (string, error) {
 | 
			
		||||
	params, err := ctx.QueryAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return defval, err
 | 
			
		||||
	}
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		len := len(vals)
 | 
			
		||||
		if len > 0 {
 | 
			
		||||
			return vals[len-1], nil // return the last elem
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, errors.New("key not found")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// QueryArray gets an array of string values from the query request
 | 
			
		||||
func (ctx *Context) QueryArray(key string, defval []string) ([]string, error) {
 | 
			
		||||
	params, err := ctx.QueryAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return defval, err
 | 
			
		||||
	}
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		return vals, nil // return the last elem
 | 
			
		||||
	}
 | 
			
		||||
	return defval, errors.New("key not found")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// QueryAll returns all queries in a request URL
 | 
			
		||||
func (ctx *Context) QueryAll() (url.Values, error) {
 | 
			
		||||
	if ctx.request != nil {
 | 
			
		||||
		return map[string][]string(ctx.request.URL.Query()), nil
 | 
			
		||||
	}
 | 
			
		||||
	return url.Values{}, errors.New("missing request in the context")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
// {{{ Post form
 | 
			
		||||
 | 
			
		||||
// FormInt gets an int value from the submitted form
 | 
			
		||||
func (ctx *Context) FormInt(key string, defval int) (int, error) {
 | 
			
		||||
	vals, err := ctx.FormAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return defval, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	valStrs, ok := vals[key]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return defval, errors.New("key not found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	valInt, err := strconv.Atoi(valStrs[0]) // Get the first one as result
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return defval, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return valInt, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormString gets a string value from the submitted form
 | 
			
		||||
func (ctx *Context) FormString(key string, defval string) (string, error) {
 | 
			
		||||
	vals, err := ctx.FormAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return defval, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	valStrs, ok := vals[key]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return defval, errors.New("key not found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return valStrs[0], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormArray gets an array of string values from the submitted form
 | 
			
		||||
func (ctx *Context) FormArray(key string, defval []string) ([]string, error) {
 | 
			
		||||
	vals, err := ctx.FormAll()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return defval, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	valStrs, ok := vals[key]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return defval, errors.New("key not found")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return valStrs, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormAll gets everything from the submitted form
 | 
			
		||||
func (ctx *Context) FormAll() (url.Values, error) {
 | 
			
		||||
	if ctx.request != nil {
 | 
			
		||||
		err := ctx.request.ParseForm()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return url.Values{}, err
 | 
			
		||||
		}
 | 
			
		||||
		return ctx.request.PostForm, err
 | 
			
		||||
	}
 | 
			
		||||
	return url.Values{}, errors.New("missing request in the context")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
// {{{ application/json
 | 
			
		||||
 | 
			
		||||
// ReadJSON binds the request JSON body to an object.
 | 
			
		||||
//
 | 
			
		||||
// A pointer of obj should be passed.
 | 
			
		||||
func (ctx *Context) ReadJSON(obj any) error {
 | 
			
		||||
	if ctx.request == nil {
 | 
			
		||||
		return errors.New("missing request in the context")
 | 
			
		||||
	}
 | 
			
		||||
	dec := json.NewDecoder(ctx.request.Body)
 | 
			
		||||
	err := dec.Decode(obj)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = dec.Decode(&struct{}{})
 | 
			
		||||
	if err != io.EOF {
 | 
			
		||||
		return errors.New("body must have only a single JSON value")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WriteJSON send back an object in JSON format with the status code
 | 
			
		||||
func (ctx *Context) WriteJSON(status int, obj any) error {
 | 
			
		||||
	// There is a timeout, some error message data must have already been
 | 
			
		||||
	// written to the output. Stop writing anything into the responseWriter.
 | 
			
		||||
	if ctx.HasTimeout() {
 | 
			
		||||
// Next runs the next function in the function chain
 | 
			
		||||
func (ctx *Context) Next() error {
 | 
			
		||||
	ctx.index++
 | 
			
		||||
	if ctx.index >= len(ctx.handlers) {
 | 
			
		||||
		// This is the end of the chain
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	data, err := json.Marshal(obj)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	ctx.responseWriter.Header().Set("Content-type", "application/json")
 | 
			
		||||
	ctx.responseWriter.WriteHeader(status)
 | 
			
		||||
	_, err = ctx.responseWriter.Write(data)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
	// Run this handler
 | 
			
		||||
	if err := ctx.handlers[ctx.index](ctx); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
// SetHandlers sets handlers for context
 | 
			
		||||
func (ctx *Context) SetHandlers(handlers []ControllerHandler) {
 | 
			
		||||
	ctx.handlers = handlers
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) SetParams(params map[string]string) {
 | 
			
		||||
	ctx.params = params
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,8 @@ import (
 | 
			
		||||
 | 
			
		||||
// Core is the core struct of the framework
 | 
			
		||||
type Core struct {
 | 
			
		||||
	router map[string]*Trie
 | 
			
		||||
	router      map[string]*Trie
 | 
			
		||||
	middlewares []ControllerHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewCore initialize the Core.
 | 
			
		||||
@ -28,40 +29,44 @@ func NewCore() *Core {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get is a simple get router
 | 
			
		||||
func (c *Core) Get(url string, handler ControllerHandler) {
 | 
			
		||||
	upperUrl := strings.ToUpper(url)
 | 
			
		||||
    if err := c.router["GET"].AddRouter(upperUrl, handler); err != nil{
 | 
			
		||||
        log.Println(err)
 | 
			
		||||
    }
 | 
			
		||||
func (c *Core) Get(url string, handlers ...ControllerHandler) {
 | 
			
		||||
	allHandlers := append(c.middlewares, handlers...)
 | 
			
		||||
	if err := c.router["GET"].AddRouter(url, allHandlers); err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Post is a simple post router
 | 
			
		||||
func (c *Core) Post(url string, handler ControllerHandler) {
 | 
			
		||||
	upperUrl := strings.ToUpper(url)
 | 
			
		||||
    if err := c.router["POST"].AddRouter(upperUrl, handler); err != nil{
 | 
			
		||||
        log.Println(err)
 | 
			
		||||
    }
 | 
			
		||||
func (c *Core) Post(url string, handlers ...ControllerHandler) {
 | 
			
		||||
	allHandlers := append(c.middlewares, handlers...)
 | 
			
		||||
	if err := c.router["POST"].AddRouter(url, allHandlers); err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Put is a simple put router
 | 
			
		||||
func (c *Core) Put(url string, handler ControllerHandler) {
 | 
			
		||||
	upperUrl := strings.ToUpper(url)
 | 
			
		||||
    if err := c.router["PUT"].AddRouter(upperUrl, handler); err != nil{
 | 
			
		||||
        log.Println(err)
 | 
			
		||||
    }
 | 
			
		||||
func (c *Core) Put(url string, handlers ...ControllerHandler) {
 | 
			
		||||
	allHandlers := append(c.middlewares, handlers...)
 | 
			
		||||
	if err := c.router["PUT"].AddRouter(url, allHandlers); err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Delete is a simple delete router
 | 
			
		||||
func (c *Core) Delete(url string, handler ControllerHandler) {
 | 
			
		||||
	upperUrl := strings.ToUpper(url)
 | 
			
		||||
    if err := c.router["DELETE"].AddRouter(upperUrl, handler); err != nil{
 | 
			
		||||
        log.Println(err)
 | 
			
		||||
    }
 | 
			
		||||
func (c *Core) Delete(url string, handlers ...ControllerHandler) {
 | 
			
		||||
	allHandlers := append(c.middlewares, handlers...)
 | 
			
		||||
	if err := c.router["DELETE"].AddRouter(url, allHandlers); err != nil {
 | 
			
		||||
		log.Println(err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Use registers middlewares
 | 
			
		||||
func (c *Core) Use(middlewares ...ControllerHandler) {
 | 
			
		||||
	c.middlewares = append(c.middlewares, middlewares...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FindRouteByRequest finds route using the request
 | 
			
		||||
func (c *Core) FindRouteByRequest(r *http.Request) ControllerHandler {
 | 
			
		||||
	upperUri := strings.ToUpper(r.URL.Path)
 | 
			
		||||
func (c *Core) FindRouteByRequest(r *http.Request) *node {
 | 
			
		||||
	upperMethod := strings.ToUpper(r.Method)
 | 
			
		||||
 | 
			
		||||
	mapper, ok := c.router[upperMethod]
 | 
			
		||||
@ -70,13 +75,13 @@ func (c *Core) FindRouteByRequest(r *http.Request) ControllerHandler {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	controller := mapper.FindRoute(upperUri)
 | 
			
		||||
	if controller == nil {
 | 
			
		||||
	node := mapper.FindRoute(r.URL.Path)
 | 
			
		||||
	if node == nil {
 | 
			
		||||
		log.Printf("URI %q is not recognized\n", r.URL.Path)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return controller
 | 
			
		||||
	return node
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *Core) Group(prefix string) IGroup {
 | 
			
		||||
@ -92,14 +97,18 @@ func (c *Core) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	ctx := NewContext(w, r)
 | 
			
		||||
 | 
			
		||||
	router := c.FindRouteByRequest(r)
 | 
			
		||||
	if router == nil {
 | 
			
		||||
	node := c.FindRouteByRequest(r)
 | 
			
		||||
	if node == nil {
 | 
			
		||||
		ctx.WriteJSON(http.StatusNotFound, "Request not found")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err := router(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
	params := node.ParseParamsFromEndNode(r.URL.Path)
 | 
			
		||||
 | 
			
		||||
	ctx.SetParams(params)
 | 
			
		||||
	ctx.SetHandlers(node.handlers)
 | 
			
		||||
 | 
			
		||||
	if err := ctx.Next(); err != nil {
 | 
			
		||||
		ctx.WriteJSON(http.StatusInternalServerError, "Internal error")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -2,16 +2,18 @@ package framework
 | 
			
		||||
 | 
			
		||||
// IGroup prefix routes
 | 
			
		||||
type IGroup interface {
 | 
			
		||||
	Get(string, ControllerHandler)
 | 
			
		||||
	Post(string, ControllerHandler)
 | 
			
		||||
	Put(string, ControllerHandler)
 | 
			
		||||
	Delete(string, ControllerHandler)
 | 
			
		||||
	Get(string, ...ControllerHandler)
 | 
			
		||||
	Post(string, ...ControllerHandler)
 | 
			
		||||
	Put(string, ...ControllerHandler)
 | 
			
		||||
	Delete(string, ...ControllerHandler)
 | 
			
		||||
	Use(...ControllerHandler)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Group is the implementation of IGroup interface
 | 
			
		||||
type Group struct {
 | 
			
		||||
	core   *Core
 | 
			
		||||
	prefix string
 | 
			
		||||
	core        *Core
 | 
			
		||||
	prefix      string
 | 
			
		||||
	middlewares []ControllerHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewGroup create a new prefix group
 | 
			
		||||
@ -23,21 +25,30 @@ func NewGroup(core *Core, prefix string) *Group {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get is a simple get router of the group
 | 
			
		||||
func (g *Group) Get(url string, handler ControllerHandler) {
 | 
			
		||||
	g.core.Get(g.prefix+url, handler)
 | 
			
		||||
func (g *Group) Get(url string, handlers ...ControllerHandler) {
 | 
			
		||||
	allHandlers := append(g.middlewares, handlers...)
 | 
			
		||||
	g.core.Get(g.prefix+url, allHandlers...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Post is a simple post router of the group
 | 
			
		||||
func (g *Group) Post(url string, handler ControllerHandler) {
 | 
			
		||||
	g.core.Post(g.prefix+url, handler)
 | 
			
		||||
func (g *Group) Post(url string, handlers ...ControllerHandler) {
 | 
			
		||||
	allHandlers := append(g.middlewares, handlers...)
 | 
			
		||||
	g.core.Post(g.prefix+url, allHandlers...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Put is a simple put router of the group
 | 
			
		||||
func (g *Group) Put(url string, handler ControllerHandler) {
 | 
			
		||||
	g.core.Put(g.prefix+url, handler)
 | 
			
		||||
func (g *Group) Put(url string, handlers ...ControllerHandler) {
 | 
			
		||||
	allHandlers := append(g.middlewares, handlers...)
 | 
			
		||||
	g.core.Put(g.prefix+url, allHandlers...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Delete is a simple delete router of the group
 | 
			
		||||
func (g *Group) Delete(url string, handler ControllerHandler) {
 | 
			
		||||
	g.core.Delete(g.prefix+url, handler)
 | 
			
		||||
func (g *Group) Delete(url string, handlers ...ControllerHandler) {
 | 
			
		||||
	allHandlers := append(g.middlewares, handlers...)
 | 
			
		||||
	g.core.Delete(g.prefix+url, allHandlers...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Use registers middlewares
 | 
			
		||||
func (g *Group) Use(middlewares ...ControllerHandler) {
 | 
			
		||||
	g.middlewares = append(g.middlewares, middlewares...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										21
									
								
								framework/middleware/recovery.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								framework/middleware/recovery.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
package middleware
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"git.vinchent.xyz/vinchent/go-web/framework"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Recovery is a middleware that recovers from the panic
 | 
			
		||||
func Recovery() framework.ControllerHandler {
 | 
			
		||||
	return func(c *framework.Context) error {
 | 
			
		||||
		defer func() {
 | 
			
		||||
			if err := recover(); err != nil {
 | 
			
		||||
				c.WriteJSON(http.StatusInternalServerError, err)
 | 
			
		||||
			}
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		c.Next()
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								framework/middleware/test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								framework/middleware/test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,34 @@
 | 
			
		||||
package middleware
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"git.vinchent.xyz/vinchent/go-web/framework"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Test1() framework.ControllerHandler {
 | 
			
		||||
	return func(c *framework.Context) error {
 | 
			
		||||
		log.Println("middleware test1 pre")
 | 
			
		||||
		c.Next()
 | 
			
		||||
		log.Println("middleware test1 post")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test2() framework.ControllerHandler {
 | 
			
		||||
	return func(c *framework.Context) error {
 | 
			
		||||
		log.Println("middleware test2 pre")
 | 
			
		||||
		c.Next()
 | 
			
		||||
		log.Println("middleware test2 post")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test3() framework.ControllerHandler {
 | 
			
		||||
	return func(c *framework.Context) error {
 | 
			
		||||
		log.Println("middleware test3 pre")
 | 
			
		||||
		c.Next()
 | 
			
		||||
		log.Println("middleware test3 post")
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								framework/middleware/timeout.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								framework/middleware/timeout.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
package middleware
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.vinchent.xyz/vinchent/go-web/framework"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Timeout(d time.Duration) framework.ControllerHandler {
 | 
			
		||||
	return func(c *framework.Context) error {
 | 
			
		||||
		finish := make(chan struct{}, 1)
 | 
			
		||||
		panicChan := make(chan interface{}, 1)
 | 
			
		||||
 | 
			
		||||
		durationCtx, cancel := context.WithTimeout(c.BaseContext(), d)
 | 
			
		||||
		defer cancel()
 | 
			
		||||
 | 
			
		||||
		go func() {
 | 
			
		||||
			// Handle panic
 | 
			
		||||
			defer func() {
 | 
			
		||||
				if p := recover(); p != nil {
 | 
			
		||||
					panicChan <- p
 | 
			
		||||
				}
 | 
			
		||||
			}()
 | 
			
		||||
 | 
			
		||||
			// Run the next middleware or the business logic
 | 
			
		||||
			c.Next()
 | 
			
		||||
 | 
			
		||||
			finish <- struct{}{}
 | 
			
		||||
		}()
 | 
			
		||||
 | 
			
		||||
		select {
 | 
			
		||||
		case p := <-panicChan:
 | 
			
		||||
			// panic
 | 
			
		||||
			log.Println(p)
 | 
			
		||||
			c.GetResponseWriter().WriteHeader(http.StatusInternalServerError)
 | 
			
		||||
		case <-finish:
 | 
			
		||||
			// finish normally
 | 
			
		||||
			log.Println("finish")
 | 
			
		||||
		case <-durationCtx.Done():
 | 
			
		||||
			c.SetHasTimeout()
 | 
			
		||||
			c.GetResponseWriter().WriteHeader(http.StatusRequestTimeout)
 | 
			
		||||
			c.GetResponseWriter().Write([]byte("time out"))
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										103
									
								
								framework/middleware/timeout_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								framework/middleware/timeout_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,103 @@
 | 
			
		||||
package middleware
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/http/httptest"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.vinchent.xyz/vinchent/go-web/framework"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestTimeout(t *testing.T) {
 | 
			
		||||
	t.Run("Test timeout handler", func(t *testing.T) {
 | 
			
		||||
		timeoutHandler := Timeout(1 * time.Millisecond)
 | 
			
		||||
 | 
			
		||||
		longHandler := func(c *framework.Context) error {
 | 
			
		||||
			time.Sleep(2 * time.Millisecond)
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res := prepareMiddlewareTest(t, timeoutHandler, longHandler)
 | 
			
		||||
 | 
			
		||||
		assertCode(t, res.StatusCode, http.StatusRequestTimeout)
 | 
			
		||||
		assertBody(t, res.Body, "time out")
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Test no timeout", func(t *testing.T) {
 | 
			
		||||
		timeoutHandler := Timeout(2 * time.Millisecond)
 | 
			
		||||
 | 
			
		||||
		quickHandler := func(c *framework.Context) error {
 | 
			
		||||
			// time.Sleep(1 * time.Millisecond)
 | 
			
		||||
			c.WriteJSON(http.StatusOK, "ok")
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res := prepareMiddlewareTest(t, timeoutHandler, quickHandler)
 | 
			
		||||
 | 
			
		||||
		assertCode(t, res.StatusCode, http.StatusOK)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRecover(t *testing.T) {
 | 
			
		||||
	t.Run("Test panic", func(t *testing.T) {
 | 
			
		||||
		recoverer := Recovery()
 | 
			
		||||
 | 
			
		||||
		panicHandler := func(c *framework.Context) error {
 | 
			
		||||
			panic("panic")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res := prepareMiddlewareTest(t, recoverer, panicHandler)
 | 
			
		||||
 | 
			
		||||
		assertCode(t, res.StatusCode, http.StatusInternalServerError)
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("Test no panic", func(t *testing.T) {
 | 
			
		||||
		recoverer := Recovery()
 | 
			
		||||
 | 
			
		||||
		normalHandler := func(c *framework.Context) error {
 | 
			
		||||
			c.WriteJSON(http.StatusOK, "ok")
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		res := prepareMiddlewareTest(t, recoverer, normalHandler)
 | 
			
		||||
 | 
			
		||||
		assertCode(t, res.StatusCode, http.StatusOK)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func prepareMiddlewareTest(
 | 
			
		||||
	t testing.TB,
 | 
			
		||||
	mid framework.ControllerHandler,
 | 
			
		||||
	in framework.ControllerHandler,
 | 
			
		||||
) *http.Response {
 | 
			
		||||
	t.Helper()
 | 
			
		||||
	request := httptest.NewRequest(http.MethodGet, "/", nil)
 | 
			
		||||
	response := httptest.NewRecorder()
 | 
			
		||||
	c := framework.NewContext(response, request)
 | 
			
		||||
 | 
			
		||||
	c.SetHandlers([]framework.ControllerHandler{in})
 | 
			
		||||
 | 
			
		||||
	err := mid(c)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	res := response.Result()
 | 
			
		||||
	return res
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func assertCode(t testing.TB, got int, want int) {
 | 
			
		||||
	t.Helper()
 | 
			
		||||
	if got != want {
 | 
			
		||||
		t.Errorf("status code got %d, want %d", got, want)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func assertBody(t testing.TB, got io.Reader, want string) {
 | 
			
		||||
	t.Helper()
 | 
			
		||||
	buf, _ := io.ReadAll(got)
 | 
			
		||||
	if cmp := bytes.Compare(buf, []byte(want)); cmp != 0 {
 | 
			
		||||
		t.Errorf("got %q, want %q", string(buf), want)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										411
									
								
								framework/request.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										411
									
								
								framework/request.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,411 @@
 | 
			
		||||
package framework
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"encoding/xml"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"mime/multipart"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	"github.com/spf13/cast"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type IRequest interface {
 | 
			
		||||
	// url query
 | 
			
		||||
	// e.g. foo.com?a=1&b=bar&c[]=bar
 | 
			
		||||
	QueryAll(key string) url.Values
 | 
			
		||||
	QueryInt(key string, defval int) (int, bool)
 | 
			
		||||
	QueryInt64(key string, defval int64) (int64, bool)
 | 
			
		||||
	QueryFloat32(key string, defval float32) (float32, bool)
 | 
			
		||||
	QueryFloat64(key string, defval float64) (float64, bool)
 | 
			
		||||
	QueryBool(key string, defval bool) (bool, bool)
 | 
			
		||||
	QueryString(key string, defval string) (string, bool)
 | 
			
		||||
	QueryStringSlice(key string, defval []string) ([]string, bool)
 | 
			
		||||
 | 
			
		||||
	// url params
 | 
			
		||||
	// e.g. /book/:id
 | 
			
		||||
	Param(key string) any
 | 
			
		||||
	ParamInt(key string, defval int) (int, bool)
 | 
			
		||||
	ParamInt64(key string, defval int64) (int64, bool)
 | 
			
		||||
	ParamFloat32(key string, defval float32) (float32, bool)
 | 
			
		||||
	ParamFloat64(key string, defval float64) (float64, bool)
 | 
			
		||||
	ParamBool(key string, defval bool) (bool, bool)
 | 
			
		||||
	ParamString(key string, defval string) (string, bool)
 | 
			
		||||
 | 
			
		||||
	// form
 | 
			
		||||
	FormAll(key string) url.Values
 | 
			
		||||
	FormInt(key string, defval int) (int, bool)
 | 
			
		||||
	FormInt64(key string, defval int64) (int64, bool)
 | 
			
		||||
	FormFloat32(key string, defval float32) (float32, bool)
 | 
			
		||||
	FormFloat64(key string, defval float64) (float64, bool)
 | 
			
		||||
	FormBool(key string, defval bool) (bool, bool)
 | 
			
		||||
	FormString(key string, defval string) (string, bool)
 | 
			
		||||
	FormStringSlice(key string, defval []string) ([]string, bool)
 | 
			
		||||
	FormFile(key string) (*multipart.FileHeader, error)
 | 
			
		||||
 | 
			
		||||
	// JSON body
 | 
			
		||||
	BindJSON(obj any) error
 | 
			
		||||
 | 
			
		||||
	// XML body
 | 
			
		||||
	BindXML(obj any) error
 | 
			
		||||
 | 
			
		||||
	// RAW body
 | 
			
		||||
	GetRawData() ([]byte, error)
 | 
			
		||||
 | 
			
		||||
	// Basic informations
 | 
			
		||||
	Uri() string
 | 
			
		||||
	Method() string
 | 
			
		||||
	Host() string
 | 
			
		||||
	ClientIP() string
 | 
			
		||||
 | 
			
		||||
	// Header
 | 
			
		||||
	Headers() map[string][]string
 | 
			
		||||
	Header(key string) (string, bool)
 | 
			
		||||
 | 
			
		||||
	// Cookie
 | 
			
		||||
	Cookies() map[string]string
 | 
			
		||||
	Cookie(key string) (string, bool)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// {{{ url query
 | 
			
		||||
 | 
			
		||||
// QueryAll returns all queries in a request URL
 | 
			
		||||
func (ctx *Context) QueryAll() url.Values {
 | 
			
		||||
	if ctx.request != nil {
 | 
			
		||||
		return map[string][]string(ctx.request.URL.Query())
 | 
			
		||||
	}
 | 
			
		||||
	return url.Values{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// QueryInt gets an int value from the query request
 | 
			
		||||
func (ctx *Context) QueryInt(key string, defval int) (int, bool) {
 | 
			
		||||
	params := ctx.QueryAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToInt(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) QueryInt64(key string, defval int64) (int64, bool) {
 | 
			
		||||
	params := ctx.QueryAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToInt64(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) QueryBool(key string, defval bool) (bool, bool) {
 | 
			
		||||
	params := ctx.QueryAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToBool(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) QueryFloat32(key string, defval float32) (float32, bool) {
 | 
			
		||||
	params := ctx.QueryAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToFloat32(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) QueryFloat64(key string, defval float64) (float64, bool) {
 | 
			
		||||
	params := ctx.QueryAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToFloat64(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// QueryString gets a string value from the query request
 | 
			
		||||
func (ctx *Context) QueryString(key string, defval string) (string, bool) {
 | 
			
		||||
	params := ctx.QueryAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToString(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// QueryArray gets an array of string values from the query request
 | 
			
		||||
func (ctx *Context) QueryStringSlice(key string, defval []string) ([]string, bool) {
 | 
			
		||||
	params := ctx.QueryAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		return cast.ToStringSlice(vals[0]), true
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
// {{{ url params
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) Param(key string) any {
 | 
			
		||||
	if ctx.params != nil {
 | 
			
		||||
		if val, ok := ctx.params[key]; ok {
 | 
			
		||||
			return val
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) ParamInt(key string, def int) (int, bool) {
 | 
			
		||||
	if val := ctx.Param(key); val != nil {
 | 
			
		||||
		return cast.ToInt(val), true
 | 
			
		||||
	}
 | 
			
		||||
	return def, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) ParamInt64(key string, def int64) (int64, bool) {
 | 
			
		||||
	if val := ctx.Param(key); val != nil {
 | 
			
		||||
		return cast.ToInt64(val), true
 | 
			
		||||
	}
 | 
			
		||||
	return def, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) ParamFloat64(key string, def float64) (float64, bool) {
 | 
			
		||||
	if val := ctx.Param(key); val != nil {
 | 
			
		||||
		return cast.ToFloat64(val), true
 | 
			
		||||
	}
 | 
			
		||||
	return def, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) ParamFloat32(key string, def float32) (float32, bool) {
 | 
			
		||||
	if val := ctx.Param(key); val != nil {
 | 
			
		||||
		return cast.ToFloat32(val), true
 | 
			
		||||
	}
 | 
			
		||||
	return def, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) ParamBool(key string, def bool) (bool, bool) {
 | 
			
		||||
	if val := ctx.Param(key); val != nil {
 | 
			
		||||
		return cast.ToBool(val), true
 | 
			
		||||
	}
 | 
			
		||||
	return def, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) ParamString(key string, def string) (string, bool) {
 | 
			
		||||
	if val := ctx.Param(key); val != nil {
 | 
			
		||||
		return cast.ToString(val), true
 | 
			
		||||
	}
 | 
			
		||||
	return def, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
// {{{ Post form
 | 
			
		||||
 | 
			
		||||
// FormAll gets everything from the submitted form
 | 
			
		||||
func (ctx *Context) FormAll() url.Values {
 | 
			
		||||
	if ctx.request != nil {
 | 
			
		||||
		_ = ctx.request.ParseForm()
 | 
			
		||||
		return ctx.request.PostForm
 | 
			
		||||
	}
 | 
			
		||||
	return url.Values{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FormInt gets an int value from the submitted form
 | 
			
		||||
func (ctx *Context) FormInt(key string, defval int) (int, bool) {
 | 
			
		||||
	params := ctx.FormAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToInt(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) FormInt64(key string, defval int64) (int64, bool) {
 | 
			
		||||
	params := ctx.FormAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToInt64(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) FormBool(key string, defval bool) (bool, bool) {
 | 
			
		||||
	params := ctx.FormAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToBool(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) FormFloat32(key string, defval float32) (float32, bool) {
 | 
			
		||||
	params := ctx.FormAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToFloat32(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) FormFloat64(key string, defval float64) (float64, bool) {
 | 
			
		||||
	params := ctx.FormAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToFloat64(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) FormString(key string, defval string) (string, bool) {
 | 
			
		||||
	params := ctx.FormAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		if len(vals) > 0 {
 | 
			
		||||
			return cast.ToString(vals[0]), true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) FormStringSlice(key string, defval []string) ([]string, bool) {
 | 
			
		||||
	params := ctx.FormAll()
 | 
			
		||||
	if vals, ok := params[key]; ok {
 | 
			
		||||
		return cast.ToStringSlice(vals[0]), true
 | 
			
		||||
	}
 | 
			
		||||
	return defval, false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
// {{{ type binder
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrNoRequest    = errors.New("missing request in the context")
 | 
			
		||||
	ErrNotSingleObj = errors.New("body must have only a single value")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// JSON body
 | 
			
		||||
func (ctx *Context) BindJSON(obj any) error {
 | 
			
		||||
	if ctx.request == nil {
 | 
			
		||||
		return ErrNoRequest
 | 
			
		||||
	}
 | 
			
		||||
	dec := json.NewDecoder(ctx.request.Body)
 | 
			
		||||
	err := dec.Decode(obj)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = dec.Decode(&struct{}{})
 | 
			
		||||
	if err != io.EOF {
 | 
			
		||||
		return ErrNotSingleObj
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// XML body
 | 
			
		||||
func (ctx *Context) BindXML(obj any) error {
 | 
			
		||||
	if ctx.request == nil {
 | 
			
		||||
		return ErrNoRequest
 | 
			
		||||
	}
 | 
			
		||||
	dec := xml.NewDecoder(ctx.request.Body)
 | 
			
		||||
	err := dec.Decode(obj)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	err = dec.Decode(&struct{}{})
 | 
			
		||||
	if err != io.EOF {
 | 
			
		||||
		return ErrNotSingleObj
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RAW body
 | 
			
		||||
func (ctx *Context) GetRawData() ([]byte, error) {
 | 
			
		||||
	if ctx.request == nil {
 | 
			
		||||
		return []byte{}, ErrNoRequest
 | 
			
		||||
	}
 | 
			
		||||
	body, err := io.ReadAll(ctx.request.Body)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return []byte{}, err
 | 
			
		||||
	}
 | 
			
		||||
	/* Restore the body (io.ReadCloser) to it's original state */
 | 
			
		||||
	ctx.request.Body = io.NopCloser(bytes.NewBuffer(body))
 | 
			
		||||
 | 
			
		||||
	return body, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
// {{{ Basic informations
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) Uri() string {
 | 
			
		||||
	return ctx.request.RequestURI
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) Method() string {
 | 
			
		||||
	return ctx.request.Method
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) Host() string {
 | 
			
		||||
	return ctx.request.Host
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) ClientIP() string {
 | 
			
		||||
	r := ctx.request
 | 
			
		||||
	ipAddress := r.Header.Get("X-Real-Ip")
 | 
			
		||||
	if ipAddress == "" {
 | 
			
		||||
		ipAddress = r.Header.Get("X-Forwarded-For")
 | 
			
		||||
	}
 | 
			
		||||
	if ipAddress == "" {
 | 
			
		||||
		ipAddress = r.RemoteAddr
 | 
			
		||||
	}
 | 
			
		||||
	return ipAddress
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
// {{{ Headers
 | 
			
		||||
 | 
			
		||||
// Header
 | 
			
		||||
func (ctx *Context) Headers() map[string][]string {
 | 
			
		||||
	return ctx.request.Header
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) Header(key string) (string, bool) {
 | 
			
		||||
	vals := ctx.request.Header.Values(key)
 | 
			
		||||
	if vals == nil || len(vals) <= 0 {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
	return vals[0], true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
// {{{ Cookies
 | 
			
		||||
 | 
			
		||||
// Cookies gets cookie key-value pairs
 | 
			
		||||
func (ctx *Context) Cookies() map[string]string {
 | 
			
		||||
	cookies := ctx.request.Cookies()
 | 
			
		||||
	ret := map[string]string{}
 | 
			
		||||
	for _, c := range cookies {
 | 
			
		||||
		ret[c.Name] = c.Value
 | 
			
		||||
	}
 | 
			
		||||
	return ret
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) Cookie(key string) (string, bool) {
 | 
			
		||||
	cookies := ctx.Cookies()
 | 
			
		||||
	if val, ok := cookies[key]; ok {
 | 
			
		||||
		return val, true
 | 
			
		||||
	}
 | 
			
		||||
	return "", false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// }}}
 | 
			
		||||
							
								
								
									
										123
									
								
								framework/response.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								framework/response.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,123 @@
 | 
			
		||||
package framework
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"encoding/xml"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type IResponse interface {
 | 
			
		||||
	WriteJSON(status int, obj any) IResponse
 | 
			
		||||
	WriteXML(status int, obj any) IResponse
 | 
			
		||||
	WriteHTML(status int, filepath string, obj any) IResponse
 | 
			
		||||
	WriteText(format string, values ...any) IResponse
 | 
			
		||||
 | 
			
		||||
	Redirect(path string) IResponse
 | 
			
		||||
	SetHeader(key string, val string) IResponse
 | 
			
		||||
	SetCookie(
 | 
			
		||||
		key string,
 | 
			
		||||
		val string,
 | 
			
		||||
		maxAge int,
 | 
			
		||||
		path, domain string,
 | 
			
		||||
		secure, httpOnly bool,
 | 
			
		||||
	) IResponse
 | 
			
		||||
	SetStatus(code int) IResponse
 | 
			
		||||
 | 
			
		||||
	// set 200
 | 
			
		||||
	SetOkStatus() IResponse
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) WriteJSON(status int, obj any) IResponse {
 | 
			
		||||
	// There is a timeout, some error message data must have already been
 | 
			
		||||
	// written to the output. Stop writing anything into the responseWriter.
 | 
			
		||||
	if ctx.HasTimeout() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	data, err := json.Marshal(obj)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ctx.SetStatus(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	ctx.responseWriter.Header().Set("Content-type", "application/json")
 | 
			
		||||
	ctx.responseWriter.WriteHeader(status)
 | 
			
		||||
	_, _ = ctx.responseWriter.Write(data)
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) WriteXML(status int, obj any) IResponse {
 | 
			
		||||
	// There is a timeout, some error message data must have already been
 | 
			
		||||
	// written to the output. Stop writing anything into the responseWriter.
 | 
			
		||||
	if ctx.HasTimeout() {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	data, err := xml.Marshal(obj)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return ctx.SetStatus(http.StatusInternalServerError)
 | 
			
		||||
	}
 | 
			
		||||
	ctx.responseWriter.Header().Set("Content-type", "application/json")
 | 
			
		||||
	ctx.responseWriter.WriteHeader(status)
 | 
			
		||||
	_, _ = ctx.responseWriter.Write(data)
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) WriteHTML(status int, filepath string, obj any) IResponse {
 | 
			
		||||
	ctx.SetHeader("Content-Type", "application/html")
 | 
			
		||||
	t, _ := template.New("output").ParseFiles(filepath)
 | 
			
		||||
	t.Execute(ctx.responseWriter, obj)
 | 
			
		||||
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) WriteText(format string, values ...any) IResponse {
 | 
			
		||||
	out := fmt.Sprintf(format, values...)
 | 
			
		||||
	ctx.SetHeader("Content-Type", "application/text")
 | 
			
		||||
	ctx.responseWriter.Write([]byte(out))
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) Redirect(path string) IResponse {
 | 
			
		||||
	http.Redirect(ctx.responseWriter, ctx.request, path, http.StatusTemporaryRedirect)
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) SetHeader(key string, val string) IResponse {
 | 
			
		||||
	ctx.responseWriter.Header().Add(key, val)
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) SetCookie(
 | 
			
		||||
	key string,
 | 
			
		||||
	val string,
 | 
			
		||||
	maxAge int,
 | 
			
		||||
	path, domain string,
 | 
			
		||||
	secure, httpOnly bool,
 | 
			
		||||
) IResponse {
 | 
			
		||||
	if path == "" {
 | 
			
		||||
		path = "/"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	http.SetCookie(ctx.responseWriter, &http.Cookie{
 | 
			
		||||
		Name:     key,
 | 
			
		||||
		Value:    url.QueryEscape(val),
 | 
			
		||||
		MaxAge:   maxAge,
 | 
			
		||||
		Path:     path,
 | 
			
		||||
		Domain:   domain,
 | 
			
		||||
		SameSite: http.SameSiteDefaultMode,
 | 
			
		||||
		Secure:   secure,
 | 
			
		||||
		HttpOnly: httpOnly,
 | 
			
		||||
	})
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ctx *Context) SetStatus(code int) IResponse {
 | 
			
		||||
	ctx.responseWriter.WriteHeader(code)
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// set 200
 | 
			
		||||
func (ctx *Context) SetOkStatus() IResponse {
 | 
			
		||||
	ctx.responseWriter.WriteHeader(http.StatusOK)
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
@ -14,10 +14,11 @@ func NewTrie() *Trie {
 | 
			
		||||
	return &Trie{root: newNode("")}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Trie) FindRoute(uri string) ControllerHandler {
 | 
			
		||||
func (t *Trie) FindRoute(uri string) *node {
 | 
			
		||||
	uri = strings.ToUpper(uri)
 | 
			
		||||
	uri = strings.TrimPrefix(uri, "/")
 | 
			
		||||
	if uri == "" {
 | 
			
		||||
		return t.root.handler
 | 
			
		||||
		return t.root
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	found := t.root.findRoute(uri)
 | 
			
		||||
@ -25,14 +26,15 @@ func (t *Trie) FindRoute(uri string) ControllerHandler {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return found.handler
 | 
			
		||||
	return found
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (t *Trie) AddRouter(uri string, handler ControllerHandler) error {
 | 
			
		||||
func (t *Trie) AddRouter(uri string, handlers []ControllerHandler) error {
 | 
			
		||||
	uri = strings.ToUpper(uri)
 | 
			
		||||
	uri = strings.TrimPrefix(uri, "/")
 | 
			
		||||
	if uri == "" {
 | 
			
		||||
		t.root.isLast = true
 | 
			
		||||
		t.root.handler = handler
 | 
			
		||||
		t.root.handlers = append(t.root.handlers, handlers...)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -44,7 +46,7 @@ func (t *Trie) AddRouter(uri string, handler ControllerHandler) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// The route does not exist, add it to the tree
 | 
			
		||||
	err := t.root.addRoute(upperUri, handler)
 | 
			
		||||
	err := t.root.addRoute(upperUri, handlers)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@ -54,8 +56,31 @@ func (t *Trie) AddRouter(uri string, handler ControllerHandler) error {
 | 
			
		||||
type node struct {
 | 
			
		||||
	isLast   bool
 | 
			
		||||
	segment  string
 | 
			
		||||
	handler  ControllerHandler
 | 
			
		||||
	handlers []ControllerHandler
 | 
			
		||||
	children []*node
 | 
			
		||||
	parent   *node
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *node) ParseParamsFromEndNode(uri string) map[string]string {
 | 
			
		||||
	ret := map[string]string{}
 | 
			
		||||
	uri = strings.ToUpper(uri)
 | 
			
		||||
	uri = strings.TrimPrefix(uri, "/")
 | 
			
		||||
	if uri == "" {
 | 
			
		||||
		// root
 | 
			
		||||
		return ret
 | 
			
		||||
	}
 | 
			
		||||
	segments := strings.Split(uri, "/")
 | 
			
		||||
	cnt := len(segments)
 | 
			
		||||
	cur := n
 | 
			
		||||
 | 
			
		||||
	for i := cnt - 1; i >= 0; i-- {
 | 
			
		||||
		if isWildcard(cur.segment) {
 | 
			
		||||
			// set params
 | 
			
		||||
			ret[cur.segment[1:]] = segments[i]
 | 
			
		||||
		}
 | 
			
		||||
		cur = cur.parent
 | 
			
		||||
	}
 | 
			
		||||
	return ret
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func newNode(segment string) *node {
 | 
			
		||||
@ -109,7 +134,7 @@ func (n *node) findRoute(uri string) *node {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (n *node) addRoute(uri string, handler ControllerHandler) error {
 | 
			
		||||
func (n *node) addRoute(uri string, handlers []ControllerHandler) error {
 | 
			
		||||
	splitted := strings.SplitN(uri, "/", 2)
 | 
			
		||||
	splittedLen := len(splitted)
 | 
			
		||||
	isLast := splittedLen == 1
 | 
			
		||||
@ -125,27 +150,28 @@ func (n *node) addRoute(uri string, handler ControllerHandler) error {
 | 
			
		||||
				} else {
 | 
			
		||||
					// otherwise, set the child
 | 
			
		||||
					child.isLast = true
 | 
			
		||||
					child.handler = handler
 | 
			
		||||
					child.handlers = append(child.handlers, handlers...)
 | 
			
		||||
					return nil
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			// More segments to check
 | 
			
		||||
			return child.addRoute(splitted[1], handler)
 | 
			
		||||
			return child.addRoute(splitted[1], handlers)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// create a new node
 | 
			
		||||
	new := newNode(splitted[0])
 | 
			
		||||
	new.parent = n
 | 
			
		||||
	if isLast {
 | 
			
		||||
		// this is the end
 | 
			
		||||
		new.handler = handler
 | 
			
		||||
		new.handlers = append(new.handlers, handlers...)
 | 
			
		||||
		new.isLast = true
 | 
			
		||||
		n.children = append(n.children, new)
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	// continue
 | 
			
		||||
	new.isLast = false
 | 
			
		||||
	err := new.addRoute(splitted[1], handler)
 | 
			
		||||
	err := new.addRoute(splitted[1], handlers)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							@ -1,3 +1,5 @@
 | 
			
		||||
module git.vinchent.xyz/vinchent/go-web
 | 
			
		||||
 | 
			
		||||
go 1.22.5
 | 
			
		||||
 | 
			
		||||
require github.com/spf13/cast v1.7.0
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 | 
			
		||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 | 
			
		||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 | 
			
		||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 | 
			
		||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 | 
			
		||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 | 
			
		||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 | 
			
		||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
 | 
			
		||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
 | 
			
		||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 | 
			
		||||
@ -58,6 +58,7 @@ func FooControllerHandler(ctx *framework.Context) error {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UserLoginController(ctx *framework.Context) error {
 | 
			
		||||
	time.Sleep(10 * time.Second)
 | 
			
		||||
	ctx.WriteJSON(http.StatusOK, "ok")
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -74,6 +75,7 @@ func SubjectUpdateController(ctx *framework.Context) error {
 | 
			
		||||
 | 
			
		||||
func SubjectGetController(ctx *framework.Context) error {
 | 
			
		||||
	ctx.WriteJSON(http.StatusAccepted, "got")
 | 
			
		||||
	log.Println(ctx.ParamInt("ID", 0))
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										25
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								main.go
									
									
									
									
									
								
							@ -1,8 +1,14 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.vinchent.xyz/vinchent/go-web/framework"
 | 
			
		||||
)
 | 
			
		||||
@ -15,7 +21,22 @@ func main() {
 | 
			
		||||
		Handler: core,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := server.ListenAndServe(); err != nil {
 | 
			
		||||
		log.Panic(err)
 | 
			
		||||
	go func() {
 | 
			
		||||
		server.ListenAndServe()
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	// create quit channel
 | 
			
		||||
	quit := make(chan os.Signal, 1)
 | 
			
		||||
 | 
			
		||||
	// set notifier
 | 
			
		||||
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
 | 
			
		||||
 | 
			
		||||
	<-quit
 | 
			
		||||
	fmt.Println("YOLO")
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 | 
			
		||||
	defer cancel()
 | 
			
		||||
	if err := server.Shutdown(ctx); err != nil {
 | 
			
		||||
		log.Fatal("server shutdown: ", err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,21 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import "git.vinchent.xyz/vinchent/go-web/framework"
 | 
			
		||||
import (
 | 
			
		||||
	"git.vinchent.xyz/vinchent/go-web/framework"
 | 
			
		||||
	"git.vinchent.xyz/vinchent/go-web/framework/middleware"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func registerRouter(core *framework.Core) {
 | 
			
		||||
	core.Use(middleware.Test1(), middleware.Test2())
 | 
			
		||||
	core.Get("/user/login", UserLoginController)
 | 
			
		||||
 | 
			
		||||
	subjectApi := core.Group("/subject")
 | 
			
		||||
	{
 | 
			
		||||
		subjectApi.Use(middleware.Test3())
 | 
			
		||||
		subjectApi.Delete("/:id", SubjectDelController)
 | 
			
		||||
		subjectApi.Put("/:id", SubjectUpdateController)
 | 
			
		||||
		subjectApi.Get("/:id", SubjectGetController)
 | 
			
		||||
		subjectApi.Get("/:id/test", SubjectGetController)
 | 
			
		||||
		subjectApi.Get("/list/all", SubjectListController)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user