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