forked from Fuyu/router
Compare commits
18 commits
Author | SHA1 | Date | |
---|---|---|---|
152bf49c4b | |||
283c75b32e | |||
66ae2a435c | |||
5b5a102c71 | |||
d504c9d2b5 | |||
2a612eb82f | |||
317961ab6e | |||
60f82143da | |||
15df08f21b | |||
717c6f65fc | |||
890ff550eb | |||
0990f5bafe | |||
4cc4ce2a2e | |||
d30d304cea | |||
a16ee2740c | |||
4c83818ecd | |||
2c930bacf1 | |||
45ec83b9d3 |
7 changed files with 302 additions and 33 deletions
159
README.md
Normal file
159
README.md
Normal file
|
@ -0,0 +1,159 @@
|
|||
# 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`)
|
||||
}
|
||||
```
|
58
context.go
58
context.go
|
@ -1,8 +1,12 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
@ -16,11 +20,29 @@ type Context struct {
|
|||
store map[string]interface{}
|
||||
}
|
||||
|
||||
func newContext(router *Router, res http.ResponseWriter, req *http.Request, param httprouter.Params) *Context {
|
||||
// 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 {
|
||||
return &Context{router, req, res, param.ByName, make(map[string]interface{})}
|
||||
}
|
||||
|
||||
// String returns the given status code and writes the bytes to the body
|
||||
// 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
|
||||
func (c *Context) Bytes(code int, b []byte) error {
|
||||
c.Response.WriteHeader(code)
|
||||
_, err := c.Response.Write(b)
|
||||
|
@ -35,6 +57,11 @@ func (c *Context) String(code int, s string) error {
|
|||
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)
|
||||
|
@ -48,14 +75,22 @@ func (c *Context) JSON(code int, data interface{}) error {
|
|||
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)
|
||||
return c.router.Renderer.Render(c.Response, template, data, c)
|
||||
_, _ = io.Copy(c.Response, &b)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set sets a value in the context. Set is not safe to be used concurrently
|
||||
|
@ -67,3 +102,20 @@ 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
|
||||
}
|
||||
|
|
16
default.go
16
default.go
|
@ -2,28 +2,26 @@ package router
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func defaultNotFoundHandler(c *Context) error {
|
||||
return c.String(404, `not found`)
|
||||
return c.StatusText(http.StatusNotFound)
|
||||
}
|
||||
|
||||
func defaultMethodNotAllowedHandler(c *Context) error {
|
||||
return c.String(504, `method not allowed`)
|
||||
return c.StatusText(http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
func defaultErrorHandler(c *Context, err interface{}) {
|
||||
fmt.Println(err)
|
||||
c.String(500, `internal server error`)
|
||||
_ = c.StatusText(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func defaultReader(c *Context, dst interface{}) bool {
|
||||
func defaultReader(c *Context, dst interface{}) (bool, error) {
|
||||
err := json.NewDecoder(c.Request.Body).Decode(dst)
|
||||
if err != nil {
|
||||
c.NoContent(400)
|
||||
return false
|
||||
return false, c.StatusText(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
|
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
|||
module git.fuyu.moe/Fuyu/router
|
||||
|
||||
go 1.13
|
||||
|
||||
require github.com/julienschmidt/httprouter v1.3.0
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -0,0 +1,2 @@
|
|||
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
94
router.go
94
router.go
|
@ -1,8 +1,12 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
@ -20,11 +24,11 @@ type Handle = func(*Context) error
|
|||
// ErrorHandle handles a request
|
||||
type ErrorHandle func(*Context, interface{})
|
||||
|
||||
// Middleware TODO:
|
||||
// Middleware is a function that runs before your route, it gets the next handler as a parameter
|
||||
type Middleware func(Handle) Handle
|
||||
|
||||
// Binder reads input to dst, returns true is successful
|
||||
type Reader func(c *Context, dst interface{}) bool
|
||||
// Reader reads input to dst, returns true if successful
|
||||
type Reader func(c *Context, dst interface{}) (bool, error)
|
||||
|
||||
// Router is the router itself
|
||||
type Router struct {
|
||||
|
@ -35,6 +39,8 @@ type Router struct {
|
|||
NotFoundHandler Handle
|
||||
MethodNotAllowedHandler Handle
|
||||
ErrorHandler ErrorHandle
|
||||
TrimTrailingSlashes bool
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// New returns a new Router
|
||||
|
@ -90,14 +96,48 @@ 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
r.server = &http.Server{Addr: addr, Handler: httpr}
|
||||
return r.server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (r *Router) getHttpr() *httprouter.Router {
|
||||
// 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 {
|
||||
httpr := httprouter.New()
|
||||
|
||||
for _, v := range r.routes {
|
||||
|
@ -106,7 +146,11 @@ func (r *Router) getHttpr() *httprouter.Router {
|
|||
handle = handlePOST(r, v.Handle)
|
||||
}
|
||||
|
||||
httpr.Handle(v.Method, v.Path, handleReq(r, handle, append(r.middleware, v.Middleware...)))
|
||||
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) {
|
||||
|
@ -118,18 +162,23 @@ func (r *Router) getHttpr() *httprouter.Router {
|
|||
})
|
||||
|
||||
httpr.PanicHandler = func(res http.ResponseWriter, req *http.Request, err interface{}) {
|
||||
c := newContext(r, res, req, nil)
|
||||
c := NewContext(r, res, req, nil)
|
||||
r.ErrorHandler(c, err)
|
||||
}
|
||||
|
||||
return httpr
|
||||
}
|
||||
if r.TrimTrailingSlashes {
|
||||
httpr.RedirectTrailingSlash = false
|
||||
|
||||
func handleErr(errHandler ErrorHandle, err interface{}) Handle {
|
||||
return func(c *Context) error {
|
||||
errHandler(c, err)
|
||||
return nil
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
return httpr
|
||||
}
|
||||
|
||||
func checkInterfaceHandle(f interface{}) {
|
||||
|
@ -154,8 +203,6 @@ 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 {
|
||||
|
@ -164,11 +211,16 @@ func handlePOST(r *Router, f interface{}) Handle {
|
|||
return func(c *Context) error {
|
||||
data := reflect.New(inputRt)
|
||||
|
||||
if !r.Reader(c, data.Interface()) {
|
||||
c.Request.Body.Close()
|
||||
return nil
|
||||
if r.Reader != nil {
|
||||
ok, err := r.Reader(c, data.Interface())
|
||||
_ = c.Request.Body.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
c.Request.Body.Close()
|
||||
|
||||
out := funcRv.Call([]reflect.Value{reflect.ValueOf(c), data.Elem()})
|
||||
|
||||
|
@ -181,10 +233,10 @@ func handlePOST(r *Router, f interface{}) Handle {
|
|||
|
||||
func handleReq(r *Router, handle Handle, m []Middleware) httprouter.Handle {
|
||||
return func(res http.ResponseWriter, req *http.Request, param httprouter.Params) {
|
||||
c := newContext(r, res, req, param)
|
||||
c := NewContext(r, res, req, param)
|
||||
|
||||
f := handle
|
||||
for i := len(m) - 1; i >= 0; i-- { // TODO: 1,2,3 of 3,2,1
|
||||
for i := len(m) - 1; i >= 0; i-- {
|
||||
f = m[i](f)
|
||||
}
|
||||
|
||||
|
|
1
type.go
1
type.go
|
@ -4,6 +4,7 @@ import (
|
|||
"io"
|
||||
)
|
||||
|
||||
// Renderer renders a template
|
||||
type Renderer interface {
|
||||
Render(w io.Writer, template string, data interface{}, c *Context) error
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue