parent
45bc099bb6
commit
69032be6b2
@ -1,171 +0,0 @@ |
|||||||
package rsp |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"sort" |
|
||||||
"strconv" |
|
||||||
"strings" |
|
||||||
) |
|
||||||
|
|
||||||
var errInvalidTypeSubtype = "accept: Invalid type '%s'." |
|
||||||
|
|
||||||
// headerAccept represents a parsed headerAccept(-Charset|-Encoding|-Language) header.
|
|
||||||
type headerAccept struct { |
|
||||||
Type, Subtype string |
|
||||||
Q float64 |
|
||||||
Extensions map[string]string |
|
||||||
} |
|
||||||
|
|
||||||
// AcceptSlice is a slice of headerAccept.
|
|
||||||
type acceptSlice []headerAccept |
|
||||||
|
|
||||||
func Accepts(header, expect string) bool { |
|
||||||
_, typeSubtype, err := parseMediaRange(expect) |
|
||||||
if err != nil { |
|
||||||
return false |
|
||||||
} |
|
||||||
return accepts(parse(header), typeSubtype[0], typeSubtype[1]) |
|
||||||
} |
|
||||||
|
|
||||||
func accepts(slice acceptSlice, typ, sub string) bool { |
|
||||||
for _, a := range slice { |
|
||||||
if a.Type != typ { |
|
||||||
continue |
|
||||||
} |
|
||||||
if a.Subtype == "*" || a.Subtype == sub { |
|
||||||
return true |
|
||||||
} |
|
||||||
} |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
// parses a HTTP headerAccept(-Charset|-Encoding|-Language) header and returns
|
|
||||||
// AcceptSlice, sorted in decreasing order of preference. If the header lists
|
|
||||||
// multiple types that have the same level of preference (same specificity of
|
|
||||||
// type and subtype, same qvalue, and same number of extensions), the type
|
|
||||||
// that was listed in the header first comes first in the returned value.
|
|
||||||
//
|
|
||||||
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14 for more information.
|
|
||||||
func parse(header string) acceptSlice { |
|
||||||
mediaRanges := strings.Split(header, ",") |
|
||||||
accepted := make(acceptSlice, 0, len(mediaRanges)) |
|
||||||
for _, mediaRange := range mediaRanges { |
|
||||||
rangeParams, typeSubtype, err := parseMediaRange(mediaRange) |
|
||||||
if err != nil { |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
accept := headerAccept{ |
|
||||||
Type: typeSubtype[0], |
|
||||||
Subtype: typeSubtype[1], |
|
||||||
Q: 1.0, |
|
||||||
Extensions: make(map[string]string), |
|
||||||
} |
|
||||||
|
|
||||||
// If there is only one rangeParams, we can stop here.
|
|
||||||
if len(rangeParams) == 1 { |
|
||||||
accepted = append(accepted, accept) |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
// Validate the rangeParams.
|
|
||||||
validParams := true |
|
||||||
for _, v := range rangeParams[1:] { |
|
||||||
nameVal := strings.SplitN(v, "=", 2) |
|
||||||
if len(nameVal) != 2 { |
|
||||||
validParams = false |
|
||||||
break |
|
||||||
} |
|
||||||
nameVal[1] = strings.TrimSpace(nameVal[1]) |
|
||||||
if name := strings.TrimSpace(nameVal[0]); name == "q" { |
|
||||||
qval, err := strconv.ParseFloat(nameVal[1], 64) |
|
||||||
if err != nil || qval < 0 { |
|
||||||
validParams = false |
|
||||||
break |
|
||||||
} |
|
||||||
if qval > 1.0 { |
|
||||||
qval = 1.0 |
|
||||||
} |
|
||||||
accept.Q = qval |
|
||||||
} else { |
|
||||||
accept.Extensions[name] = nameVal[1] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if validParams { |
|
||||||
accepted = append(accepted, accept) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
sort.Sort(accepted) |
|
||||||
return accepted |
|
||||||
} |
|
||||||
|
|
||||||
// Len implements the Len() method of the Sort interface.
|
|
||||||
func (a acceptSlice) Len() int { |
|
||||||
return len(a) |
|
||||||
} |
|
||||||
|
|
||||||
// Less implements the Less() method of the Sort interface. Elements are
|
|
||||||
// sorted in order of decreasing preference.
|
|
||||||
func (a acceptSlice) Less(i, j int) bool { |
|
||||||
// Higher qvalues come first.
|
|
||||||
if a[i].Q > a[j].Q { |
|
||||||
return true |
|
||||||
} else if a[i].Q < a[j].Q { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
// Specific types come before wildcard types.
|
|
||||||
if a[i].Type != "*" && a[j].Type == "*" { |
|
||||||
return true |
|
||||||
} else if a[i].Type == "*" && a[j].Type != "*" { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
// Specific subtypes come before wildcard subtypes.
|
|
||||||
if a[i].Subtype != "*" && a[j].Subtype == "*" { |
|
||||||
return true |
|
||||||
} else if a[i].Subtype == "*" && a[j].Subtype != "*" { |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
// A lot of extensions comes before not a lot of extensions.
|
|
||||||
if len(a[i].Extensions) > len(a[j].Extensions) { |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
// Swap implements the Swap() method of the Sort interface.
|
|
||||||
func (a acceptSlice) Swap(i, j int) { |
|
||||||
a[i], a[j] = a[j], a[i] |
|
||||||
} |
|
||||||
|
|
||||||
// parseMediaRange parses the provided media range, and on success returns the
|
|
||||||
// parsed range params and type/subtype pair.
|
|
||||||
func parseMediaRange(mediaRange string) (rangeParams, typeSubtype []string, err error) { |
|
||||||
rangeParams = strings.Split(mediaRange, ";") |
|
||||||
typeSubtype = strings.Split(rangeParams[0], "/") |
|
||||||
|
|
||||||
// typeSubtype should have a length of exactly two.
|
|
||||||
if len(typeSubtype) > 2 { |
|
||||||
err = fmt.Errorf(errInvalidTypeSubtype, rangeParams[0]) |
|
||||||
return |
|
||||||
} else { |
|
||||||
typeSubtype = append(typeSubtype, "*") |
|
||||||
} |
|
||||||
|
|
||||||
// Sanitize typeSubtype.
|
|
||||||
typeSubtype[0] = strings.TrimSpace(typeSubtype[0]) |
|
||||||
typeSubtype[1] = strings.TrimSpace(typeSubtype[1]) |
|
||||||
if typeSubtype[0] == "" { |
|
||||||
typeSubtype[0] = "*" |
|
||||||
} |
|
||||||
if typeSubtype[1] == "" { |
|
||||||
typeSubtype[1] = "*" |
|
||||||
} |
|
||||||
|
|
||||||
return |
|
||||||
} |
|
@ -1,83 +0,0 @@ |
|||||||
package rsp |
|
||||||
|
|
||||||
import ( |
|
||||||
"github.com/labstack/echo/v4" |
|
||||||
"net/http" |
|
||||||
) |
|
||||||
|
|
||||||
// GuardFunc 参数守卫函数前面
|
|
||||||
type GuardFunc[T any] func(c echo.Context, req *T) error |
|
||||||
|
|
||||||
// ServeFunc 参数处理函数签名
|
|
||||||
type ServeFunc[T any, R any] func(c echo.Context, req *T) (*R, error) |
|
||||||
|
|
||||||
// ServeWithDataFunc 参数处理函数签名,支持自定义数据
|
|
||||||
type ServeWithDataFunc[T any, R any, O any] func(c echo.Context, req *T, opt O) (*R, error) |
|
||||||
|
|
||||||
// RespondFunc 数据响应函数前面
|
|
||||||
type RespondFunc[R any] func(c echo.Context, res *R) error |
|
||||||
|
|
||||||
// Handle 通用 CRUD 函数构造器,具体参数与函数 HandleWithData 保持一致
|
|
||||||
func Handle[T any, R any](guard GuardFunc[T], serve ServeFunc[T, R], respond ...RespondFunc[R]) echo.HandlerFunc { |
|
||||||
return HandleWithData[T, R, any](guard, func(c echo.Context, req *T, opt any) (*R, error) { |
|
||||||
return serve(c, req) |
|
||||||
}, nil, respond...) |
|
||||||
} |
|
||||||
|
|
||||||
// HandleWithData 通用 CRUD 函数构造器,可以预置数据
|
|
||||||
//
|
|
||||||
// 参数 guard 可以为空值,表示无参数守卫;
|
|
||||||
// 参数 serve 必传;
|
|
||||||
// 参数 data 为自定义数据,该值最好不可被修改;
|
|
||||||
// 参数 respond 为自定义响应函数,若未指定,内部将使用 Ok 或 Created 来响应结果。
|
|
||||||
func HandleWithData[T any, R any, D any](guard GuardFunc[T], serve ServeWithDataFunc[T, R, D], data D, respond ...RespondFunc[R]) echo.HandlerFunc { |
|
||||||
if serve == nil { |
|
||||||
panic("miss ServeFunc") |
|
||||||
} |
|
||||||
return func(c echo.Context) error { |
|
||||||
var req T |
|
||||||
if err := c.Bind(&req); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if err := c.Validate(&req); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if guard != nil { |
|
||||||
err := guard(c, &req) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
res, err := serve(c, &req, data) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
for _, send := range respond { |
|
||||||
if send != nil { |
|
||||||
return send(c, res) |
|
||||||
} |
|
||||||
} |
|
||||||
// 我们认为凡是 PUT 请求,都是创建数据
|
|
||||||
// 所以这里使用 Created 函数来响应数据。
|
|
||||||
if c.Request().Method == http.MethodPut { |
|
||||||
return Created(c, res) |
|
||||||
} |
|
||||||
return Ok(c, res) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func Bind[T any](c echo.Context, guards ...GuardFunc[T]) (*T, error) { |
|
||||||
var req T |
|
||||||
if err := c.Bind(&req); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
if err := c.Validate(&req); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
for _, guard := range guards { |
|
||||||
if err := guard(c, &req); err != nil { |
|
||||||
return nil, err |
|
||||||
} |
|
||||||
} |
|
||||||
return &req, nil |
|
||||||
} |
|
@ -0,0 +1,277 @@ |
|||||||
|
package rsp |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"github.com/labstack/echo/v4" |
||||||
|
"sort" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
type cache struct { |
||||||
|
hint int |
||||||
|
slice AcceptSlice |
||||||
|
} |
||||||
|
|
||||||
|
// Negotiator An HTTP content negotiator
|
||||||
|
//
|
||||||
|
// Accept: <MIME_type>/<MIME_subtype>
|
||||||
|
// Accept: <MIME_type>/*
|
||||||
|
// Accept: */*
|
||||||
|
// Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8
|
||||||
|
type Negotiator struct { |
||||||
|
capacity int // for cache
|
||||||
|
caches map[string]*cache |
||||||
|
} |
||||||
|
|
||||||
|
func NewNegotiator(capacity int) *Negotiator { |
||||||
|
if capacity <= 0 { |
||||||
|
capacity = 10 |
||||||
|
} |
||||||
|
return &Negotiator{ |
||||||
|
capacity: capacity, |
||||||
|
caches: make(map[string]*cache), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (n *Negotiator) Slice(header string) AcceptSlice { |
||||||
|
if c, ok := n.caches[header]; ok { |
||||||
|
c.hint++ |
||||||
|
return c.slice |
||||||
|
} |
||||||
|
if len(n.caches) >= n.capacity { |
||||||
|
var s string |
||||||
|
var hint int |
||||||
|
for i, x := range n.caches { |
||||||
|
if hint == 0 || hint < x.hint { |
||||||
|
hint = x.hint |
||||||
|
s = i |
||||||
|
} |
||||||
|
} |
||||||
|
delete(n.caches, s) |
||||||
|
} |
||||||
|
slice := newSlice(header) |
||||||
|
n.caches[header] = &cache{1, slice} |
||||||
|
return slice |
||||||
|
} |
||||||
|
|
||||||
|
func (n *Negotiator) Is(header string, expects ...string) bool { |
||||||
|
return n.Slice(header).Is(expects...) |
||||||
|
} |
||||||
|
|
||||||
|
func (n *Negotiator) Type(header string, expects ...string) string { |
||||||
|
return n.Slice(header).Type(expects...) |
||||||
|
} |
||||||
|
|
||||||
|
// Accept represents a parsed `Accept` header.
|
||||||
|
type Accept struct { |
||||||
|
Type, Subtype string |
||||||
|
Q float64 |
||||||
|
mime string |
||||||
|
} |
||||||
|
|
||||||
|
func (a *Accept) Mime() string { |
||||||
|
if a.mime == "" { |
||||||
|
a.mime = a.Type + "/" + a.Subtype |
||||||
|
} |
||||||
|
return a.mime |
||||||
|
} |
||||||
|
|
||||||
|
// AcceptSlice is a slice of accept.
|
||||||
|
type AcceptSlice []Accept |
||||||
|
|
||||||
|
// newSlice parses an HTTP Accept header and returns AcceptSlice, sorted in
|
||||||
|
// decreasing order of preference. If the header lists multiple types that
|
||||||
|
// have the same level of preference (same specificity of a type and subtype,
|
||||||
|
// same qvalue, and same number of extensions), the type that was listed
|
||||||
|
// in the header first comes in the returned value.
|
||||||
|
//
|
||||||
|
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14 for more information.
|
||||||
|
func newSlice(header string) AcceptSlice { |
||||||
|
mediaRanges := strings.Split(header, ",") |
||||||
|
accepted := make(AcceptSlice, 0, len(mediaRanges)) |
||||||
|
for _, mediaRange := range mediaRanges { |
||||||
|
rangeParams, typeSubtype, err := parseMediaRange(mediaRange) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
item := Accept{ |
||||||
|
Type: typeSubtype[0], |
||||||
|
Subtype: typeSubtype[1], |
||||||
|
Q: 1.0, |
||||||
|
} |
||||||
|
// If there is only one rangeParams, we can stop here.
|
||||||
|
if len(rangeParams) == 1 { |
||||||
|
accepted = append(accepted, item) |
||||||
|
continue |
||||||
|
} |
||||||
|
// Validate the rangeParams.
|
||||||
|
validParams := true |
||||||
|
for _, v := range rangeParams[1:] { |
||||||
|
nameVal := strings.SplitN(v, "=", 2) |
||||||
|
if len(nameVal) != 2 { |
||||||
|
validParams = false |
||||||
|
break |
||||||
|
} |
||||||
|
nameVal[1] = strings.TrimSpace(nameVal[1]) |
||||||
|
if name := strings.TrimSpace(nameVal[0]); name == "q" { |
||||||
|
qval, err := strconv.ParseFloat(nameVal[1], 64) |
||||||
|
if err != nil || qval < 0 { |
||||||
|
validParams = false |
||||||
|
break |
||||||
|
} |
||||||
|
if qval > 1.0 { |
||||||
|
qval = 1.0 |
||||||
|
} |
||||||
|
item.Q = qval |
||||||
|
//break // 不跳过,检查 validParams
|
||||||
|
} |
||||||
|
} |
||||||
|
if validParams { |
||||||
|
accepted = append(accepted, item) |
||||||
|
} |
||||||
|
} |
||||||
|
sort.Sort(accepted) |
||||||
|
return accepted |
||||||
|
} |
||||||
|
|
||||||
|
// Len implements the Len() method of the Sort interface.
|
||||||
|
func (a AcceptSlice) Len() int { |
||||||
|
return len(a) |
||||||
|
} |
||||||
|
|
||||||
|
// Less implements the Less() method of the Sort interface. Elements are
|
||||||
|
// sorted in order of decreasing preference.
|
||||||
|
func (a AcceptSlice) Less(i, j int) bool { |
||||||
|
// Higher qvalues come first.
|
||||||
|
if a[i].Q > a[j].Q { |
||||||
|
return true |
||||||
|
} else if a[i].Q < a[j].Q { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Specific types come before wildcard types.
|
||||||
|
if a[i].Type != "*" && a[j].Type == "*" { |
||||||
|
return true |
||||||
|
} else if a[i].Type == "*" && a[j].Type != "*" { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Specific subtypes come before wildcard subtypes.
|
||||||
|
if a[i].Subtype != "*" && a[j].Subtype == "*" { |
||||||
|
return true |
||||||
|
} else if a[i].Subtype == "*" && a[j].Subtype != "*" { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
// Swap implements the Swap() method of the Sort interface.
|
||||||
|
func (a AcceptSlice) Swap(i, j int) { |
||||||
|
a[i], a[j] = a[j], a[i] |
||||||
|
} |
||||||
|
|
||||||
|
func (a AcceptSlice) Is(expect ...string) bool { |
||||||
|
for _, e := range expect { |
||||||
|
for _, s := range a { |
||||||
|
if e == s.Mime() { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func (a AcceptSlice) Type(expects ...string) string { |
||||||
|
if len(expects) == 0 { |
||||||
|
return "" |
||||||
|
} |
||||||
|
var fuzzies [][2]string |
||||||
|
for _, expect := range expects { |
||||||
|
switch expect { |
||||||
|
case "html": |
||||||
|
if a.Is(echo.MIMETextHTML) { |
||||||
|
return expect |
||||||
|
} |
||||||
|
fuzzies = append(fuzzies, [2]string{expect, "text/*"}) |
||||||
|
case "json": |
||||||
|
if a.Is(echo.MIMEApplicationJSON) { |
||||||
|
return expect |
||||||
|
} |
||||||
|
fuzzies = append(fuzzies, [2]string{expect, "text/*"}) |
||||||
|
case "jsonp": |
||||||
|
if a.Is(echo.MIMEApplicationJavaScript) { |
||||||
|
return expect |
||||||
|
} |
||||||
|
case "xml": |
||||||
|
if a.Is(echo.MIMEApplicationXML, echo.MIMETextXML) { |
||||||
|
return expect |
||||||
|
} |
||||||
|
fuzzies = append(fuzzies, [2]string{expect, "text/*"}) |
||||||
|
case "form": |
||||||
|
if a.Is(echo.MIMEMultipartForm, echo.MIMEApplicationForm) { |
||||||
|
return expect |
||||||
|
} |
||||||
|
case "protobuf": |
||||||
|
if a.Is(echo.MIMEApplicationProtobuf) { |
||||||
|
return expect |
||||||
|
} |
||||||
|
case "msgpack": |
||||||
|
if a.Is(echo.MIMEApplicationMsgpack) { |
||||||
|
return expect |
||||||
|
} |
||||||
|
case "text", "string": |
||||||
|
if a.Is(echo.MIMETextPlain) { |
||||||
|
return expect |
||||||
|
} |
||||||
|
default: |
||||||
|
_, typeSubtype, err := parseMediaRange(expect) |
||||||
|
if err != nil { |
||||||
|
continue |
||||||
|
} |
||||||
|
if a.Is(typeSubtype[0] + "/" + typeSubtype[1]) { |
||||||
|
return expect |
||||||
|
} |
||||||
|
//if typeSubtype[0] == "text" {
|
||||||
|
// fuzzies = append(fuzzies, [2]string{expect, "text/*"})
|
||||||
|
//}
|
||||||
|
fuzzies = append(fuzzies, [2]string{expect, typeSubtype[0] + "/*"}) |
||||||
|
} |
||||||
|
} |
||||||
|
if fuzzies != nil { |
||||||
|
for _, f := range fuzzies { |
||||||
|
if a.Is(f[1]) { |
||||||
|
return f[0] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if a.Is("*/*") { |
||||||
|
return expects[0] |
||||||
|
} |
||||||
|
return "" |
||||||
|
} |
||||||
|
|
||||||
|
// parseMediaRange parses the provided media range, and on success returns the
|
||||||
|
// parsed range params and type/subtype pair.
|
||||||
|
func parseMediaRange(mediaRange string) (rangeParams, typeSubtype []string, err error) { |
||||||
|
rangeParams = strings.Split(mediaRange, ";") |
||||||
|
typeSubtype = strings.Split(rangeParams[0], "/") |
||||||
|
// typeSubtype should have a length of exactly two.
|
||||||
|
if len(typeSubtype) > 2 { |
||||||
|
err = fmt.Errorf("go-slim.dev/slim: invalid accept type '%s'", rangeParams[0]) |
||||||
|
return |
||||||
|
} else { |
||||||
|
typeSubtype = append(typeSubtype, "*") |
||||||
|
} |
||||||
|
// Sanitize typeSubtype.
|
||||||
|
typeSubtype[0] = strings.TrimSpace(typeSubtype[0]) |
||||||
|
typeSubtype[1] = strings.TrimSpace(typeSubtype[1]) |
||||||
|
if typeSubtype[0] == "" { |
||||||
|
typeSubtype[0] = "*" |
||||||
|
} |
||||||
|
if typeSubtype[1] == "" { |
||||||
|
typeSubtype[1] = "*" |
||||||
|
} |
||||||
|
return |
||||||
|
} |
Loading…
Reference in new issue