goa

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 9, 2023 License: MIT Imports: 20 Imported by: 10

README

goa

A golang http router with regexp and document generation support.

Build Status Coverage Status Go Report Card Documentation

Usage

demo main package
package main

import (
	"net/url"
	"os"
	"path/filepath"
	"strings"

	"github.com/lovego/fs"
	"github.com/lovego/goa"
	"github.com/lovego/goa/benchmark/example/users"
	"github.com/lovego/goa/middlewares"
	"github.com/lovego/goa/server"
	"github.com/lovego/goa/utilroutes"
	"github.com/lovego/logger"
)

func main() {
	router := goa.New()
	// logger should comes first, to handle panic and log all requests
	router.Use(middlewares.NewLogger(logger.New(os.Stdout)).Record)
	router.Use(middlewares.NewCORS(allowOrigin).Check)
	utilroutes.Setup(&router.RouterGroup)

	if os.Getenv("GOA_DOC") != "" {
		router.DocDir(filepath.Join(fs.SourceDir(), "docs", "apis"))
	}

	// If don't need document, use this simple style.
	router.Get("/", func(c *goa.Context) {
		c.Data("index", nil)
	})

	// If need document, use this style for automated routes document generation.
	router.Group("/users", "用户", "用户相关的接口").
		Get("/", func(req struct {
			Title   string        `用户列表`
			Desc    string        `根据搜索条件获取用户列表`
			Query   users.ListReq
			Session users.Session
		}, resp *struct {
			Data  users.ListResp
			Error error
		}) {
			resp.Data, resp.Error = req.Query.Run(&req.Session)
		}).
		Get(`/(\d+)`, func(req struct {
			Title string       `用户详情`
			Desc  string       `根据用户ID获取用户详情`
			Param int64        `用户ID`
			Ctx   *goa.Context 
		}, resp *struct {
			Data  users.DetailResp
			Error error
		}) {
			resp.Data, resp.Error = users.Detail(req.Param)
		})

	if os.Getenv("GOA_DOC") != "" {
		return
	}

	server.ListenAndServe(router)
}

func allowOrigin(origin string, c *goa.Context) bool {
	u, err := url.Parse(origin)
	if err != nil {
		return false
	}
	hostname := u.Hostname()
	return strings.HasSuffix(hostname, ".example.com") ||
		hostname == "example.com" || hostname == "localhost"
}
demo users package
package users

import "time"

type ListReq struct {
	Name     string `c:"用户名称"`
	Type     string `c:"用户类型"`
	Page     int    `c:"页码"`
	PageSize int    `c:"每页数据条数"`
}

type ListResp struct {
	TotalSize int `c:"总数据条数"`
	TotalPage int `c:"总页数"`
	Rows      []struct {
		Id    int    `c:"ID"`
		Name  string `c:"名称"`
		Phone string `c:"电话号码"`
	}
}

type Session struct {
	UserId  int64
	LoginAt time.Time
}

func (l *ListReq) Run(sess *Session) (ListResp, error) {
	return ListResp{}, nil
}

type DetailResp struct {
	TotalSize int `c:"总数据条数"`
	TotalPage int `c:"总页数"`
	Rows      []struct {
		Id    int    `c:"ID"`
		Name  string `c:"名称"`
		Phone string `c:"电话号码"`
	}
}

func Detail(userId int64) (DetailResp, error) {
	return DetailResp{}, nil
}

Handler func

The req (request) parameter

