feat: 重构响应库,支持验证器的错误

main
熊二 2 years ago
parent 45bc099bb6
commit 69032be6b2
  1. 171
      pkg/rsp/accept.go
  2. 33
      pkg/rsp/error.go
  3. 83
      pkg/rsp/handler.go
  4. 277
      pkg/rsp/negotiator.go
  5. 113
      pkg/rsp/respond_utils.go

@ -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
}

@ -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,
)
}

@ -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
}

@ -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 {

Loading…
Cancel
Save