Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/honmaple/forest

lightweight and fast HTTP router written in Go
https://github.com/honmaple/forest

go golang http router web-framework

Last synced: 10 days ago
JSON representation

lightweight and fast HTTP router written in Go

Awesome Lists containing this project

README

        

** Example
#+begin_src go
package main

import (
"net/http"

"github.com/honmaple/forest"
"github.com/honmaple/forest/middleware"
)

func main() {
r := forest.New(forest.Debug())
r.Use(middleware.Recover())
r.Use(middleware.Logger())
r.GET("/", func(c forest.Context) error {
return c.HTML(http.StatusOK, "

Hello Forest

")
})

v1 := r.Group(forest.WithPrefix("/api"))
{
v1.GET("/posts/{title}", func(c forest.Context) error {
return c.JSON(http.StatusOK, forest.H{"title": c.Param("title")})
})
v1.POST("/posts", func(c forest.Context) error {
type post struct {
Title string `json:"title" form:"title"`
Content string `json:"content" form:"content"`
}
p := post{}
if err := c.Bind(&p); err != nil {
return c.JSON(http.BadRequest, forest.H{"message": err.Error()})
}
return c.JSON(http.StatusOK, p)
})
}

v2 := forest.NewGroup(forest.WithHost("v2.localhost:8000"))
{
v2.GET("/posts/{title}", func(c forest.Context) error {
return c.JSON(http.StatusOK, forest.H{"title": c.Param("title")})
})
}

r.Mount(v2)
r.Start("127.0.0.1:8000")
}
#+end_src

** Route

*** Single parameter in path
#+begin_src go
router := forest.New()
// /posts/1 {"var": "1"}
// /posts/test {"var": "test"}
// /posts, /posts/, /posts/1/1 not match
router.GET("/posts/:var", handler)
// /posts/ {"var": ""}
// /posts/1 {"var": "1"}
// /posts/test {"var": "test"}
// /posts, /posts/1/1 not match
router.GET("/posts/:var?", handler)
// /posts/ {"var": ""}
// /posts/1 {"var": "1"}
// /posts/1/ {"var": "1/"}
// /posts/1/test/2 {"var": "1/test/2"}
router.GET("/posts/*var", handler)
// /posts/1 {"var": "1"}
// /posts/test {"var": "test"}
// /posts, /posts/, /posts/1/1 not match
router.GET("/posts/{var}", handler)
router.GET("/posts/{var:string}", handler)
// /posts/ {"var": ""}
// /posts/1 {"var": "1"}
// /posts/test {"var": "test"}
// /posts, /posts/1/1 not match
router.GET("/posts/{var?}", handler)
// /posts/1 {"var": "1"}
// /posts/test not match
router.GET("/posts/{var:int}", handler)
// /posts/1 not match
// /posts/test not match
// /posts/.1 {"var": ".1"}
// /posts/1.1 {"var": "1.1"}
// /posts/1.10 {"var": "1.10"}
router.GET("/posts/{var:float}", handler)
// /posts/1 {"var": "1"}
// /posts/test {"var": "test"}
// /posts/test/1 {"var": "test/1"}
router.GET("/posts/{var:path}", handler)
#+end_src
*** Multi parameters in path
#+begin_src go
// /posts/1 not match
// /posts/prefixtest {"var": "test"}
router.GET("/posts/prefix:var", handler)
// /posts/prefixtest {"var:end": "test"}
router.GET("/posts/prefix:var:end", handler)
// /posts/test not match
// /posts/test/1 not match
// /posts/test/1/test {"var": "test/1"}
router.GET("/posts/*var/test", handler)
// /posts/test not match
// /posts/test-123 {"var": "test", "var1": "123"}
router.GET("/posts/{var}-{var1:int}", handler)
// /posts/123-test {"var": "123", "var1": "test"}
router.GET("/posts/{var:int}-{var1}", handler)
// /posts/123-test {"var": "123", "var1": "test"}
// /posts/123/test-test {"var": "123/test", "var1": "test"}
router.GET("/posts/{var:path}-{var1}", handler)
// /posts/1/1/test {"var": "1", "var1": "1", "var2": "test"}
// /posts/test/1/1/test {"var": "test/1", "var1": "1", "var2": "test"}
// /posts/test/1/1/s/test not match
router.GET("/posts/{var:path}/{var1:int}/{var2}", handler)
// /posts/1/1/test {"var": "1", "var1": "1", "var2": "test"}
// /posts/test/1/1/test {"var": "test", "var1": "1", "var2": "1/test"}
// /posts/test/1/1/s/test {"var": "test", "var1": "1", "var2": "1/s/test"}
router.GET("/posts/{var:path}/{var1:int}/{var2:path}", handler)
#+end_src

*** Named Route
#+begin_src go
r := forest.New()
g1 := r.Group(forest.WithPrefix("/api"), forest.WithName("g1"))
g2 := g1.Group(forest.WithPrefix("/v1"), forest.WithName("g2"))
r1 := api.GET("/posts").Named("list_posts", "some description")
r2 := api.DELETE("/posts/:pk").Named("delete_post", "delete post with pk param")
// result
r.Route("g1.g2.list_posts") == r1
r.URL("g1.g2.list_posts") == r1.URL() == "/v1/api/posts"
r.Route("g1.g2.delete_post") == r2
r.URL("g1.g2.delete_post", "12") == r2.URL("12") == "/v1/api/posts/12"
#+end_src

*** Server Static files
#+begin_src go
r := forest.New()
r.GET("/static/*", func(c forest.Context) error {
path := filepath.Join("static", c.Param("*"))
return c.FileFromFS(path, http.FS(staticFS))
})
r.GET("/robots.txt", func(c forest.Context) error {
return c.FileFromFS("static/robots.txt", http.FS(staticFS))
})
r.GET("/favicon.ico", func(c forest.Context) error {
return c.FileFromFS("static/favicon.ico", http.FS(staticFS))
})
#+end_src

*** Bind Params
#+begin_src go
type Params struct {
Text string `query:"text" json:"text" form:"text" param:"text"`
}
p := Params{}
// bind query, method: not POST, PUT, PATCH
// bind form or json or xml, method: POST, PUT, PATCH
c.Bind(&p)
// bind params, GET /test/:text
c.BindParams(&p)
// bind other params
c.BindWith(&p, bind.Query)
c.BindWith(&p, bind.Form)
c.BindWith(&p, bind.MultipartForm)
c.BindWith(&p, bind.JSON)
c.BindWith(&p, bind.XML)
c.BindWith(&p, bind.Params)
c.BindWith(&p, bind.Header)
// custom bind tag
c.BindWith(&p, bind.FormBinder{"json"})
c.BindWith(&p, bind.QueryBinder{"json"})
#+end_src

** Custom
*** Custom Middleware
#+begin_src go
func MyMiddleware(c forest.Context) error {
// do something
// c.Next() is required, or else your handler will not execute
return c.Next()
}
router := forest.New()
// with root
router.Use(MyMiddleware)
// with group
group := router.Group(forest.WithPrefix("/api/v1"), forest.WithMiddlewares(MyMiddleware))
// with special handler
group.GET("/", MyMiddleware, func(c forest.Context) error {
return nil
})
#+end_src

*** Custom Logger
#+begin_src go
router := forest.New()
router.Logger = Logger1

router.GET("/posts", func(c forest.Context) error {
// c.Logger() == Logger1
...
})

group := router.Group(forest.WithPrefix("/api/v1"))
group.GET("/posts", func(c forest.Context) error {
// c.Logger() == Logger1
...
})

group := router.Group(forest.WithPrefix("/api/v2"))
group.Logger = Logger2
group.GET("/posts", func(c forest.Context) error {
// c.Logger() == Logger2
...
})
#+end_src

*** Custom Error Handler
#+begin_src go
router := forest.New()
// engine only
router.NotFound(func(c forest.Context) error {
return c.JSON(404, forest.H{"message": "not found"})
})
router.MethodNotAllowed(func(c forest.Context) error {
return c.JSON(405, forest.H{"message": "method not allowed"})
})

router.ErrorHandler = func(err error, c Context) {
c.String(500, err.Error())
}
group := router.Group(forest.WithPrefix("/api/v1"))
// group only
group.ErrorHandler = func(err error, c Context) {
c.String(501, err.Error())
}
#+end_src

*** Custom Context
#+begin_src go
type MyContext struct {
forest.Context
}

func (c *MyContext) Next() error {
return c.NextWith(c)
}

func MyContextMiddleware(c forest.Context) error {
// doing somthing
return c.NextWith(&MyContext{c})
}
#+end_src

*** Custom Host Matcher
#+begin_src go
func matcher(host, dst string) bool {
return host == dst
}
r := forest.New(forest.HostMatch(matcher))
// or use internal matcher
r := forest.New(forest.HostMatch(forest.HostMatcher))
#+end_src

*** Custom URL Param
#+begin_src go
import (
"github.com/google/uuid"
)

type UUIDMatcher struct {
}

func (s *UUIDMatcher) Name() string {
return "uuid"
}

func (s *UUIDMatcher) Match(path string, index int, next bool) (int, bool) {
if index > 0 {
return 0, false
}
if len(path) < 18 || (!next && len(path) > 18) {
return 0, false
}
_, err := uuid.Parse(path[:18])
if err != nil {
return 0, false
}
return 18, true
}

func NewUUIDMatcher(rule string) forest.Matcher {
return &UUIDMatcher{}
}

forest.RegisterRule("uuid", NewUUIDMatcher)

router := forest.New()
router.GET("/api/v1/user/{pk:uuid}", handler)
#+end_src