The req parameter must be a struct, and it can have the following 8 fields. The Title and Desc fields are just used for document generation, so can be of any type, and their values are untouched; The other fields's values are set in advance from the http.Request, so can be used directly in the handler. Except Session and Ctx, other fields's full tag is used as description for the corresponding object in the document. For Param, Query, Header and Body, if it's a struct, the struct fields's comment or c tag is used as the description for the fields in the document.

  1. Title: It's full tag is used as title of the route in document.
  2. Desc: It's full tag is used as description the route in document.
  3. Param: Subexpression parameters in regular expression path. If there is only one subexpression in the path and it's not named, the whole Param is set to the match properly. Otherwise, the Param must be a struct, and it's fields are set properly to the corresponding named subexpression. The first letter of the subexpression name is changed to uppercase to find the corresponding field.
  4. Query: Query parameters in the the request, Query must be a struct, and it's fields are set properly to the corresponding query paramter. The first letter of the query parameter name is changed to uppercase to find the corresponding field in the struct.
  5. Header: Headers in the request. Header must be a struct, and it's fields are set properly to the corresponding header. The field's header tag(if present) or it's name is used as the corresponding header name.
  6. Body: The request body is set to Body using json.Unmarshal.
  7. Session: Session is set to goa.Context.Get("session"), so the type must be exactly the same.
  8. Ctx: Ctx must be of type *goa.Context.
The resp (response) parameter.

The resp parameter must be a struct pointer, and it can have the following 3 fields. The fields's full tag is used as description for the corresponding object in the document. For Data and Header, if it's a struct, the struct fields's comment or c tag is used as the description for the fields in the document.

  1. Data: Data is writed in response body as a data field using json.Marshal.
  2. Error: Error is writed in response body as the code and message fields.
  3. Header: Header is writed in response headers.

see full examples and the generated documents.

Default middlewares

  • logging with error alarm support
  • list of requests in processing
  • CORS check

Attentions

  • static route is always matched before regexp route.
  • call c.Next() in middleware to pass control to the next midlleware or route, if you don't call c.Next() no remaining midlleware or route will be executed.
  • generally don't use midlleware after routes, because generally the routes don't call c.Next().

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Context

type Context struct {
	ContextBeforeLookup
	// contains filtered or unexported fields
}

func (*Context) Next

func (c *Context) Next()

run the next midllware or route handler.

Example
c := &Context{}
c.Next()
c.Next()
Output:

func (*Context) Param

func (c *Context) Param(i int) string

Param returns captured subpatterns. index begin at 0, so pass 0 to get the first captured subpatterns.

Example
c := &Context{params: []string{"123", "sdf"}}
fmt.Println(c.Param(0), "-")
fmt.Println(c.Param(1), "-")
fmt.Println(c.Param(2), "-")
Output:

123 -
sdf -
 -

type ContextBeforeLookup added in v0.1.7

type ContextBeforeLookup struct {
	*http.Request
	http.ResponseWriter
	// contains filtered or unexported fields
}

func (*ContextBeforeLookup) ClientAddr added in v0.1.7

func (c *ContextBeforeLookup) ClientAddr() string

func (*ContextBeforeLookup) Context added in v0.1.7

func (c *ContextBeforeLookup) Context() context.Context

func (*ContextBeforeLookup) Data added in v0.1.7

func (c *ContextBeforeLookup) Data(data interface{}, err error)

func (*ContextBeforeLookup) Flush added in v0.1.7

func (c *ContextBeforeLookup) Flush()

func (*ContextBeforeLookup) Get added in v0.1.7

func (c *ContextBeforeLookup) Get(key string) interface{}

func (*ContextBeforeLookup) GetError added in v0.1.7

func (c *ContextBeforeLookup) GetError() error

func (*ContextBeforeLookup) Hijack added in v0.1.7

func (c *ContextBeforeLookup) Hijack() (net.Conn, *bufio.ReadWriter, error)

func (*ContextBeforeLookup) Json added in v0.1.7

func (c *ContextBeforeLookup) Json(data interface{})

func (*ContextBeforeLookup) Json2 added in v0.1.7

func (c *ContextBeforeLookup) Json2(data interface{}, err error)

func (*ContextBeforeLookup) Ok added in v0.1.7

func (c *ContextBeforeLookup) Ok(message string)

func (*ContextBeforeLookup) Origin added in v0.1.7

func (c *ContextBeforeLookup) Origin() string

