Giter Site home page Giter Site logo

xgfone / ship Goto Github PK

View Code? Open in Web Editor NEW
47.0 4.0 4.0 731 KB

A flexible, powerful, high performance and minimalist Go Web HTTP router framework.

Home Page: https://github.com/xgfone/ship

License: Apache License 2.0

Go 100.00%
router route routes go-route go-router go golang mux go-mux http

ship's Introduction

ship Build Status GoDoc License

ship is a flexible, powerful, high performance and minimalist Go Web HTTP router framework supporting Go 1.11+. It is inspired by echo and httprouter. Thanks for those contributors.

Features

  • Support the url parameter.
  • Support the session manager.
  • Support the customized router manager.
  • Support the pre-route and route middlewares.
  • Support the route group builder to build the route.
  • Support the mulit-virtual hosts and the default host.
  • Support the exact, prefix, suffix and regexp hostname.
  • Support the binding of the request data, such as body and query.
  • Support the renderer, such as the HTML template.
  • ......

Components

  • Ship is the pure router framework based on the method and the path, including Middleware, Context, Router, etc.
  • HostManager and HostHandler are the vhost manager and the standard http handler with the vhost manager.
  • Runner is the runner to start the http server with the standard http handler.

Install

go get -u github.com/xgfone/ship/v5

Quick Start

// example.go
package main

import (
	"github.com/xgfone/ship/v5"
	"github.com/xgfone/ship/v5/middleware"
)

func main() {
	router := ship.New()
	router.Use(middleware.Logger(), middleware.Recover()) // Use the middlewares.

	router.Route("/ping").GET(func(c *ship.Context) error {
		return c.JSON(200, map[string]interface{}{"message": "pong"})
	})

	group := router.Group("/group")
	group.Route("/ping").GET(func(c *ship.Context) error {
		return c.Text(200, "group")
	})

	subgroup := group.Group("/subgroup")
	subgroup.Route("/ping").GET(func(c *ship.Context) error {
		return c.Text(200, "subgroup")
	})

	// Start the HTTP server.
	ship.StartServer(":8080", router)
	// or
	// http.ListenAndServe(":8080", router)
}
$ go run example.go
$ curl http://127.0.0.1:8080/ping
{"message":"pong"}

$ curl http://127.0.0.1:8080/group/ping
group

$ curl http://127.0.0.1:8080/group/subgroup/ping
subgroup

Route Path

