diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 00000000..aabe5153 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,11 @@ +with-expecter: True +disable-version-string: True +dir: mocks/{{ replaceAll .InterfaceDirRelative "internal" "" }} +mockname: "{{.InterfaceName}}" +outpkg: "{{.PackageName}}" +filename: "{{.InterfaceName}}.go" +all: True +packages: + github.com/TheTNB/panel/internal/biz: + config: + recursive: True diff --git a/go.mod b/go.mod index 5e2ed3c7..b1aa3ea5 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,6 @@ require ( github.com/glebarez/sqlite v1.11.0 github.com/go-chi/chi/v5 v5.1.0 github.com/go-gormigrate/gormigrate/v2 v2.1.3 - github.com/go-playground/locales v0.14.1 - github.com/go-playground/universal-translator v0.18.1 - github.com/go-playground/validator/v10 v10.23.0 github.com/go-rat/chix v1.1.4 github.com/go-rat/gormstore v1.0.6 github.com/go-rat/sessions v1.0.11 @@ -21,6 +18,7 @@ require ( github.com/go-sql-driver/mysql v1.8.1 github.com/golang-cz/httplog v0.0.0-20241002114323-98e09d6f537a github.com/gomodule/redigo v1.9.2 + github.com/gookit/validate v1.5.3 github.com/gorilla/websocket v1.5.3 github.com/hashicorp/go-version v1.7.0 github.com/knadh/koanf/parsers/yaml v0.1.0 @@ -62,11 +60,12 @@ require ( github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/gofiber/schema v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gookit/filter v1.2.1 // indirect + github.com/gookit/goutil v0.6.18 // indirect github.com/jaevor/go-nanoid v1.4.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/knadh/koanf/maps v0.1.1 // indirect - github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index e8a6ea7f..f279c340 100644 --- a/go.sum +++ b/go.sum @@ -32,14 +32,6 @@ github.com/go-gormigrate/gormigrate/v2 v2.1.3 h1:ei3Vq/rpPI/jCJY9mRHJAKg5vU+EhZy github.com/go-gormigrate/gormigrate/v2 v2.1.3/go.mod h1:VJ9FIOBAur+NmQ8c4tDVwOuiJcgupTG105FexPFrXzA= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= -github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-rat/chix v1.1.4 h1:YVDwF4vrlkm5hmlDGWKoiWPgP98pJooBvhsto8UWq+o= github.com/go-rat/chix v1.1.4/go.mod h1:eKUTG7GXqMS278PH0QZi8yMWZddvMfxqWu7mverM6kQ= github.com/go-rat/gormstore v1.0.6 h1:YxmaY6n3JoTk6o9RXoZ1+cQ6HQL+ayf/6d8D76ydCAc= @@ -70,6 +62,14 @@ github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlG github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gookit/filter v1.2.1 h1:37XivkBm2E5qe1KaGdJ5ZfF5l9NYdGWfLEeQadJD8O4= +github.com/gookit/filter v1.2.1/go.mod h1:rxynQFr793x+XDwnRmJFEb53zDw0Zqx3OD7TXWoR9mQ= +github.com/gookit/goutil v0.6.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw= +github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA= +github.com/gookit/validate v1.5.3 h1:czD1H+fcOJX+dfY0vzjHSMb+jrKTMeI8Fhu59ZMIB9o= +github.com/gookit/validate v1.5.3/go.mod h1:1OfvI1eGJqSODHERKdxvPGcconT4eTvVLmYt1A3QZlI= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= @@ -94,8 +94,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ= @@ -151,6 +149,8 @@ github.com/tufanbarisyildirim/gonginx v0.0.0-20241205102811-323481085fb4 h1:p5dI github.com/tufanbarisyildirim/gonginx v0.0.0-20241205102811-323481085fb4/go.mod h1:hdMWBc1+TyB6G5ZZBBgPWQ8cjRZ6zpYdhal0uu6E9QM= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/internal/app/global.go b/internal/app/global.go index 2b266a66..21eb7f10 100644 --- a/internal/app/global.go +++ b/internal/app/global.go @@ -4,8 +4,6 @@ import ( "log/slog" "github.com/go-chi/chi/v5" - ut "github.com/go-playground/universal-translator" - "github.com/go-playground/validator/v10" "github.com/go-rat/sessions" "github.com/go-rat/utils/crypt" "github.com/knadh/koanf/v2" @@ -16,16 +14,14 @@ import ( ) var ( - Conf *koanf.Koanf - Http *chi.Mux - Orm *gorm.DB - Validator *validator.Validate - Translator *ut.Translator - Session *sessions.Manager - Cron *cron.Cron - Queue *queue.Queue - Logger *slog.Logger - Crypter crypt.Crypter + Conf *koanf.Koanf + Http *chi.Mux + Orm *gorm.DB + Session *sessions.Manager + Cron *cron.Cron + Queue *queue.Queue + Logger *slog.Logger + Crypter crypt.Crypter ) // 面板状态常量 diff --git a/internal/bootstrap/app.go b/internal/bootstrap/app.go index f0b5cafe..79286242 100644 --- a/internal/bootstrap/app.go +++ b/internal/bootstrap/app.go @@ -19,7 +19,6 @@ func boot() { func BootWeb() { boot() - initValidator() initSession() initQueue() initCron() diff --git a/internal/bootstrap/validator.go b/internal/bootstrap/validator.go index e54e8e5f..4d64fe19 100644 --- a/internal/bootstrap/validator.go +++ b/internal/bootstrap/validator.go @@ -1,30 +1,15 @@ package bootstrap import ( - "log" - - "github.com/go-playground/locales/zh_Hans_CN" - ut "github.com/go-playground/universal-translator" - "github.com/go-playground/validator/v10" - "github.com/go-playground/validator/v10/translations/zh" - - "github.com/TheTNB/panel/internal/app" - "github.com/TheTNB/panel/internal/http/rule" + "github.com/gookit/validate" + "github.com/gookit/validate/locales/zhcn" ) -func initValidator() { - translator := zh_Hans_CN.New() - uni := ut.New(translator, translator) - trans, _ := uni.GetTranslator("zh_Hans_CN") - - validate := validator.New(validator.WithRequiredStructEnabled()) - if err := zh.RegisterDefaultTranslations(validate, trans); err != nil { - log.Fatalf("failed to register validator translations: %v", err) - } - - app.Translator = &trans - app.Validator = validate - if err := rule.RegisterRules(validate); err != nil { - log.Fatalf("failed to register validator rules: %v", err) - } +func init() { + zhcn.RegisterGlobal() + validate.Config(func(opt *validate.GlobalOption) { + opt.StopOnError = false + opt.SkipOnEmpty = true + opt.FieldTag = "form" + }) } diff --git a/internal/http/request/request.go b/internal/http/request/request.go index ffb7fc89..48df3cea 100644 --- a/internal/http/request/request.go +++ b/internal/http/request/request.go @@ -16,6 +16,10 @@ type WithRules interface { Rules(r *http.Request) map[string]string } +type WithFilters interface { + Filters(r *http.Request) map[string]string +} + type WithMessages interface { Messages(r *http.Request) map[string]string } diff --git a/internal/http/rule/exists.go b/internal/http/rule/exists.go index b5d480ce..9843f642 100644 --- a/internal/http/rule/exists.go +++ b/internal/http/rule/exists.go @@ -2,12 +2,16 @@ package rule import ( "fmt" - "strings" - "github.com/go-playground/validator/v10" "gorm.io/gorm" ) +// Exists 验证一个值在某个表中的字段中存在,支持同时判断多个字段 +// Exists verify a value exists in a table field, support judging multiple fields at the same time +// 用法:exists:表名称,字段名称,字段名称,字段名称 +// Usage: exists:table_name,field_name,field_name,field_name +// 例子:exists:users,phone,email +// Example: exists:users,phone,email type Exists struct { DB *gorm.DB } @@ -16,20 +20,17 @@ func NewExists(db *gorm.DB) *Exists { return &Exists{DB: db} } -// Exists 格式 `exists=categories id other_field` -func (r *Exists) Exists(fl validator.FieldLevel) bool { - requestValue := fl.Field().Interface() - params := strings.Fields(fl.Param()) - if len(params) < 2 { +func (r *Exists) Passes(val any, options ...any) bool { + if len(options) < 2 { return false } - tableName := params[0] - fieldNames := params[1:] + tableName := options[0].(string) + fieldNames := options[1:] - query := r.DB.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), requestValue) + query := r.DB.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), val) for _, fieldName := range fieldNames[1:] { - query = query.Or(fmt.Sprintf("%s = ?", fieldName), requestValue) + query = query.Or(fmt.Sprintf("%s = ?", fieldName), val) } var count int64 diff --git a/internal/http/rule/not_exists.go b/internal/http/rule/not_exists.go index b5919c4c..7ee89bb5 100644 --- a/internal/http/rule/not_exists.go +++ b/internal/http/rule/not_exists.go @@ -2,12 +2,16 @@ package rule import ( "fmt" - "strings" - "github.com/go-playground/validator/v10" "gorm.io/gorm" ) +// NotExists 验证一个值在某个表中的字段中不存在,支持同时判断多个字段 +// NotExists verify a value does not exist in a table field, support judging multiple fields at the same time +// 用法:not_exists:表名称,字段名称,字段名称,字段名称 +// Usage: not_exists:table_name,field_name,field_name,field_name +// 例子:not_exists:users,phone,email +// Example: not_exists:users,phone,email type NotExists struct { DB *gorm.DB } @@ -16,20 +20,17 @@ func NewNotExists(db *gorm.DB) *NotExists { return &NotExists{DB: db} } -// NotExists 格式 `not_exists=categories id other_field` -func (r *NotExists) NotExists(fl validator.FieldLevel) bool { - requestValue := fl.Field().Interface() - params := strings.Fields(fl.Param()) - if len(params) < 2 { +func (r *NotExists) Passes(val any, options ...any) bool { + if len(options) < 2 { return false } - tableName := params[0] - fieldNames := params[1:] + tableName := options[0].(string) + fieldNames := options[1:] - query := r.DB.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), requestValue) + query := r.DB.Table(tableName).Where(fmt.Sprintf("%s = ?", fieldNames[0]), val) for _, fieldName := range fieldNames[1:] { - query = query.Or(fmt.Sprintf("%s = ?", fieldName), requestValue) + query = query.Or(fmt.Sprintf("%s = ?", fieldName), val) } var count int64 diff --git a/internal/http/rule/password.go b/internal/http/rule/password.go index cc8948b3..ab1d6bda 100644 --- a/internal/http/rule/password.go +++ b/internal/http/rule/password.go @@ -3,18 +3,18 @@ package rule import ( "unicode" - "github.com/go-playground/validator/v10" + "github.com/spf13/cast" ) +// Password 密码复杂度校验 type Password struct{} func NewPassword() *Password { return &Password{} } -// Password 密码复杂度校验 -func (r *Password) Password(fl validator.FieldLevel) bool { - password := fl.Field().String() +func (r *Password) Passes(val any, options ...any) bool { + password := cast.ToString(val) // 不对空密码进行校验,有需要可以使用 required 标签 if password == "" { return true diff --git a/internal/http/rule/regex.go b/internal/http/rule/regex.go deleted file mode 100644 index 4d56f8b1..00000000 --- a/internal/http/rule/regex.go +++ /dev/null @@ -1,29 +0,0 @@ -package rule - -import ( - "regexp" - "strings" - - "github.com/go-playground/validator/v10" -) - -type Regex struct{} - -func NewRegex() *Regex { - return &Regex{} -} - -func (r *Regex) Regex(fl validator.FieldLevel) bool { - // 从标签中获取正则,格式类似于 `regex=^[a-zA-Z0-9_]+$` - pattern := fl.Param() - // 替换转义字符 - pattern = strings.ReplaceAll(pattern, ",", ",") - - re, err := regexp.Compile(pattern) - if err != nil { - return false - } - - value := fl.Field().String() - return re.MatchString(value) -} diff --git a/internal/http/rule/rule.go b/internal/http/rule/rule.go index 548b62c7..0331741b 100644 --- a/internal/http/rule/rule.go +++ b/internal/http/rule/rule.go @@ -1,66 +1,19 @@ package rule import ( - ut "github.com/go-playground/universal-translator" - "github.com/go-playground/validator/v10" - - "github.com/TheTNB/panel/internal/app" + "github.com/gookit/validate" + "gorm.io/gorm" ) -func RegisterRules(v *validator.Validate) error { - if err := v.RegisterValidation("exists", NewExists(app.Orm).Exists); err != nil { - return err - } - if err := v.RegisterValidation("not_exists", NewNotExists(app.Orm).NotExists); err != nil { - return err - } - if err := v.RegisterValidation("regex", NewRegex().Regex); err != nil { - return err - } - if err := v.RegisterValidation("password", NewPassword().Password); err != nil { - return err - } - - if err := v.RegisterTranslation("exists", *app.Translator, - func(ut ut.Translator) error { - return ut.Add("exists", "{0} 不存在", true) - }, - func(ut ut.Translator, fe validator.FieldError) string { - t, _ := ut.T("exists", fe.Field()) - return t - }); err != nil { - return err - } - if err := v.RegisterTranslation("not_exists", *app.Translator, - func(ut ut.Translator) error { - return ut.Add("not_exists", "{0} 已存在", true) - }, - func(ut ut.Translator, fe validator.FieldError) string { - t, _ := ut.T("not_exists", fe.Field()) - return t - }); err != nil { - return err - } - if err := v.RegisterTranslation("regex", *app.Translator, - func(ut ut.Translator) error { - return ut.Add("regex", "{0} 格式不正确", true) - }, - func(ut ut.Translator, fe validator.FieldError) string { - t, _ := ut.T("regex", fe.Field()) - return t - }); err != nil { - return err - } - if err := v.RegisterTranslation("password", *app.Translator, - func(ut ut.Translator) error { - return ut.Add("password", "密码不满足要求(8-20位,至少包含字母、数字、特殊字符中的两种)", true) - }, - func(ut ut.Translator, fe validator.FieldError) string { - t, _ := ut.T("password") - return t - }); err != nil { - return err - } - - return nil +func GlobalRules(db *gorm.DB) { + validate.AddValidators(validate.M{ + "exists": NewExists(db).Passes, + "not_exists": NewNotExists(db).Passes, + "password": NewPassword().Passes, + }) + validate.AddGlobalMessages(map[string]string{ + "exists": "{field} 不存在", + "not_exists": "{field} 已存在", + "password": "密码不满足要求(8-20位,至少包含字母、数字、特殊字符中的两种)", + }) } diff --git a/internal/service/base.go b/internal/service/helper.go similarity index 68% rename from internal/service/base.go rename to internal/service/helper.go index a9d220c3..2abc2530 100644 --- a/internal/service/base.go +++ b/internal/service/helper.go @@ -1,16 +1,14 @@ package service import ( - "errors" "fmt" "net/http" "slices" "strings" - "github.com/go-playground/validator/v10" "github.com/go-rat/chix" + "github.com/gookit/validate" - "github.com/TheTNB/panel/internal/app" "github.com/TheTNB/panel/internal/http/request" ) @@ -53,7 +51,7 @@ func ErrorSystem(w http.ResponseWriter) { render.Header(chix.HeaderContentType, chix.MIMEApplicationJSONCharsetUTF8) // must before Status() render.Status(http.StatusInternalServerError) render.JSON(&ErrorResponse{ - Message: "系统内部错误", + Message: http.StatusText(http.StatusInternalServerError), }) } @@ -64,12 +62,6 @@ func Bind[T any](r *http.Request) (*T, error) { // 绑定参数 binder := chix.NewBind(r) defer binder.Release() - if err := binder.URI(req); err != nil { - return nil, err - } - if err := binder.Query(req); err != nil { - return nil, err - } if slices.Contains([]string{"POST", "PUT", "PATCH", "DELETE"}, strings.ToUpper(r.Method)) { if r.ContentLength > 0 { if err := binder.Body(req); err != nil { @@ -77,48 +69,58 @@ func Bind[T any](r *http.Request) (*T, error) { } } } + if err := binder.Query(req); err != nil { + return nil, err + } + if err := binder.URI(req); err != nil { + return nil, err + } // 准备验证 + df, err := validate.FromStruct(req) + if err != nil { + return nil, err + } + v := df.Create() + if reqWithPrepare, ok := any(req).(request.WithPrepare); ok { - if err := reqWithPrepare.Prepare(r); err != nil { + if err = reqWithPrepare.Prepare(r); err != nil { return nil, err } } if reqWithAuthorize, ok := any(req).(request.WithAuthorize); ok { - if err := reqWithAuthorize.Authorize(r); err != nil { + if err = reqWithAuthorize.Authorize(r); err != nil { return nil, err } } if reqWithRules, ok := any(req).(request.WithRules); ok { if rules := reqWithRules.Rules(r); rules != nil { - app.Validator.RegisterStructValidationMapRules(rules, req) + for key, value := range rules { + v.StringRule(key, value) + } + } + } + if reqWithFilters, ok := any(req).(request.WithFilters); ok { + if filters := reqWithFilters.Filters(r); filters != nil { + v.FilterRules(filters) + } + } + if reqWithMessages, ok := any(req).(request.WithMessages); ok { + if messages := reqWithMessages.Messages(r); messages != nil { + v.AddMessages(messages) } } - // 验证参数 - err := app.Validator.Struct(req) - if err == nil { + // 开始验证 + if v.Validate() && v.IsSuccess() { return req, nil } - // 翻译错误信息 - var errs validator.ValidationErrors - if errors.As(err, &errs) { - for _, e := range errs { - if reqWithMessages, ok := any(req).(request.WithMessages); ok { - if msg, found := reqWithMessages.Messages(r)[fmt.Sprintf("%s.%s", e.Field(), e.Tag())]; found { - return nil, errors.New(msg) - } - } - return nil, errors.New(e.Translate(*app.Translator)) // nolint:staticcheck - } - } - - return nil, err + return nil, v.Errors.OneError() } // Paginate 取分页条目 -func Paginate[T any](r *http.Request, allItems []T) (pagedItems []T, total uint) { +func Paginate[T any](r *http.Request, items []T) (pagedItems []T, total uint) { req, err := Bind[request.Paginate](r) if err != nil { req = &request.Paginate{ @@ -126,19 +128,19 @@ func Paginate[T any](r *http.Request, allItems []T) (pagedItems []T, total uint) Limit: 10, } } - total = uint(len(allItems)) - startIndex := (req.Page - 1) * req.Limit - endIndex := req.Page * req.Limit + total = uint(len(items)) + start := (req.Page - 1) * req.Limit + end := req.Page * req.Limit if total == 0 { return []T{}, 0 } - if startIndex > total { + if start > total { return []T{}, total } - if endIndex > total { - endIndex = total + if end > total { + end = total } - return allItems[startIndex:endIndex], total + return items[start:end], total }