func (*ContextBeforeLookup) ParseForm added in v0.1.7

func (c *ContextBeforeLookup) ParseForm() error

func (*ContextBeforeLookup) Redirect added in v0.1.7

func (c *ContextBeforeLookup) Redirect(url string)

func (*ContextBeforeLookup) RequestBody added in v0.1.7

func (c *ContextBeforeLookup) RequestBody() ([]byte, error)

func (*ContextBeforeLookup) RequestId added in v0.1.7

func (c *ContextBeforeLookup) RequestId() string

func (*ContextBeforeLookup) ResponseBody added in v0.1.7

func (c *ContextBeforeLookup) ResponseBody() []byte

func (*ContextBeforeLookup) ResponseBodySize added in v0.1.7

func (c *ContextBeforeLookup) ResponseBodySize() int64

func (*ContextBeforeLookup) Scheme added in v0.1.7

func (c *ContextBeforeLookup) Scheme() string

func (*ContextBeforeLookup) Set added in v0.1.7

func (c *ContextBeforeLookup) Set(key string, value interface{})

func (*ContextBeforeLookup) SetError added in v0.1.7

func (c *ContextBeforeLookup) SetError(err error)

func (*ContextBeforeLookup) Status added in v0.1.7

func (c *ContextBeforeLookup) Status() int64

func (*ContextBeforeLookup) StatusJson added in v0.1.7

func (c *ContextBeforeLookup) StatusJson(status int, data interface{})

func (*ContextBeforeLookup) Url added in v0.1.7

func (c *ContextBeforeLookup) Url() string

func (*ContextBeforeLookup) Write added in v0.1.7

func (c *ContextBeforeLookup) Write(content []byte) (int, error)

type HandlerFuncs

type HandlerFuncs []func(*Context)

func (HandlerFuncs) String

func (handlers HandlerFuncs) String() string

to print regex_tree.Node in unit tests.

func (HandlerFuncs) StringIndent

func (handlers HandlerFuncs) StringIndent(indent string) string

type Router

type Router struct {
	RouterGroup
	// contains filtered or unexported fields
}
Example
router := New()

router.Get("/", func(c *Context) {
	fmt.Println("root")
})
users := router.Group("/users")

users.Get("/", func(c *Context) {
	fmt.Println("list users")
})
users.Get(`/(\d+)`, func(c *Context) {
	fmt.Printf("show user: %s\n", c.Param(0))
})

users.Post(`/`, func(c *Context) {
	fmt.Println("create a user")
})
users.Post(`/postx`, func(c *Context) {
})

users = users.Group(`/(\d+)`)

users.Put(`/`, func(c *Context) {
	fmt.Printf("fully update user: %s\n", c.Param(0))
})
users.Put(`/putx`, func(c *Context) {
})

users.Patch(`/`, func(c *Context) {
	fmt.Printf("partially update user: %s\n", c.Param(0))
})
users.Patch(`/patchx`, func(c *Context) {
})

users.Delete(`/`, func(c *Context) {
	fmt.Printf("delete user: %s\n", c.Param(0))
})
users.Delete(`/deletex`, func(c *Context) {
})

request, err := http.NewRequest("GET", "http://localhost/", nil)
if err != nil {
	log.Panic(err)
}
for _, route := range [][2]string{
	{"GET", "/"},
	{"GET", "/users"},
	{"POST", "/users"},
	{"GET", "/users/101/"}, // with a trailing slash
	{"PUT", "/users/101"},
	{"PATCH", "/users/101"},
	{"DELETE", "/users/101"},
} {
	request.Method = route[0]
	request.URL.Path = route[1]
	router.ServeHTTP(nil, request)
}
Output:

root
list users
create a user
show user: 101
fully update user: 101
partially update user: 101
delete user: 101

func New

func New() *Router

func (*Router) BeforeLookup added in v0.0.8

func (r *Router) BeforeLookup(fun func(ctx *ContextBeforeLookup))

