Compare commits

..

No commits in common. "master" and "v0.0.1" have entirely different histories.

9 changed files with 72 additions and 448 deletions

29
LICENSE
View file

@ -1,29 +0,0 @@
BSD 3-Clause License
Copyright (c) 2018, Fuyu
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

159
README.md
View file

@ -1,159 +0,0 @@
# router
Router is a HTTP router for Golang. It's built on [httprouter](https://github.com/julienschmidt/httprouter) and takes inspiration from [labstack/echo](https://github.com/labstack/echo), but with reduced complexity and easier data binding.
The data binding is made easier by specifying your input as a parameter to your function
Example:
```golang
type someType struct {
A string `json:"a"`
B int `json:"b"`
}
func handlePOST(c *router.Context, input someType) error {
fmt.Println(input)
return c.NoContent(200)
}
```
### Why make data binding shorter?
Many applications read, bind and validate data for most calls. In Echo this could mean adding boilerplate code to every call. This extra boilerplate code can make your code significantly longer and very hard to read.
```golang
func handlePOST(c *echo.Context) error {
var input someType
err := c.Bind(&input)
if err != nil {
return c.NoContent(http.StatusBadRequest)
}
err = c.Validate(input)
if err != nil {
return c.NoContent(http.StatusBadRequest)
}
// Your actual code
}
```
In router you define your code to read, bind and validate the input once, and it applies to every POST, PATCH and PUT call.
### What about the performance of dynamic parameters?
While using dynamic parameters takes a bit of extra processing, this barely has any impact on performance. Router can still handle tens to hunderds of thousands of requests per second.
### How does middleware work?
Middleware works similar to most other routers. The dynamic parameters has no effect on middleware, input data is parsed after all middlewares and right before your handler.
## Installation
```bash
go get git.fuyu.moe/Fuyu/router
```
## Getting started
```golang
package main
import "git.fuyu.moe/Fuyu/router"
func main() {
// Create a router instance
r := router.New()
// Add routes
r.GET(`/`, yourGetFunc)
r.POST(`/`, yourPostFunc)
// Start router
panic(r.Start(`127.0.0.1:8080`))
}
```
## Advice
### Configuration
For a serious project you should set `r.Reader`, `r.ErrorHandler`, `r.NotFoundHandler`, and `r.MethodNotAllowedHandler`.
### Templating
You can set `r.Renderer` and call `c.Render(code, tmplName, data)` in your handlers
## Examples
```golang
package main
import (
"fmt"
"time"
"git.fuyu.moe/Fuyu/router"
)
type product struct {
Name string `json:"name"`
}
func main() {
r := router.New()
r.Use(accessLog)
r.GET(`/hello`, hello)
a := r.Group(`/api`, gandalf)
a.POST(`/product`, createProduct)
a.PATCH(`/product/:id`, updateProduct)
a.POST(`/logout`, logout)
r.Start(`:8080`)
}
func accessLog(next router.Handle) router.Handle {
return func(c *router.Context) error {
t := time.Now()
err := next(c)
fmt.Println(c.Request.Method, c.Request.URL.Path, c.Request.RemoteAddr, t, time.Since(t))
return err
}
}
func gandalf(next router.Handle) router.Handle {
youShallPass := false
return func(c *router.Context) error {
if !youShallPass {
return c.String(401, `You shall not pass`)
}
return next(c)
}
}
func hello(c *router.Context) error {
return c.String(200, `Hello`)
}
func createProduct(c *router.Context, p product) error {
return c.JSON(200, p)
}
func updateProduct(c *router.Context, p product) error {
productID := c.Param(`id`)
return c.String(200, fmt.Sprintf(
`ProductID %d new name %s`, productID, p.Name,
))
}
func logout(c *router.Context) error {
return c.String(200, `logout`)
}
```

View file

