package crud import ( "errors" "github.com/labstack/echo/v4" "net/http" "reflect" "sorbet/pkg/db" "sorbet/pkg/rsp" "strconv" "strings" ) type ( // RequestGuarder 参数守卫函数签名 RequestGuarder func(c echo.Context, req any) error // BodyBinder 将请求体绑定到结构体上 BodyBinder interface { BindBody(c echo.Context, i any) (err error) } ) // 使用内置数据绑定函数 // 默认使用 echo.DefaultBinder var binder BodyBinder // Validate 验证数据 func Validate(c echo.Context, t any, guards ...RequestGuarder) error { err := c.Validate(t) if err != nil && !errors.Is(err, echo.ErrValidatorNotRegistered) { return err } if v, ok := t.(interface{ Validate() error }); ok { err = v.Validate() if err != nil { return err } } for _, guard := range guards { err = guard(c, t) if err != nil { return err } } return nil } // Bind 将提交的参数绑定到泛型 T 的实例上 func Bind[T any](c echo.Context, guards ...RequestGuarder) (*T, error) { var req T if err := c.Bind(&req); err != nil { return nil, err } if err := Validate(c, &req, guards...); err != nil { return nil, err } return &req, nil } // BindBody 将提交请求体绑定到泛型 T 的实例上 func BindBody[T any](c echo.Context, guards ...RequestGuarder) (*T, error) { b, ok := c.Echo().Binder.(BodyBinder) if !ok || b == nil { if binder == nil { binder = &echo.DefaultBinder{} } b = binder } var req T if err := b.BindBody(c, &req); err != nil { return nil, err } if err := Validate(c, &req, guards...); err != nil { return nil, err } return &req, nil } type identifier struct { ID any `json:"id" xml:"id"` } // BindID 自请求中获取需要的数据编号 // 查找的是名称为 `id` 的数据,查找顺序如下 // 1. 查看路由中是否定义,比如:`/configs/:id` // 2. 查看查询字符串中是否存在,比如:`?id=123` // 3. 查看请求体中是否存在,支持 json 和 xml 两种格式。 func BindID(c echo.Context) (id any, err error) { defer func() { if recovered := recover(); recovered != nil { c.Logger().Debugf("%#v", recovered) id = nil err = rsp.ErrBadParams } }() for i, name := range c.ParamNames() { if name == "id" { id = c.ParamValues()[i] if id != "" { return } break } } switch c.Request().Method { case http.MethodGet, http.MethodDelete, http.MethodHead: for key, values := range c.QueryParams() { if key == "id" { id = values[0] if id != "" { return } break } } } if c.Request().ContentLength == 0 { err = rsp.ErrBadParams return } var data *identifier data, err = BindBody[identifier](c) if err != nil { return nil, err } if data.ID == nil { return nil, rsp.ErrBadParams } rv := reflect.ValueOf(data.ID) if rv.IsZero() || rv.IsNil() { return nil, rsp.ErrBadParams } return data.ID, nil } func BindQuery[T any](c echo.Context, qb *db.QueryBuilder[T]) (page, limit int, err error) { query := c.QueryParams() var paginating bool for key, values := range query { switch key { case "sortby": for _, s := range values { if s[0] == '+' { qb.AscentBy(s[1:]) } else if s[0] == '-' { qb.DescentBy(s[1:]) } else { qb.AscentBy(s) } } case "limit", "page": var v int if values[0] != "" { v, err = strconv.Atoi(values[0]) if err != nil { return } } if v <= 0 { err = rsp.ErrInternal return } if key == "limit" { qb.Limit(v) limit = v } else { paginating = true page = max(v, 1) } default: v := values[0] i := strings.IndexByte(key, '#') if i == -1 { qb.Eq(key, v) continue } switch k, op := key[:i], key[i+1:]; op { case "=": qb.Eq(k, v) case "!=": qb.Neq(k, v) case "<": qb.Lt(k, v) case "<=": qb.Lte(k, v) case ">": qb.Gt(k, v) case ">=": qb.Gte(k, v) case "<>", "><": var less, more any switch len(values) { case 2: less, more = values[0], values[1] case 1: vs := strings.Split(v, ",") if len(vs) != 2 || vs[0] == "" || vs[1] == "" { err = rsp.ErrBadParams return } less, more = vs[0], vs[1] default: err = rsp.ErrBadParams return } if op == "<>" { qb.Between(k, less, more) } else { qb.NotBetween(k, key, more) } case "nil": qb.IsNull(k) case "!nil": qb.NotNull(k) case "~": qb.Like(k, v) case "!~": qb.NotLike(k, v) case "in", "!in": if len(values) == 1 { values = strings.Split(v, ",") } vs := make([]any, len(values)) for i, value := range values { vs[i] = value } if op == "in" { qb.In(k, vs...) } else { qb.NotIn(k, vs...) } default: qb.Eq(key, v) } } } if paginating { if limit == 0 { limit = 30 qb.Limit(limit) } qb.Offset((page - 1) * limit) } return }