BeforeLookup regiter a function to be run before every route Lookup.

func (*Router) NotFound

func (r *Router) NotFound(handler func(*Context))
Example
router := New()
router.Use(func(c *Context) {
	fmt.Println("middleware")
	c.Next()
})
router.Get("/", func(c *Context) {
	fmt.Println("root")
})

request, err := http.NewRequest("GET", "http://localhost/404", nil)
if err != nil {
	log.Panic(err)
}
rw := httptest.NewRecorder()
router.ServeHTTP(rw, request)

response := rw.Result()
if body, err := ioutil.ReadAll(response.Body); err != nil {
	fmt.Println(err)
} else {
	fmt.Println(response.StatusCode, string(body))
}

router.NotFound(func(c *Context) {
	fmt.Println("404 not found")
})
router.ServeHTTP(nil, request)

request.URL.Path = "/"
router.ServeHTTP(nil, request)
Output:

middleware
404 {"code":"404","message":"Not Found."}
middleware
404 not found
middleware
root

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request)

func (*Router) String

func (r *Router) String() string
Example
router := New()
router.Use(func(c *Context) {
	c.Next()
})

router.Get("/", func(c *Context) {
	fmt.Println("root")
})
users := router.Group("/users")

users.Get("/", func(c *Context) {
	fmt.Println("list users")
})
users.Get(`/(\d+)`, func(c *Context) {
	fmt.Printf("show user: %s\n", c.Param(0))
})

users.Post(`/`, func(c *Context) {
	fmt.Println("create a user")
})
users.Post(`/postx`, func(c *Context) {
})

fmt.Println(router)
Output:

{
  handlers: [
    github.com/lovego/goa.ExampleRouter_String.func1
  ]
  routes: {
    GET:
    { static: /, data: [
      github.com/lovego/goa.ExampleRouter_String.func1
      github.com/lovego/goa.ExampleRouter_String.func2
    ], children: [
      { static: users, data: [
        github.com/lovego/goa.ExampleRouter_String.func1
        github.com/lovego/goa.ExampleRouter_String.func3
      ], children: [
        { dynamic: ^/([0-9]+), data: [
          github.com/lovego/goa.ExampleRouter_String.func1
          github.com/lovego/goa.ExampleRouter_String.func4
        ] }
      ] }
    ] }
    POST:
    { static: /users, data: [
      github.com/lovego/goa.ExampleRouter_String.func1
      github.com/lovego/goa.ExampleRouter_String.func5
    ], children: [
      { static: /postx, data: [
        github.com/lovego/goa.ExampleRouter_String.func1
        github.com/lovego/goa.ExampleRouter_String.func6
      ] }
    ] }
  }
  notFound: github.com/lovego/goa.defaultNotFound
}

func (*Router) Use

func (r *Router) Use(handlers ...func(*Context))
Example
router := New()
router.Use(func(c *Context) {
	fmt.Println("middleware 1 pre")
	c.Next()
	fmt.Println("middleware 1 post")
})
router.Use(func(c *Context) {
	fmt.Println("middleware 2 pre")
	c.Next()
	fmt.Println("middleware 2 post")
})
router.Get("/", func(c *Context) {
	fmt.Println("root")
})

request, err := http.NewRequest("GET", "http://localhost/", nil)
if err != nil {
	log.Panic(err)
}
router.ServeHTTP(nil, request)
Output:

middleware 1 pre
middleware 2 pre
root
middleware 2 post
middleware 1 post

type RouterGroup

type RouterGroup struct {
	// contains filtered or unexported fields
}
Example
g := &RouterGroup{basePath: "/users", routes: make(map[string]*regex_tree.Node)}
g.Use(func(*Context) {})
g.Get("/nil", nil)
g.Get("/", func(*Context) {})

fmt.Println(g)
fmt.Println(g.Lookup("HEAD", "/users"))
fmt.Println(g.Lookup("POST", "/users"))
Output:

