From 69032be6b262f02a69f10bebc1866e4bda2d3c84 Mon Sep 17 00:00:00 2001 From: hupeh Date: Thu, 12 Oct 2023 17:09:17 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=BA=93=EF=BC=8C=E6=94=AF=E6=8C=81=E9=AA=8C=E8=AF=81=E5=99=A8?= =?UTF-8?q?=E7=9A=84=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/rsp/accept.go | 171 ------------------------ pkg/rsp/error.go | 33 +++++ pkg/rsp/handler.go | 83 ------------ pkg/rsp/negotiator.go | 277 +++++++++++++++++++++++++++++++++++++++ pkg/rsp/respond_utils.go | 113 ++++++++++------ 5 files changed, 384 insertions(+), 293 deletions(-) delete mode 100644 pkg/rsp/accept.go delete mode 100644 pkg/rsp/handler.go create mode 100644 pkg/rsp/negotiator.go diff --git a/pkg/rsp/accept.go b/pkg/rsp/accept.go deleted file mode 100644 index 1cb5719..0000000 --- a/pkg/rsp/accept.go +++ /dev/null @@ -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 -} diff --git a/pkg/rsp/error.go b/pkg/rsp/error.go index 3a8cde9..4e6ebf7 100644 --- a/pkg/rsp/error.go +++ b/pkg/rsp/error.go @@ -88,6 +88,15 @@ func (e *Error) WithText(text ...string) *Error { return e } +func (e *Error) AsProblem(label string) *Problem { + return &Problem{ + Label: label, + Code: e.code, + Message: e.text, + Problems: nil, + } +} + func (e *Error) AsHttpError(code int) *echo.HTTPError { he := echo.NewHTTPError(code, e.text) return he.WithInternal(e) @@ -100,3 +109,27 @@ func (e *Error) String() string { func (e *Error) Error() string { return e.String() } + +type Problem struct { + Label string `json:"-" xml:"-"` + Code int `json:"code" xml:"code"` + Message string `json:"message" xml:"message"` + Problems map[string][]*Problem `json:"problems,omitempty" xml:"errors,omitempty"` +} + +func (p *Problem) AddSubproblem(err *Problem) { + if p.Problems == nil { + p.Problems = make(map[string][]*Problem) + } + if _, ok := p.Problems[err.Label]; !ok { + p.Problems[err.Label] = make([]*Problem, 0) + } + p.Problems[err.Label] = append(p.Problems[err.Label], err) +} + +func (p *Problem) Error() string { + return fmt.Sprintf( + "label=%s code=%d, message=%v", + p.Label, p.Code, p.Message, + ) +} diff --git a/pkg/rsp/handler.go b/pkg/rsp/handler.go deleted file mode 100644 index 76f307e..0000000 --- a/pkg/rsp/handler.go +++ /dev/null @@ -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 -} diff --git a/pkg/rsp/negotiator.go b/pkg/rsp/negotiator.go new file mode 100644 index 0000000..1c5f19e --- /dev/null +++ b/pkg/rsp/negotiator.go @@ -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: / +// Accept: /* +// 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 +} diff --git a/pkg/rsp/respond_utils.go b/pkg/rsp/respond_utils.go index 9ac2ab4..16bc188 100644 --- a/pkg/rsp/respond_utils.go +++ b/pkg/rsp/respond_utils.go @@ -4,8 +4,11 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "github.com/labstack/echo/v4" "net/http" + "runtime" + "sorbet/pkg/v" ) var ( @@ -13,6 +16,8 @@ var ( HtmlMarshaller func(map[string]any) (string, error) JsonpCallbacks []string DefaultJsonpCallback string + + negotiator *Negotiator ) func init() { @@ -20,6 +25,7 @@ func init() { HtmlMarshaller = toText JsonpCallbacks = []string{"callback", "cb", "jsonp"} DefaultJsonpCallback = "callback" + negotiator = NewNegotiator(10) } func toText(m map[string]any) (string, error) { @@ -104,88 +110,117 @@ func respond(c echo.Context, o *response) error { } }() + // 返回的数据 m := map[string]any{ "code": nil, "success": false, "message": o.message, } - var err *Error - if errors.As(o.err, &err) { - m["code"] = err.code - m["message"] = err.text - m["success"] = errors.Is(err, ErrOK) + var success bool + if err, ok := o.err.(*v.Errors); ok { + pb := &Problem{} + for _, e := range err.All() { + pb.AddSubproblem(ErrBadParams.WithText(e.Error()).AsProblem(e.Field())) + } + m["code"] = ErrBadParams.code + m["message"] = ErrBadParams.text + m["problems"] = pb.Problems + } else if ex, yes := o.err.(*Error); yes { + m["code"] = ex.code + m["message"] = ex.text + success = errors.Is(ex, ErrOK) + } else if pb, okay := o.err.(*Problem); okay { + m["code"] = pb.Code + m["message"] = pb.Message + m["problems"] = pb.Problems } else if o.err != nil { m["code"] = ErrInternal.code m["message"] = o.err.Error() } else { + success = true m["code"] = 0 - m["success"] = true if o.data != nil { - if v, ok := o.data.(RespondValuer); ok { - m["data"] = v.RespondValue() + if val, ok := o.data.(RespondValuer); ok { + m["data"] = val.RespondValue() } else { m["data"] = o.data } } } + m["success"] = success + if !success && c.Echo().Debug { + m["error"] = relevantCaller() + } if m["message"] == "" { m["message"] = http.StatusText(o.code) } - + // 如果已经输出过,就忽略 if c.Response().Committed { return nil } - + // 设置报头 if o.headers != nil { header := c.Response().Header() for key, value := range o.headers { header.Set(key, value) } } - + // 设置 cookie if o.cookies != nil { for _, cookie := range o.cookies { c.SetCookie(cookie) } } - + // HEAD 请求没有结果 r := c.Request() if r.Method == http.MethodHead { return c.NoContent(o.code) } - - slice := parse(r.Header.Get("Accept")) - for _, a := range slice { - switch x := a.Type + "/" + a.Subtype; x { - case echo.MIMEApplicationJavaScript: - qs := c.Request().URL.Query() - for _, name := range JsonpCallbacks { - if cb := qs.Get(name); cb != "" { - return c.JSONP(o.code, cb, m) - } - } - return c.JSONP(o.code, DefaultJsonpCallback, m) - case echo.MIMEApplicationJSON: - return c.JSON(o.code, m) - case echo.MIMEApplicationXML, echo.MIMETextXML: - return c.XML(o.code, m) - case echo.MIMETextHTML: - if html, err := HtmlMarshaller(m); err != nil { - return err - } else { - return c.HTML(o.code, html) - } - case echo.MIMETextPlain: - if text, err := TextMarshaller(m); err != nil { - return err - } else { - return c.String(o.code, text) + // 根据报头响应不同的格式 + accept := r.Header.Get(echo.HeaderAccept) + switch negotiator.Type(accept, "html", "json", "jsonp", "xml", "text", "text/*") { + case "html": + if html, err := HtmlMarshaller(m); err != nil { + return err + } else { + return c.HTML(o.code, html) + } + case "json": + return c.JSON(o.code, m) + case "jsonp": + qs := c.Request().URL.Query() + for _, name := range JsonpCallbacks { + if cb := qs.Get(name); cb != "" { + return c.JSONP(o.code, cb, m) } } + return c.JSONP(o.code, DefaultJsonpCallback, m) + case "xml": + return c.XML(o.code, m) + case "text", "text/*": + if text, err := TextMarshaller(m); err != nil { + return err + } else { + return c.String(o.code, text) + } } return c.JSON(o.code, m) } +func relevantCaller() []string { + pc := make([]uintptr, 16) + n := runtime.Callers(1, pc) + frames := runtime.CallersFrames(pc[:n]) + var traces []string + for { + frame, more := frames.Next() + traces = append(traces, fmt.Sprintf("%s:%s:%d", frame.File, frame.Func.Name(), frame.Line)) + if !more { + return traces + } + } +} + func Respond(c echo.Context, opts ...Option) error { o := response{code: http.StatusOK} for _, option := range opts {