@ -1,12 +1,8 @@
package router
import (
"bytes"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"github.com/julienschmidt/httprouter"
)
@ -20,29 +16,11 @@ type Context struct {
store map[string]interface{}
}
// NewContext creates a new context, this function is only exported for use in tests
func NewContext(router *Router, res http.ResponseWriter, req *http.Request, param httprouter.Params) *Context {
func newContext(router *Router, res http.ResponseWriter, req *http.Request, param httprouter.Params) *Context {
return &Context{router, req, res, param.ByName, make(map[string]interface{})}
}
// QueryParam returns the specified parameter from the query string.
// Returns an empty string if it doesn't exist. Returns the first parameter if multiple instances exist
func (c *Context) QueryParam(param string) string {
params := c.Request.URL.Query()[param]
if params == nil {
return ``
}
return params[0]
}
// Redirect sends a redirect to the client
func (c *Context) Redirect(code int, url string) error {
http.Redirect(c.Response, c.Request, url, code)
return nil
}
// Bytes returns the given status code and writes the bytes to the body
// 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)
@ -51,17 +29,11 @@ func (c *Context) Bytes(code int, b []byte) error {
// String returns the given status code and writes the string to the body
func (c *Context) String(code int, s string) error {
c.Response.Header().Set(`Content-Type`, `text/plain`)
c.Response.WriteHeader(code)
_, err := c.Response.Write([]byte(s))
return err
}
// StatusText returns the given status code with the matching status text
func (c *Context) StatusText(code int) error {
return c.String(code, http.StatusText(code))
}
// NoContent returns the given status code without writing anything to the body
func (c *Context) NoContent(code int) error {
c.Response.WriteHeader(code)
@ -70,27 +42,17 @@ func (c *Context) NoContent(code int) error {
// JSON returns the given status code and writes JSON to the body
func (c *Context) JSON(code int, data interface{}) error {
c.Response.Header().Set(`Content-Type`, `application/json`)
c.Response.WriteHeader(code)
return json.NewEncoder(c.Response).Encode(data) // TODO: Encode to buffer first to prevent partial responses on error
}
// Render renders a templating using the Renderer set in router
func (c *Context) Render(code int, template string, data interface{}) error {
if c.router.Renderer == nil {
panic(`Cannot call render without a renderer set`)
}
var b bytes.Buffer
err := c.router.Renderer.Render(&b, template, data, c)
if err != nil {
return err
}
c.Response.Header().Set(`Content-Type`, `text/html`)
c.Response.WriteHeader(code)
_, _ = io.Copy(c.Response, &b)
return nil
return c.router.Renderer.Render(c.Response, template, data, c)
}
// Set sets a value in the context. Set is not safe to be used concurrently
@ -102,20 +64,3 @@ func (c *Context) Set(key string, value interface{}) {
func (c *Context) Get(key string) interface{} {
return c.store[key]
}
// RealIP uses proxy headers for the real ip, if none exist the IP of the current connection is returned
func (c *Context) RealIP() string {
reqIP := c.Request.RemoteAddr
if ip := c.Request.Header.Get(`X-Forwarded-For`); ip != `` {
reqIP = strings.Split(ip, `, `)[0]
} else if ip := c.Request.Header.Get(`X-Real-IP`); ip != `` {
reqIP = ip
}
ra, _, _ := net.SplitHostPort(reqIP)
if ra != `` {
reqIP = ra
}
return reqIP
}

View file

@ -1,27 +0,0 @@
package router
import (
"encoding/json"
"net/http"
)
func defaultNotFoundHandler(c *Context) error {
return c.StatusText(http.StatusNotFound)
}
func defaultMethodNotAllowedHandler(c *Context) error {
return c.StatusText(http.StatusMethodNotAllowed)
}
func defaultErrorHandler(c *Context, err interface{}) {
_ = c.StatusText(http.StatusInternalServerError)
}
func defaultReader(c *Context, dst interface{}) (bool, error) {
err := json.NewDecoder(c.Request.Body).Decode(dst)
if err != nil {
return false, c.StatusText(http.StatusBadRequest)
}
return true, nil
}

5
go.mod
View file

@ -1,5 +0,0 @@
module git.fuyu.moe/Fuyu/router
go 1.13
require github.com/julienschmidt/httprouter v1.3.0

2
go.sum
View file

@ -1,2 +0,0 @@
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=

View file

@ -6,49 +6,48 @@ func join(prefix, path string) string {
return urlpath.Join(prefix, urlpath.Clean(path))
}
// Group is a router group with a shared prefix and set of middlewares
type Group struct {
router *Router
prefix string
middleware []Middleware
}
// Group creates a new router group with a shared prefix and set of middlewares
func (g *Group) Group(prefix string, middleware ...Middleware) *Group {
return &Group{prefix: join(g.prefix, prefix), router: g.router, middleware: append(g.middleware, middleware...)}
func (g *Group) Group(prefix string) *Group {
return &Group{prefix: join(g.prefix, prefix), router: g.router}
}
// GET adds a GET route
func (g *Group) GET(path string, handle Handle, middleware ...Middleware) {
g.router.GET(join(g.prefix, path), handle, append(g.middleware, middleware...)...)
func (g *Group) GET(path string, handle GetHandle) {
g.router.GET(join(g.prefix, path), handle)
}
// POST adds a POST route
func (g *Group) POST(path string, handle interface{}, middleware ...Middleware) {
g.router.POST(join(g.prefix, path), handle, append(g.middleware, middleware...)...)
func (g *Group) POST(path string, handle interface{}) {
g.router.POST(join(g.prefix, path), handle)
}
// DELETE adds a DELETE route
func (g *Group) DELETE(path string, handle Handle, middleware ...Middleware) {
g.router.DELETE(join(g.prefix, path), handle, append(g.middleware, middleware...)...)
func (g *Group) DELETE(path string, handle GetHandle) {
g.router.DELETE(join(g.prefix, path), handle)
}
// PUT adds a PUT route
func (g *Group) PUT(path string, handle interface{}, middleware ...Middleware) {
g.router.PUT(join(g.prefix, path), handle, append(g.middleware, middleware...)...)
func (g *Group) PUT(path string, handle interface{}) {
checkInterfaceHandle(handle)
g.router.PUT(join(g.prefix, path), handle)
}
// PATCH adds a PATCH route
func (g *Group) PATCH(path string, handle interface{}, middleware ...Middleware) {
g.router.PATCH(join(g.prefix, path), handle, append(g.middleware, middleware...)...)
func (g *Group) PATCH(path string, handle interface{}) {
checkInterfaceHandle(handle)
g.router.PATCH(join(g.prefix, path), handle)
}
// HEAD adds a HEAD route
func (g *Group) HEAD(path string, handle Handle, middleware ...Middleware) {
g.router.HEAD(join(g.prefix, path), handle, append(g.middleware, middleware...)...)
func (g *Group) HEAD(path string, handle GetHandle) {
g.router.HEAD(join(g.prefix, path), handle)
}
// OPTIONS adds a OPTIONS route
func (g *Group) OPTIONS(path string, handle Handle, middleware ...Middleware) {
g.router.OPTIONS(join(g.prefix, path), handle, append(g.middleware, middleware...)...)
func (g *Group) OPTIONS(path string, handle GetHandle) {
g.router.OPTIONS(join(g.prefix, path), handle)
}

185
router.go
View file

@ -1,12 +1,10 @@
package router
import (
"context"
"crypto/tls"
"net"
"encoding/json"
"fmt"
"net/http"
"reflect"
"time"
"github.com/julienschmidt/httprouter"
)
@ -15,174 +13,88 @@ type route struct {
Method string
Path string
Handle interface{}
Middleware []Middleware
}
// Handle handles a request
type Handle = func(*Context) error
// ErrorHandle handles a request
type ErrorHandle func(*Context, interface{})
// Middleware is a function that runs before your route, it gets the next handler as a parameter
type Middleware func(Handle) Handle
// Reader reads input to dst, returns true if successful
type Reader func(c *Context, dst interface{}) (bool, error)
// 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
Reader Reader
Renderer Renderer
middleware []Middleware
NotFoundHandler Handle
MethodNotAllowedHandler Handle
ErrorHandler ErrorHandle
TrimTrailingSlashes bool
server *http.Server
}
// New returns a new Router
func New() *Router {
return &Router{Reader: defaultReader, NotFoundHandler: defaultNotFoundHandler, MethodNotAllowedHandler: defaultMethodNotAllowedHandler, ErrorHandler: defaultErrorHandler}
return &Router{}
}
// Use adds a global middleware
func (r *Router) Use(m ...Middleware) {
r.middleware = append(r.middleware, m...)
}
// Group creates a new router group with a shared prefix and set of middlewares
func (r *Router) Group(prefix string, middleware ...Middleware) *Group {
return &Group{prefix: prefix, router: r, middleware: middleware}
func (r *Router) Group(prefix string) *Group {
return &Group{prefix: prefix, router: r}
}
// GET adds a GET route
func (r *Router) GET(path string, handle Handle, middleware ...Middleware) {
r.routes = append(r.routes, route{`GET`, path, handle, middleware})
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{}, middleware ...Middleware) {
func (r *Router) POST(path string, handle interface{}) {
checkInterfaceHandle(handle)
r.routes = append(r.routes, route{`POST`, path, handle, middleware})
r.routes = append(r.routes, route{`POST`, path, handle})
}
// DELETE adds a DELETE route
func (r *Router) DELETE(path string, handle Handle, middleware ...Middleware) {
r.routes = append(r.routes, route{`DELETE`, path, handle, middleware})
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{}, middleware ...Middleware) {
func (r *Router) PUT(path string, handle interface{}) {
checkInterfaceHandle(handle)
r.routes = append(r.routes, route{`PUT`, path, handle, middleware})
r.routes = append(r.routes, route{`PUT`, path, handle})
}
// PATCH adds a PATCH route
func (r *Router) PATCH(path string, handle interface{}, middleware ...Middleware) {
func (r *Router) PATCH(path string, handle interface{}) {
checkInterfaceHandle(handle)
r.routes = append(r.routes, route{`PATCH`, path, handle, middleware})
r.routes = append(r.routes, route{`PATCH`, path, handle})
}
// HEAD adds a HEAD route
func (r *Router) HEAD(path string, handle Handle, middleware ...Middleware) {
r.routes = append(r.routes, route{`HEAD`, path, handle, middleware})
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 Handle, middleware ...Middleware) {
r.routes = append(r.routes, route{`OPTIONS`, path, handle, middleware})
}
// Serve accepts incoming HTTP connections on the listener
func (r *Router) Serve(l net.Listener) error {
httpr := r.getHttpr()
r.server = &http.Server{Handler: httpr}
return r.server.Serve(l)
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()
r.server = &http.Server{Addr: addr, Handler: httpr}
return r.server.ListenAndServe()
return http.ListenAndServe(addr, httpr)
}
// StartTLS starts a TLS web server using the given key, cert and config and binds to the given address
func (r *Router) StartTLS(addr, certFile, keyFile string, conf *tls.Config) error {
httpr := r.getHttpr()
r.server = &http.Server{Addr: addr, Handler: httpr, TLSConfig: conf}
return r.server.ListenAndServeTLS(certFile, keyFile)
}
// Stop stops the web server
func (r *Router) Stop() error {
if r.server == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
err := r.server.Shutdown(ctx)
if err == context.DeadlineExceeded {
err = r.server.Close()
}
r.server = nil
return err
}
func (r *Router) getHttpr() http.Handler {
func (r *Router) getHttpr() *httprouter.Router {
httpr := httprouter.New()
for _, v := range r.routes {
handle, ok := v.Handle.(Handle)
if !ok {
handle = handlePOST(r, v.Handle)
if handle, ok := v.Handle.(GetHandle); ok {
httpr.Handle(v.Method, v.Path, handleGET(r, handle))
continue
}
middleware := make([]Middleware, len(r.middleware)+len(v.Middleware))
copy(middleware, r.middleware)
copy(middleware[len(r.middleware):], v.Middleware)
httpr.Handle(v.Method, v.Path, handleReq(r, handle, middleware))
}
httpr.NotFound = http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
handleReq(r, r.NotFoundHandler, r.middleware)(res, req, nil)
})
httpr.MethodNotAllowed = http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
handleReq(r, r.MethodNotAllowedHandler, r.middleware)(res, req, nil)
})
httpr.PanicHandler = func(res http.ResponseWriter, req *http.Request, err interface{}) {
c := NewContext(r, res, req, nil)
r.ErrorHandler(c, err)
}
if r.TrimTrailingSlashes {
httpr.RedirectTrailingSlash = false
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
l := len(req.URL.Path)
if l > 1 && req.URL.Path[l-1] == '/' {
req.URL.Path = req.URL.Path[:l-1]
}
httpr.ServeHTTP(w, req)
})
httpr.Handle(v.Method, v.Path, handlePOST(r, v.Handle))
}
return httpr
}
func checkInterfaceHandle(f interface{}) {
if _, ok := f.(Handle); ok {
if _, ok := f.(GetHandle); ok {
return
}
@ -203,47 +115,38 @@ func checkInterfaceHandle(f interface{}) {
if rt.In(0) != reflect.TypeOf(&Context{}) {
panic(`handle should accept Context as first argument`)
}
return
}
func handlePOST(r *Router, f interface{}) Handle {
func handlePOST(r *Router, f interface{}) httprouter.Handle {
funcRv, inputRt := reflect.ValueOf(f), reflect.TypeOf(f).In(1)
return func(c *Context) error {
data := reflect.New(inputRt)
return func(res http.ResponseWriter, req *http.Request, param httprouter.Params) {
c := newContext(r, res, req, param)
if r.Reader != nil {
ok, err := r.Reader(c, data.Interface())
_ = c.Request.Body.Close()
data := reflect.New(inputRt)
{
err := json.NewDecoder(req.Body).Decode(data.Interface())
req.Body.Close()
if err != nil {
return err
}
if !ok {
return nil
c.NoContent(400) // TODO: send info about error (BindError)
return
}
}
out := funcRv.Call([]reflect.Value{reflect.ValueOf(c), data.Elem()})
if out[0].IsNil() {
return nil
}
return out[0].Interface().(error)
err := out[0].Interface()
_ = err
}
}
func handleReq(r *Router, handle Handle, m []Middleware) httprouter.Handle {
func handleGET(r *Router, f GetHandle) httprouter.Handle {
return func(res http.ResponseWriter, req *http.Request, param httprouter.Params) {
c := NewContext(r, res, req, param)
f := handle
for i := len(m) - 1; i >= 0; i-- {
f = m[i](f)
}
c := newContext(r, res, req, param)
err := f(c)
if err != nil {
r.ErrorHandler(c, err)
}
fmt.Println(err)
}
}

View file

@ -4,7 +4,6 @@ import (
"io"
)
// Renderer renders a template
type Renderer interface {
Render(w io.Writer, template string, data interface{}, c *Context) error
}