commit f4d27689e76b0206e52432ca58812adafa31fbec Author: NiseVoid Date: Fri Aug 3 21:29:15 2018 +0200 Initial commit diff --git a/context.go b/context.go new file mode 100644 index 0000000..4ca32f7 --- /dev/null +++ b/context.go @@ -0,0 +1,56 @@ +package router + +import ( + "encoding/json" + "net/http" + + "github.com/julienschmidt/httprouter" +) + +// Context is passed to handlers and middlewares +type Context struct { + Request *http.Request + Response http.ResponseWriter + Param func(string) string + store map[string]interface{} +} + +func newContext(res http.ResponseWriter, req *http.Request, param httprouter.Params) *Context { + return &Context{req, res, param.ByName, make(map[string]interface{})} +} + +// String returns the given status code and writes the bytes to the body +func (c *Context) Bytes(code int, b []byte) error { + c.Response.WriteHeader(code) + _, err := c.Response.Write(b) + return err +} + +// String returns the given status code and writes the string to the body +func (c *Context) String(code int, s string) error { + c.Response.WriteHeader(code) + _, err := c.Response.Write([]byte(s)) + return err +} + +// NoContent returns the given status code without writing anything to the body +func (c *Context) NoContent(code int) error { + c.Response.WriteHeader(code) + return nil +} + +// JSON returns the given status code and writes JSON to the body +func (c *Context) JSON(code int, data interface{}) error { + c.Response.WriteHeader(code) + return json.NewEncoder(c.Response).Encode(data) // TODO: Encode to buffer first to prevent partial responses on error +} + +// Set sets a value in the context. Set is not safe to be used concurrently +func (c *Context) Set(key string, value interface{}) { + c.store[key] = value +} + +// Get retrieves a value from the context. +func (c *Context) Get(key string) interface{} { + return c.store[key] +} diff --git a/router.go b/router.go new file mode 100644 index 0000000..df9d566 --- /dev/null +++ b/router.go @@ -0,0 +1,148 @@ +package router + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + + "github.com/julienschmidt/httprouter" +) + +type route struct { + Method string + Path string + Handle interface{} +} + +// GetHandle handles a request that doesn't receive a body +type GetHandle func(*Context) error + +// Router is the router itself +type Router struct { + routes []route + Port int +} + +// New returns a new Router +func New() *Router { + return &Router{} +} + +// GET adds a GET route +func (r *Router) GET(path string, handle GetHandle) { + r.routes = append(r.routes, route{`GET`, path, handle}) +} + +// POST adds a POST route +func (r *Router) POST(path string, handle interface{}) { + checkInterfaceHandle(handle) + r.routes = append(r.routes, route{`POST`, path, handle}) +} + +// DELETE adds a DELETE route +func (r *Router) DELETE(path string, handle GetHandle) { + r.routes = append(r.routes, route{`DELETE`, path, handle}) +} + +// PUT adds a PUT route +func (r *Router) PUT(path string, handle interface{}) { + checkInterfaceHandle(handle) + r.routes = append(r.routes, route{`PUT`, path, handle}) +} + +// PATCH adds a PATCH route +func (r *Router) PATCH(path string, handle interface{}) { + checkInterfaceHandle(handle) + r.routes = append(r.routes, route{`PATCH`, path, handle}) +} + +// HEAD adds a HEAD route +func (r *Router) HEAD(path string, handle GetHandle) { + r.routes = append(r.routes, route{`HEAD`, path, handle}) +} + +// OPTIONS adds a OPTIONS route +func (r *Router) OPTIONS(path string, handle GetHandle) { + r.routes = append(r.routes, route{`OPTIONS`, path, handle}) +} + +// Start starts the web server and binds to the given address +func (r *Router) Start(addr string) error { + httpr := r.getHttpr() + + return http.ListenAndServe(addr, httpr) +} + +func (r *Router) getHttpr() *httprouter.Router { + httpr := httprouter.New() + + for _, v := range r.routes { + if handle, ok := v.Handle.(GetHandle); ok { + httpr.Handle(v.Method, v.Path, handleGET(handle)) + continue + } + + httpr.Handle(v.Method, v.Path, handlePOST(v.Handle)) + } + + return httpr +} + +func checkInterfaceHandle(f interface{}) { + if _, ok := f.(GetHandle); ok { + return + } + + rt := reflect.TypeOf(f) + + if rt.Kind() != reflect.Func { + panic(`non-func handle`) + } + + if rt.NumIn() != 2 { + panic(`handle should take 2 arguments`) + } + + if rt.NumOut() != 1 || rt.Out(0).Name() != `error` { + panic(`handle should return only error`) + } + + if rt.In(0) != reflect.TypeOf(&Context{}) { + panic(`handle should accept Context as first argument`) + } + + return +} + +func handlePOST(f interface{}) httprouter.Handle { + funcRv, inputRt := reflect.ValueOf(f), reflect.TypeOf(f).In(1) + + return func(res http.ResponseWriter, req *http.Request, param httprouter.Params) { + c := newContext(res, req, param) + + data := reflect.New(inputRt) + { + err := json.NewDecoder(req.Body).Decode(data.Interface()) + req.Body.Close() + if err != nil { + c.NoContent(400) // TODO: send info about error (BindError) + return + } + } + + out := funcRv.Call([]reflect.Value{reflect.ValueOf(c), data.Elem()}) + err := out[0].Interface() + _ = err + } +} + +func handleGET(f GetHandle) httprouter.Handle { + return func(res http.ResponseWriter, req *http.Request, param httprouter.Params) { + c := newContext(res, req, param) + + err := f(c) + + fmt.Println(err) + } +}