The route path supports the parameters like :paramName, * or *restParamName.

  • /path/to/route only matches the path /path/to/route.
  • /path/:param1/to matches the path /path/abc/to, /path/xyz/to, etc. And :param1 is equal to abc or xyz.
  • /path/:param1/to/:param2 matches the path /path/p11/to/p21, /path/p12/to/p22, etc. And :parma1 is equal to p11 or p12, and :param2 is equal to p12 or p22.
  • /path/to/* or /path/to/*all matches the path /path/to/abc, /path/to/abc/efg, /path/to/xyz, /path/to/xyz/123, etc. And * or *all is equal to abc, abc/efg, xyz, or xzy/123. Notice: * or *restParamName must be the last one of the route path.
  • /path/:param/to/* matches the path /path/abc/to/efg, /path/abc/to/efg/123, etc. And :param is equal to abc, and * is equal to efg or efg/123

For the parameter, it can be accessed by Context.Param(paramName).

  • For *, the parameter name is *, like Context.Param("*").
  • For *restParamName, the parameter name is restParamName, like Context.Param(restParamName).

API Example

Route Builder

Using CONNECT, HEAD, GET, POST, PUT, PATCH, DELETE and OPTIONS

func main() {
	router := ship.New()
	router.Route("/path/get").GET(getHandler)
	router.Route("/path/put").PUT(putHandler)
	router.Route("/path/head").HEAD(headHandler)
	router.Route("/path/post").POST(postHandler)
	router.Route("/path/patch").PATCH(patchHandler)
	router.Route("/path/delete").DELETE(deleteHandler)
	router.Route("/path/option").OPTIONS(optionHandler)
	router.Route("/path/connect").CONNECT(connectHandler)
	ship.StartServer(":8080", router)
}

Notice: you can register the same handler with more than one method by Route(path string).Method(handler Handler, method ...string).

Cascade the registered routes

func main() {
	router := ship.New()
	router.Route("/path/to").GET(getHandler).POST(postHandler).DELETE(deleteHandler)
	ship.StartServer(":8080", router)
}

Use the mapping of the route methods

func main() {
	router := ship.New()
	router.Route("/path/to").Map(map[string]ship.Handler{
		"GET": getHandler,
		"POST": postHandler,
		"DELETE": deleteHandler,
	})
	ship.StartServer(":8080", router)
}

Name the route

When registering the route, it can be named with a name.

func main() {
	router := ship.New()
	router.Route("/path/:id").Name("get_url").GET(func(c *ship.Context) error {
		fmt.Println(c.URL("get_url", c.Param("id")))
		return nil
	})
	ship.StartServer(":8080", router)
}

Use the route group

package main

import (
	"github.com/xgfone/ship/v5"
	"github.com/xgfone/ship/v5/middleware"
)

// MyAuthMiddleware returns a middleare to authenticate the request.
func MyAuthMiddleware() ship.Middleware {
	return func(next ship.Handler) ship.Handler {
		return func(c *ship.Context) error {
			// TODO: authenticate the request.
			return next(c)
		}
	}
}

func main() {
	router := ship.New()
	router.Use(middleware.Logger(), middleware.Recover())

	// v1 Group, which will inherit the middlewares of the parent router.
	v1 := router.Group("/v1")
	v1.Route("/get").GET(func(c *ship.Context) error { return nil }) // Route: GET /v1/get

	// v2 Group, which won't inherit the middlewares of the parent router.
	v2 := router.Group("/v2").ResetMiddlewares(MyAuthMiddleware())
	v2.Route("/post").POST(func(c *ship.Context) error { return nil }) // Route: POST /v2/post

	// For sub-group of v2 Group.
	v2g := v2.Group("/child")
	v2g.Route("/path").GET(func(c *ship.Context) error { return nil }) // Route: GET /v2/child/path

	ship.StartServer(":8080", router)
}

Filter the unacceptable routes

package main

import (
	"strings"

	"github.com/xgfone/ship/v5"
)

func filter(ri ship.Route) bool {
	if ri.Name == "" || !strings.HasPrefix(ri.Path, "/prefix/") {
		return true
	}
	return false
}

func main() {
	handler := func(c *ship.Context) error { return nil }

	router := ship.New()
	router.RouteFilter = filter // Don't register the router without name.

	router.Group("/prefix").Route("/name").Name("test").GET(handler) // Register the route
	router.Group("/prefix").Route("/noname").GET(handler)            // Don't register the route
	router.Route("/no_group").GET(handler)                           // Don't register the route

	ship.StartServer(":8080", router)
}

Modify the route before registering it

package main

import "github.com/xgfone/ship/v5"

func modifier(ri ship.Route) ship.Route {
	ri.Path = "/prefix" + ri.Path
	return ri
}

func main() {
	handler := func(c *ship.Context) error { return nil }

	router := ship.New()
	router.RouteModifier = modifier
	router.Route("/path").Name("test").GET(handler) // Register the path as "/prefix/path".

	ship.StartServer(":8080", router)
}

Use Middleware

package main

import (
	"fmt"
	"strings"

	"github.com/xgfone/ship/v5"
	"github.com/xgfone/ship/v5/middleware"
)

// RemovePathPrefix returns a middleware to remove the prefix from the request path.
func RemovePathPrefix(prefix string) ship.Middleware {
	if len(prefix) < 2 || prefix[len(prefix)-1] == '/' {
		panic(fmt.Errorf("invalid prefix: '%s'", prefix))
	}

	return func(next ship.Handler) ship.Handler {
		return func(c *ship.Context) error {
			req := c.Request()
			req.URL.Path = strings.TrimPrefix(req.URL.Path, prefix)
			return next(c)
		}
	}
}

func main() {
	router := ship.New()

	// Execute the middlewares before finding the route.
	router.Pre(RemovePathPrefix("/static"))

	// Execute the middlewares after finding the route.
	router.Use(middleware.Logger(), middleware.Recover())

	handler := func(c *ship.Context) error { return nil }
	router.Route("/path1").GET(handler)
	router.Route("/path2").GET(handler)
	router.Route("/path3").GET(handler)

	ship.StartServer(":8080", router)
}

Use the virtual host

package main

import (
	"github.com/xgfone/ship/v5"
)

func main() {
	vhosts := ship.NewHostManagerHandler(nil)

	_default := ship.New()
	_default.Route("/").GET(func(c *ship.Context) error { return c.Text(200, "default") })
	vhosts.SetDefaultHost("", _default)

	// Exact Match Host
	vhost1 := ship.New()
	vhost1.Route("/").GET(func(c *ship.Context) error { return c.Text(200, "vhost1") })
	vhosts.AddHost("www.host1.example.com", vhost1)

	// Suffix Match Host
	vhost2 := ship.New()
	vhost2.Route("/").GET(func(c *ship.Context) error { return c.Text(200, "vhost2") })
	vhosts.AddHost("*.host2.example.com", vhost2)

	// Prefix Match Host
	vhost3 := ship.New()
	vhost3.Route("/").GET(func(c *ship.Context) error { return c.Text(200, "vhost3") })
	vhosts.AddHost("www.host3.*", vhost3)

	// Regexp Match Host by using Go regexp package
	vhost4 := ship.New()
	vhost4.Route("/").GET(func(c *ship.Context) error { return c.Text(200, "vhost4") })
	vhosts.AddHost(`www\.[a-zA-z0-9]+\.example\.com`, vhost4)

	ship.StartServer(":8080", vhosts)
}
$ curl http://127.0.0.1:8080/
default

$ curl http://127.0.0.1:8080/ -H 'Host: www.host1.example.com' # Exact
vhost1

$ curl http://127.0.0.1:8080/ -H 'Host: www.host2.example.com' # Suffix
vhost2

$ curl http://127.0.0.1:8080/ -H 'Host: www.host3.example.com' # Prefix
vhost3

$ curl http://127.0.0.1:8080/ -H 'Host: www.host4.example.com' # Regexp
vhost4

Handle the complex response

package main

import "github.com/xgfone/ship/v5"

func responder(c *ship.Context, args ...interface{}) error {
	switch len(args) {
	case 0:
		return c.NoContent(200)
	case 1:
		switch v := args[0].(type) {
		case int:
			return c.NoContent(v)
		case string:
			return c.Text(200, v)
		}
	case 2:
		switch v0 := args[0].(type) {
		case int:
			return c.Text(v0, "%v", args[1])
		}
	}
	return c.NoContent(500)
}

func main() {
	router := ship.New()
	router.Responder = responder
	router.Route("/path1").GET(func(c *ship.Context) error { return c.Respond() })
	router.Route("/path2").GET(func(c *ship.Context) error { return c.Respond(200) })
	router.Route("/path3").GET(func(c *ship.Context) error { return c.Respond("Hello, World") })
	router.Route("/path4").GET(func(c *ship.Context) error { return c.Respond(200, "Hello, World") })
	ship.StartServer(":8080", router)
}

Bind JSON, XML or Form data from the request payload

package main

import "github.com/xgfone/ship/v5"

// Login is the login information.
type Login struct {
	Username string `json:"username" xml:"username"`
	Password string `json:"password" xml:"password"`
}

func main() {
	router := ship.Default()
	router.Route("/login").POST(func(c *ship.Context) (err error) {
		var login Login
		if err = c.Bind(&login); err != nil {
			return ship.ErrBadRequest.New(err)
		}
		return c.Text(200, "username=%s, password=%s", login.Username, login.Password)
	})

	ship.StartServer(":8080", router)
}
$ curl http://127.0.0.1:8080/login \
    -H 'Content-Type: application/json' \
    -d '{"username":"xgfone","password":"123456"}'
username=xgfone, password=123456

$ curl http://127.0.0.1:8080/login \
    -H 'Content-Type: application/xml' \
    -d '<login><username>xgfone</username><password>123456</password></login>'
username=xgfone, password=123456

Render HTML template

In the directory /path/to/templates, there is a template file named index.tmpl as follow:

<!DOCTYPE html>
<html>
    <head></head>
    <body>
        This is the body content: </pre>{{ . }}</pre>
    </body>
</html>
package main

import (
	"github.com/xgfone/ship/v5"
	"github.com/xgfone/ship/v5/render/template"
)

func main() {
	// It will recursively load all the files in the directory as the templates.
	loader := template.NewDirLoader("/path/to/templates")
	tmplRender := template.NewHTMLTemplateRender(loader)

	router := ship.Default()
	router.Renderer.(*ship.MuxRenderer).Add(".tmpl", tmplRender)
	router.Route("/html").GET(func(c *ship.Context) error {
		return c.RenderOk("index.tmpl", "Hello World")
	})

	// Start the HTTP server.
	ship.StartServer(":8080", router)
}

When accessing http://127.0.0.1:8080/html, it returns

<!DOCTYPE html>
<html>
    <head></head>
    <body>
        This is the body content: </pre>Hello World</pre>
    </body>
</html>

Route Management

ship supply a default implementation based on Radix tree to manage the route with Zero Garbage (See Benchmark), which refers to echo, that's, NewRouter().

You can appoint your own implementation by implementing the interface Router.

type Router interface {
	// Range traverses all the registered routes.
	Range(func(name, path, method string, handler interface{}))

	// Path generates a url path by the path name and parameters.
	//
	// Return "" if there is not the route path named name.
	Path(name string, params ...interface{}) string

	// Add adds the route and returns the number of the parameters
	// if there are the parameters in the route path.
	//
	// name is the name of the path, which is optional and must be unique
	// if not empty.
	//
	// If method is empty, handler is the handler of all the methods supported
	// by the implementation. Or, it is only that of the given method.
	//
	// For the parameter in the path, the format is determined by the implementation.
	Add(name, path, method string, handler interface{}) (paramNum int, err error)

	// Del deletes the given route.
	//
	// If method is empty, deletes all the routes associated with the path.
	// Or, only delete the given method for the path.
	Del(path, method string) (err error)

	// Match matches the route by path and method, puts the path parameters
	// into pnames and pvalues, then returns the handler and the number
	// of the path paramethers.
	//
	// If pnames or pvalues is empty, it will ignore the path paramethers
	// when finding the route handler.
	//
	// Return (nil, 0) if not found the route handler.
	Match(path, method string, pnames, pvalues []string) (handler interface{}, pn int)
}
func main() {
	NewMyRouter := func() (router ship.Router) {
		// TODO: new a Router.
		return
	}

	router := ship.New()
	router.Router = NewMyRouter()
	// ...
}

Benchmark

HP Laptop 14s-dr2014TU
go:     1.16.4
goos:   windows
goarch: amd64
memory: 16GB DDR4-3200
cpu:    11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
Framework Version
github.com/gin-gonic/gin v1.7.2
github.com/labstack/echo/v4 v4.4.0
github.com/xgfone/ship/v5 v5.0.0
Function ops ns/op B/opt allocs/op
BenchmarkEchoStatic-8 43269 27676 2056 157
BenchmarkEchoGitHubAPI-8 29738 40773 2788 203
BenchmarkEchoGplusAPI-8 668731 1967 207 13
BenchmarkEchoParseAPI-8 362774 3369 398 26
BenchmarkGinStatic-8 47384 24037 8267 157
BenchmarkGinGitHubAPI-8 33747 34648 10771 203
BenchmarkGinGplusAPI-8 598628 1830 681 13
BenchmarkGinParseAPI-8 356298 3314 1442 26
BenchmarkShipEchoStatic-8 51788 23219 668 0
BenchmarkShipEchoGitHubAPI-8 32854 35759 1054 0
BenchmarkShipEchoGplusAPI-8 746049 1809 92 0
BenchmarkShipEchoParseAPI-8 396067 3310 174 0
Function ops ns/op B/opt allocs/op
BenchmarkShipWithoutVHost-8 19691887 54.53 0 0
BenchmarkShipWithExactVHost-8 17158249 64.19 0 0
BenchmarkShipWithPrefixVHost-8 13445091 90.81 0 0
BenchmarkShipWithRegexpVHost-8 4668913 248.0 0 0

ship's People

Contributors

dependabot[bot] avatar three7six avatar xgfone avatar xmx avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

ship's Issues

文件名包含中文,Safari浏览器下载乱码

已知会乱码的浏览器:iOS Safari 和 macOS Safari
简单起见,只写核心复现代码了(理论上其他非ASCII文字也会乱码)

func Download(c *ship.Context) error {
	return c.Attachment("/path/to/file/汉字名字.png", "")
}

一般是在设置Content-Disposition时使用 filename*

c.SetRespHeader(ship.HeaderContentDisposition, "attachment; filename*=utf-8''"+url.QueryEscape("汉字名字.png"))

参考链接:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition

希望 query 参数绑定能支持时间类型

func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {

Query 参数绑定器不支持 time.Time time.Duration 类型,希望能添加支持。
下面是复现代码:

type Request struct {
	Birthday time.Time     `query:"birthday"` // 请求参数是 RFC3339 格式
	Interval time.Duration `query:"interval"`
}

// Demo 请求方式: GET http://127.0.0.1/demo?interval=60s&birthday=2021-11-11T05:23:11.152Z
func Demo(c *ship.Context) error {
	var req Request
	if err := c.BindQuery(&req); err != nil {
		// 此时会报 unknown type 的错误
		return err
	}

	// TODO 业务处理

	return nil
}

请求body为formdata,对应接收结构体存在嵌套继承时,参数绑定不上

go version:1.17.3
ship version:5.1.0

复现代码:

type person struct {
	Name string `json:"name" form:"name"`
	Age  int    `json:"age"  form:"age"`
}

type Student struct {
	person        // 这个结构体必须是首字母小写
	School string `json:"school" form:"school"`
	Grade  int    `json:"grade"  form:"grade"`
}

func Demo(c *ship.Context) error {
	var stu Student
	if err := c.Bind(&stu); err != nil {
		return ship.ErrBadRequest.New(err)
	}

	fmt.Println(stu.Name)   // 参数没有绑定上
	fmt.Println(stu.Age)    // 参数没有绑定上
	fmt.Println(stu.School) // OK
	fmt.Println(stu.Grade)  // OK

	return c.JSON(http.StatusOK, stu)
}

func main() {
	router := ship.Default()
	router.Route("/demo").POST(Demo)
	ship.StartServer(":9999", router)

	// $ go run main.go
	//
	// 请求 body 格式是 FormData:
	// $ curl -X POST -F "name=小明" -F "age=9" -F "school=新民小学" -F "grade=3" http://localhost:9999/demo
	//
	// 响应结果:
	// {"name":"","age":0,"school":"新民小学","grade":3}
}

what diffence compare with gin

I dont like gin error handle and context part . I want found a fast and slight web framework .
Do you test benchmark gin and ship?
thank you

获取 Content-Type 的小问题

ship/context.go

Lines 410 to 417 in 405a71d

// ContentType returns the Content-Type of the request without the charset.
func (c *Context) ContentType() (ct string) {
ct = c.req.Header.Get(HeaderContentType)
if index := strings.IndexAny(ct, ";"); index > 0 {
ct = strings.TrimSpace(ct[:index])
}
return
}

ship/binder.go

Lines 72 to 83 in 405a71d

// Bind implements the interface Binder, which looks up the registered binder
// by the request header "Content-Type" and calls it to bind the value dst
// to req.
func (mb *MuxBinder) Bind(dst interface{}, req *http.Request) error {
ct := req.Header.Get("Content-Type")
if index := strings.IndexAny(ct, ";"); index > 0 {
ct = strings.TrimSpace(ct[:index])
}
if ct == "" {
return ErrMissingContentType
}

上面两处代码在获取 Content-Type 时用的是 index > 0,如果测试用例是 ; charset=utf-8,那么切割出的 Content-Type; charset=utf-8。此时我们期望结果的应该是 空字符串

故:此处使用 index >= 0 更为妥当

下载文件 Non-ASCII 文件名编码

文件下载时,对于 Non-ASCII 的文件名,常见的 Go HTTP 框架几乎都使用 url.QueryEscape 编码:

ship 使用 url.QueryEscape

gin 使用 url.QueryEscape

beego 使用 url.PathEscape

iris 使用 url.QueryEscape

Go Frame 使用 url.QueryEscape

echo 未对 Non-ASCII 做处理

但是 url.QueryEscape 会将 空格转换+,如果文件名是 a b.txt,下载者看到的文件名就是 a+b.txt,虽然 url.QueryEscape 可以解决 Non-ASCII Header 乱码问题,但是我个人觉得并非最优解。对于 HTTP Header 我个人觉得这种场景正确处理方式应该是使用 mime.FormatMediaType 格式化。

基于上述问题,想请教下你对 Header 编码的看法。

参考 RFC2183

mime 代码示例:

// name: 文件名,例如:a b.txt
// dispositionType: inline 或 attachment
params := map[string]string{"filename": name}
disposition := mime.FormatMediaType(dispositionType, params)
c.res.Header().Set(HeaderContentDisposition, disposition)

静态文件 index.html 404

目录文件布局如下

.
├─main.go
└─webui
  └─index.html

main.go 内容如下:

package main

import "github.com/xgfone/ship/v5"

func main() {
	s := ship.Default()
	s.Route("/webui").Static("webui") // 只有注册的路由是 / 时 index.html 才正常访问
	ship.StartServer(":8080", s)
}

webui/index.html 内容如下:

<h1>Hello Ship</h1>

结果

访问 http://127.0.0.1:8080/webui 报 404

访问 http://127.0.0.1:8080/webui/index.html 依然报 404

原因分析

注册静态文件路由时,调用 s.Route("/webui").Static("webui") ship 会注册两个路由GET /webui/*pathHEAD /webui/*path

当浏览器访问 GET /webui/index.htmlhttp.FileServer 会将 index.html 的结尾的Path重定向到 ./ ,即:将 GET /webui/index.html 重定向到 GET /webui/ ,而 /webui/ 无法命中 /webui/*path,导致 index.html 404

HTTP状态码一旦写入后,HandleError无法修改状态码

ship 版本

v5.1.1

代码

package main

import (
	"encoding/json"
	"net/http"

	"github.com/xgfone/ship/v5"
)

func main() {
	s := ship.New()
	s.HandleError = func(c *ship.Context, err error) {
		// 如果出现错误:http状态码为400
		_ = c.Text(http.StatusBadRequest, err.Error())
	}

	s.Route("/demo").GET(func(c *ship.Context) error {
		wrong := json.RawMessage("a:b")     // 错误的JSON数据
		return c.JSON(http.StatusOK, wrong) // 如果执行正常:http状态码200
	})

	ship.StartServer(":8080", s)
}

curl 测试输出(问题在响应报文的状态码)

$ curl -v 'http://127.0.0.1:8080/demo' 
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /demo HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.80.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=UTF-8
< Date: Wed, 29 Dec 2021 02:41:51 GMT
< Content-Length: 110
< 
* Connection #0 to host 127.0.0.1 left intact
json: error calling MarshalJSON for type json.RawMessage: invalid character 'a' looking for beginning of value% 

期望

// 当正常执行的时候输出HTTP状态码为200
// 当出现错误的时候输出的HTTP状态码由HandleError控制
return c.JSON(http.StatusOK, wrong)

建议:session 接口增加 ctx 参数

方法调用时传递 ctx 可以由调用者灵活控制。
在一些特殊情况下,例如:
将 session 存在了数据库或 redis 等外部系统中,由于网络波动或性能差,导致 session 操作耗时,
客户端可能超时或主动取消了请求,但是没有 ctx 的控制,session 操作仍然可能还在操作中。
虽然 session 实现者可以指定一个带超时时间的 ctx,但是该 ctx 很难与客户端的请求的 ctx 产生关联(http.Request.Context())。

效果如下:

type Session interface {
	GetSession(ctx context.Context, id string) (value interface{}, err error)
	SetSession(ctx context.Context, id string, value interface{}) error
	DelSession(ctx context.Context, id string) error
}

ship.Context 实现可改为:

func (c *Context) GetSession(ctx context.Context, id string) (v interface{}, err error) {
	if ctx == nil {
		ctx = c.req.Context()
	}
	if id == "" {
		err = ErrInvalidSession
	} else if v, err = c.Session.GetSession(ctx, id); err == nil && v == nil {
		err = ErrSessionNotExist
	}

	return
}

func (c *Context) SetSession(ctx context.Context, id string, value interface{}) (err error) {
	if id == "" || value == nil {
		return ErrInvalidSession
	}
	if ctx == nil {
		ctx = c.req.Context()
	}
	return c.Session.SetSession(ctx, id, value)
}

func (c *Context) DelSession(ctx context.Context, id string) (err error) {
	if id == "" {
		return ErrInvalidSession
	}
	if ctx == nil {
		ctx = c.req.Context()
	}
	return c.Session.DelSession(ctx, id)
}

query form 匿名嵌套结构体参数无法绑定了

版本:v5.2.0

package main

import (
	"fmt"

	"github.com/xgfone/ship/v5"
)

func main() {
	s := ship.Default()
	s.Route("/users").GET(List)
	ship.StartServer(":9990", s)
}

type Page struct {
	Page int `query:"page"`
	Size int `query:"size"`
}

type Req struct {
	Page
	Name string `query:"name"`
}

func List(c *ship.Context) error {
	var r Req
	if err := c.BindQuery(&r); err != nil {
		return err
	}

	fmt.Println(r) // r.Page 参数绑定不上

	return nil
}

希望可以支持 multipart.FileHeader 自动绑定

ship 目前不支持对 multipart.FileHeader 类型参数自动绑定。

在 Parse 数据前,Content-Type 为 form-data 时 执行 r.ParseMultipartForm(maxMemory) 操作,
但是 在绑定时 取的是 r.Form
r.Form 只包含 string 类型的数据,r.MultipartForm 包含了 string 类型数据和文件流

gin 框架对 mutlipart.FileHeader 的绑定操作参考:

https://github.com/gin-gonic/gin/blob/master/binding/multipart_form_mapping.go

https://github.com/gin-gonic/gin/blob/master/binding/multipart_form_mapping_test.go

添加 path 相同且带有 Name 的路由会 panic

package main

import (
	"net/http"

	"github.com/xgfone/ship/v5"
)

func main() {
	handler := ship.OkHandler()

	s := ship.New()
	s.Route("/user").Name("添加用户").Method(handler, http.MethodPost)
	s.Route("/user").Name("删除用户").Method(handler, http.MethodDelete)
	// panic: inconsistent route name: name=删除用户, path=/user, method=DELETE
}

复现代码如上所述。

比如在业务中,POST /user添加用户 逻辑,DELETE /user删除用户 逻辑,

在注册路由时候业务名称设置成路由的名字,就会 panic,查看源码发现是 Router.Add 做了校验。

虽然可以使用 Route.Data 解决上述问题,但是还是有点疑惑:

为什么路由的 namepath 唯一对应,而不是 namemethod + path 唯一对应?

希望支持非 HTTP 标准方法的路由注册。

我基于 ship 开发 WebDAV 文件共享时,想把 /dav /dav/* 这个路径作为 WebDAV 的文件共享路径,WebDAV 是基于 HTTP 的协议,但是在 HTTP 标准的方法中扩展了一些方法,如 MKCOL COPY LOCK UNLOCK 等方法,但是当前路由只有 GET POST PUT ... 这些标准 HTTP 方法。希望能支持除此之外的其他方法。

代码示例如下:

package main

import (
	"net/http"

	"github.com/xgfone/ship/v5"
	"golang.org/x/net/webdav"
)

func main() {
	h := &webdav.Handler{
		FileSystem: webdav.Dir("/"),
		LockSystem: webdav.NewMemLS(),
	}

	fn := func(c *ship.Context) error {
		req := c.Request()
		path := c.Param("path")
		req.URL.Path = "/" + path
		h.ServeHTTP(c.ResponseWriter(), req)
		return nil
	}

	methods := []string{
		http.MethodOptions, http.MethodGet, http.MethodHead, http.MethodPost, http.MethodDelete,
		http.MethodPut, "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", "PROPFIND", "PROPPATCH",
	}
	sh := ship.Default()
	sh.Route("/dav").Method(fn, methods...)
	sh.Route("/dav/*path").Method(fn, methods...)

	_ = http.ListenAndServe(":8080", sh)
}

ship/router/echo/echo.go

Lines 124 to 163 in 405a71d

func (mh *methodHandler) AddHandler(method string, handler interface{}) {
switch method {
case "": // For Any Method
*mh = methodHandler{
get: handler,
put: handler,
post: handler,
head: handler,
patch: handler,
trace: handler,
delete: handler,
options: handler,
connect: handler,
propfind: handler,
report: handler,
}
case http.MethodGet:
mh.get = handler
case http.MethodPut:
mh.put = handler
case http.MethodPost:
mh.post = handler
case http.MethodHead:
mh.head = handler
case http.MethodPatch:
mh.patch = handler
case http.MethodDelete:
mh.delete = handler
case http.MethodOptions:
mh.options = handler
case http.MethodConnect:
mh.connect = handler
case http.MethodTrace:
mh.trace = handler
case PROPFIND:
mh.propfind = handler
case REPORT:
mh.report = handler
}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.