go项目脚手架
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sorbet/pkg/crud/echo_utils.go

246 lines
4.8 KiB

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
}