Compare commits

..

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

7 changed files with 34 additions and 303 deletions

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)
@ -57,11 +35,6 @@ 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)
@ -75,22 +48,14 @@ 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)
_, _ = 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 +67,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

@ -2,26 +2,28 @@ package router
import (
"encoding/json"
"net/http"
"fmt"
)
func defaultNotFoundHandler(c *Context) error {
return c.StatusText(http.StatusNotFound)
return c.String(404, `not found`)
}
func defaultMethodNotAllowedHandler(c *Context) error {
return c.StatusText(http.StatusMethodNotAllowed)
return c.String(504, `method not allowed`)
}
func defaultErrorHandler(c *Context, err interface{}) {
_ = c.StatusText(http.StatusInternalServerError)
fmt.Println(err)
c.String(500, `internal server error`)
}
func defaultReader(c *Context, dst interface{}) (bool, error) {
func defaultReader(c *Context, dst interface{}) bool {
err := json.NewDecoder(c.Request.Body).Decode(dst)
if err != nil {
return false, c.StatusText(http.StatusBadRequest)
c.NoContent(400)
return false
}
return true, nil
return true
}

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

@ -1,12 +1,8 @@
package router
import (
"context"
"crypto/tls"
"net"
"net/http"
"reflect"
"time"
"github.com/julienschmidt/httprouter"
)
@ -24,11 +20,11 @@ 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
// Middleware TODO:
type Middleware func(Handle) Handle
// Reader reads input to dst, returns true if successful
type Reader func(c *Context, dst interface{}) (bool, error)
// Binder reads input to dst, returns true is successful
type Reader func(c *Context, dst interface{}) bool
// Router is the router itself
type Router struct {
@ -39,8 +35,6 @@ type Router struct {
NotFoundHandler Handle
MethodNotAllowedHandler Handle
ErrorHandler ErrorHandle
TrimTrailingSlashes bool
server *http.Server
}
// New returns a new Router
@ -96,48 +90,14 @@ 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()
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 {
@ -146,11 +106,7 @@ func (r *Router) getHttpr() http.Handler {
handle = handlePOST(r, v.Handle)
}
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.Handle(v.Method, v.Path, handleReq(r, handle, append(r.middleware, v.Middleware...)))
}
httpr.NotFound = http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
@ -162,25 +118,20 @@ func (r *Router) getHttpr() http.Handler {
})
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)
}
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)
})
}
return httpr
}
func handleErr(errHandler ErrorHandle, err interface{}) Handle {
return func(c *Context) error {
errHandler(c, err)
return nil
}
}
func checkInterfaceHandle(f interface{}) {
if _, ok := f.(Handle); ok {
return
@ -203,6 +154,8 @@ 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 {
@ -211,16 +164,11 @@ func handlePOST(r *Router, f interface{}) Handle {
return func(c *Context) error {
data := reflect.New(inputRt)
if r.Reader != nil {
ok, err := r.Reader(c, data.Interface())
_ = c.Request.Body.Close()
if err != nil {
return err
}
if !ok {
if !r.Reader(c, data.Interface()) {
c.Request.Body.Close()
return nil
}
}
c.Request.Body.Close()
out := funcRv.Call([]reflect.Value{reflect.ValueOf(c), data.Elem()})
@ -233,10 +181,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-- {
for i := len(m) - 1; i >= 0; i-- { // TODO: 1,2,3 of 3,2,1
f = m[i](f)
}

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
}