{
  basePath: /users
  handlers: [
    github.com/lovego/goa.ExampleRouterGroup.func1
  ]
  routes: {
    GET:
    { static: /users, data: [
      github.com/lovego/goa.ExampleRouterGroup.func1
      github.com/lovego/goa.ExampleRouterGroup.func2
    ] }
  }
}

[
  github.com/lovego/goa.ExampleRouterGroup.func1
  github.com/lovego/goa.ExampleRouterGroup.func2
] []
[ ] []
Example (ConcatPath_basic)
fmt.Println(RouterGroup{}.concatPath("/"))
fmt.Println(RouterGroup{}.concatPath("/users/"))
fmt.Println(RouterGroup{basePath: "/admin"}.concatPath(`/users/(\d+)`))
Output:

/
/users
/admin/users/(\d+)
Example (ConcatPath_error1)
defer func() {
	fmt.Println(recover())
}()
fmt.Println(RouterGroup{}.concatPath(""))
Output:

router path must not be empty.
Example (ConcatPath_error2)
defer func() {
	fmt.Println(recover())
}()
fmt.Println(RouterGroup{}.concatPath("abc"))
Output:

router path must begin with "/".

func (*RouterGroup) Add

func (g *RouterGroup) Add(method, path string, handler interface{}, args ...interface{}) *RouterGroup
Example (Error1)
defer func() {
	fmt.Println(recover())
}()
g := &RouterGroup{routes: make(map[string]*regex_tree.Node)}
g.Add("GET", "/(", func(*Context) {})
Output:

error parsing regexp: missing closing ): `/(`
Example (Error2)
defer func() {
	fmt.Println(recover())
}()
g := &RouterGroup{routes: make(map[string]*regex_tree.Node)}
g.Add("GET", "/", func(*Context) {})
g.Add("GET", "/", func(*Context) {})
Output:

path already exists

func (*RouterGroup) Child added in v0.3.4

func (g *RouterGroup) Child(path string, descs ...string) *RouterGroup

Child returns a new RouterGroup with inline docs. That means, its docs is generated in its parent's REAMED file.

func (*RouterGroup) Delete

func (g *RouterGroup) Delete(path string, handler interface{}, args ...interface{}) *RouterGroup

func (*RouterGroup) DocDir

func (g *RouterGroup) DocDir(dir string) *RouterGroup

func (*RouterGroup) Get

func (g *RouterGroup) Get(path string, handler interface{}, args ...interface{}) *RouterGroup

func (*RouterGroup) GetPost

func (g *RouterGroup) GetPost(path string, handler interface{}, args ...interface{}) *RouterGroup

func (*RouterGroup) Group

func (g *RouterGroup) Group(path string, descs ...string) *RouterGroup

Group returns a new RouterGroup with separate docs. That means, its docs is generated in its own README file unless path is "", ".", or "/".

func (*RouterGroup) Lookup

func (g *RouterGroup) Lookup(method, path string) (HandlerFuncs, []string)

func (*RouterGroup) Patch

func (g *RouterGroup) Patch(path string, handler interface{}, args ...interface{}) *RouterGroup

func (*RouterGroup) Post

func (g *RouterGroup) Post(path string, handler interface{}, args ...interface{}) *RouterGroup

func (*RouterGroup) Put

func (g *RouterGroup) Put(path string, handler interface{}, args ...interface{}) *RouterGroup

func (*RouterGroup) RoutesString

func (g *RouterGroup) RoutesString() string

func (*RouterGroup) String

func (g *RouterGroup) String() string

func (*RouterGroup) Use

func (g *RouterGroup) Use(handlers ...func(*Context)) *RouterGroup

Use adds middlewares to the group, which will be executed for all routes in this group.

func (*RouterGroup) Watch added in v0.0.9

func (g *RouterGroup) Watch(
	watchers ...func(method, fullPath string, args []interface{}) func(*Context),
) *RouterGroup

Watch watchs every route in the group, and optionally return a middleware to be executed only for this route.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL