Compare commits

...

7 Commits

  1. 28
      internal/init.go
  2. 27
      internal/repositories/ioc.go
  3. 19
      internal/services/company/service.go
  4. 20
      internal/services/config/service.go
  5. 17
      internal/services/feature/service.go
  6. 17
      internal/services/resource/service.go
  7. 35
      internal/services/service.go
  8. 17
      internal/services/system/service.go
  9. 16
      pkg/app/applet.go
  10. 20
      pkg/app/controller.go
  11. 20
      pkg/app/service.go
  12. 228
      pkg/ioc/container.go
  13. 96
      pkg/ioc/ioc.go
  14. 19
      pkg/ioc/util.go
  15. 607
      pkg/is/is.go
  16. 79
      pkg/is/regexes.go
  17. 211
      pkg/is/util.go
  18. 171
      pkg/rsp/accept.go
  19. 33
      pkg/rsp/error.go
  20. 14
      pkg/rsp/error_handler.go
  21. 83
      pkg/rsp/handler.go
  22. 277
      pkg/rsp/negotiator.go
  23. 91
      pkg/rsp/respond_utils.go
  24. 246
      pkg/v/error.go
  25. 52
      pkg/v/matcher.go
  26. 144
      pkg/v/translations.go
  27. 112
      pkg/v/v.go
  28. 696
      pkg/v/valuer.go

@ -3,26 +3,19 @@ package internal
import (
"errors"
"github.com/labstack/echo/v4"
echoSwagger "github.com/swaggo/echo-swagger"
"gorm.io/gorm"
"net/http"
"sorbet/internal/entities"
"sorbet/internal/middleware"
"sorbet/internal/repositories"
"sorbet/internal/util"
"sorbet/pkg/db"
"sorbet/pkg/env"
"sorbet/pkg/ioc"
"sorbet/pkg/log"
"sorbet/pkg/rsp"
"sorbet/pkg/ticket"
)
func Init() error {
ioc.Bind(db.DB()) // 注入数据库操作
ioc.Bind(log.Default()) // 注入日志操作
repositories.Init() // 注入数据仓库操作
// 同步数据库结构
if err := syncEntities(); err != nil {
if !errors.Is(err, db.ErrNoCodeFirst) {
@ -32,7 +25,6 @@ func Init() error {
log.Error("同步数据表结构需要开启 [DB_CODE_FIRST],在生产模式下请务必关闭。")
}
}
return nil
}
@ -64,26 +56,11 @@ func Start() error {
e := echo.New()
e.HideBanner = true
e.HidePort = true
e.HTTPErrorHandler = func(err error, c echo.Context) {
if !c.Response().Committed {
http.Error(c.Response(), err.Error(), 500)
}
}
e.HTTPErrorHandler = rsp.HTTPErrorHandler
e.Logger = util.NewLogger()
e.Use(middleware.Recover())
e.Use(middleware.CORS())
e.Use(middleware.Logger)
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
db := ioc.MustGet[gorm.DB]().WithContext(c.Request().Context())
ci := ioc.Fork()
ci.Bind(db)
c.Set("db", db)
c.Set("ioc", ci)
return next(c)
}
})
e.GET("/swagger/*", echoSwagger.WrapHandler)
e.GET("/", func(c echo.Context) error {
repo := repositories.NewCompanyRepository(c.Get("db").(*gorm.DB))
company, err := repo.GetByID(c.Request().Context(), 1)
@ -103,15 +80,12 @@ func Start() error {
"token": token,
})
})
e.Group("", middleware.Ticket(false, "system")).GET("/u", func(c echo.Context) error {
return rsp.Ok(c, echo.Map{
"ticket": c.Get("ticket"),
"claims": c.Get("ticket_claims").(*ticket.Claims),
})
})
e.Logger.Fatal(e.Start(":1323"))
return nil
}

@ -1,27 +0,0 @@
package repositories
import (
"sorbet/pkg/ioc"
)
func Init() {
ioc.MustFactory(NewCompanyRepository)
ioc.MustFactory(NewCompanyDepartmentRepository)
ioc.MustFactory(NewCompanyStaffRepository)
ioc.MustFactory(NewConfigRepository)
ioc.MustFactory(NewConfigGroupRepository)
ioc.MustFactory(NewFeatureRepository)
ioc.MustFactory(NewFeatureCategoryRepository)
ioc.MustFactory(NewFeatureConfigRepository)
ioc.MustFactory(NewFeatureContentRepository)
ioc.MustFactory(NewFeatureContentChapterRepository)
ioc.MustFactory(NewFeatureContentDetailRepository)
ioc.MustFactory(NewResourceRepository)
ioc.MustFactory(NewResourceCategoryRepository)
ioc.MustFactory(NewSystemLogRepository)
ioc.MustFactory(NewSystemMenuRepository)
ioc.MustFactory(NewSystemPermissionRepository)
ioc.MustFactory(NewSystemRoleRepository)
ioc.MustFactory(NewSystemRolePowerRepository)
ioc.MustFactory(NewSystemUserRepository)
}

@ -7,17 +7,20 @@ import (
type Service struct{}
func (s *Service) Init(ctx *app.Context) error {
ctx.Routes(&controller.CompanyController{})
ctx.Routes(&controller.CompanyDepartmentController{})
ctx.Routes(&controller.CompanyStaffController{})
return nil
func (*Service) Name() string {
return "company"
}
func (s *Service) Start() error {
return nil
func (*Service) Priority() int {
return 1
}
func (s *Service) Stop() error {
func (*Service) Init(ctx *app.Context) error {
ctx.Routes(&controller.CompanyController{})
ctx.Routes(&controller.CompanyDepartmentController{})
ctx.Routes(&controller.CompanyStaffController{})
return nil
}
func (s *Service) Bootstrap() error { return nil }
func (s *Service) Destroy() error { return nil }

@ -5,19 +5,21 @@ import (
"sorbet/pkg/app"
)
type Service struct {
}
type Service struct{}
func (s *Service) Init(ctx *app.Context) error {
ctx.Routes(&controller.ConfigGroupController{})
ctx.Routes(&controller.ConfigController{})
return nil
func (*Service) Name() string {
return "config"
}
func (s *Service) Start() error {
return nil
func (*Service) Priority() int {
return 0
}
func (s *Service) Stop() error {
func (*Service) Init(ctx *app.Context) error {
ctx.Routes(&controller.ConfigGroupController{})
ctx.Routes(&controller.ConfigController{})
return nil
}
func (s *Service) Bootstrap() error { return nil }
func (s *Service) Destroy() error { return nil }

@ -7,6 +7,14 @@ import (
type Service struct{}
func (*Service) Name() string {
return "feature"
}
func (*Service) Priority() int {
return 3
}
func (s *Service) Init(ctx *app.Context) error {
ctx.Routes(&controller.FeatureCategoryController{})
ctx.Routes(&controller.FeatureConfigController{})
@ -16,10 +24,5 @@ func (s *Service) Init(ctx *app.Context) error {
return nil
}
func (s *Service) Start() error {
return nil
}
func (s *Service) Stop() error {
return nil
}
func (s *Service) Bootstrap() error { return nil }
func (s *Service) Destroy() error { return nil }

@ -7,16 +7,19 @@ import (
type Service struct{}
func (s *Service) Init(ctx *app.Context) error {
ctx.Routes(&controller.ResourceController{})
ctx.Routes(&controller.ResourceCategoryController{})
return nil
func (*Service) Name() string {
return "resource"
}
func (s *Service) Start() error {
return nil
func (*Service) Priority() int {
return 2
}
func (s *Service) Stop() error {
func (*Service) Init(ctx *app.Context) error {
ctx.Routes(&controller.ResourceController{})
ctx.Routes(&controller.ResourceCategoryController{})
return nil
}
func (s *Service) Bootstrap() error { return nil }
func (s *Service) Destroy() error { return nil }

@ -2,7 +2,9 @@ package services
import (
"context"
"errors"
"github.com/labstack/echo/v4"
"slices"
"sorbet/internal/services/company"
"sorbet/internal/services/config"
"sorbet/internal/services/feature"
@ -11,26 +13,41 @@ import (
"sorbet/pkg/app"
)
var applets []app.Applet
var services []app.Service
func Register(service app.Service) error {
for _, applet := range services {
if applet.Name() == service.Name() {
return errors.New("service already registered")
}
}
services = append(services, service)
return nil
}
func Init() {
applets = []app.Applet{
services = []app.Service{
&config.Service{},
&company.Service{},
&resource.Service{},
&feature.Service{},
&system.Service{},
}
// 按优先级排序
slices.SortFunc(services, func(a, b app.Service) int {
return b.Priority() - a.Priority()
})
}
func Start(ctx context.Context) error {
func Bootstrap(ctx context.Context) error {
e := ctx.Value("echo_framework").(*echo.Echo)
for _, service := range applets {
for _, service := range services {
err := service.Init(app.NewContext(ctx, e.Group("")))
if err != nil {
return err
}
err = service.Start()
err = service.Bootstrap()
if err != nil {
return err
}
@ -38,10 +55,12 @@ func Start(ctx context.Context) error {
return nil
}
func Stop() error {
for _, service := range applets {
err := service.Stop()
func Destroy() error {
for i := len(services) - 1; i >= 0; i++ {
service := services[i]
err := service.Destroy()
if err != nil {
// TODO(hupeh): 是否需要销毁策略,比如可以继续或者中断等行为
return err
}
}

@ -7,6 +7,14 @@ import (
type Service struct{}
func (*Service) Name() string {
return "system"
}
func (*Service) Priority() int {
return 3
}
func (s *Service) Init(ctx *app.Context) error {
ctx.Routes(&controller.SystemLogController{})
ctx.Routes(&controller.SystemMenuController{})
@ -17,10 +25,5 @@ func (s *Service) Init(ctx *app.Context) error {
return nil
}
func (s *Service) Start() error {
return nil
}
func (s *Service) Stop() error {
return nil
}
func (s *Service) Bootstrap() error { return nil }
func (s *Service) Destroy() error { return nil }

@ -1,16 +0,0 @@
package app
import "github.com/labstack/echo/v4"
type Applet interface {
// Init 初始化服务
Init(ctx *Context) error
// Start 启动服务
Start() error
// Stop 停止服务
Stop() error
}
type Routable interface {
InitRoutes(r *echo.Group)
}

@ -5,7 +5,6 @@ import (
"gorm.io/gorm"
"reflect"
"sorbet/pkg/db"
"sorbet/pkg/ioc"
"sorbet/pkg/rsp"
)
@ -72,16 +71,15 @@ func (ctr *Controller[Entity, Upsert]) RegisterRoutes(path string, r *echo.Group
// ORM 获取 gorm.DB 实例
func (ctr *Controller[Entity, Upsert]) ORM(c echo.Context) (*gorm.DB, error) {
orm, err := ioc.Get[gorm.DB]()
if err != nil {
return nil, err
if orm, ok := c.Get("orm").(*gorm.DB); ok {
return orm, nil
}
return orm.WithContext(c.Request().Context()), nil
return db.DB().WithContext(c.Request().Context()), nil
}
// Repository 获取 Repository 实例
func (ctr *Controller[Entity, Upsert]) Repository() (*db.Repository[Entity], error) {
orm, err := ioc.Get[gorm.DB]()
func (ctr *Controller[Entity, Upsert]) Repository(c echo.Context) (*db.Repository[Entity], error) {
orm, err := ctr.ORM(c)
if err != nil {
return nil, err
}
@ -103,7 +101,7 @@ func (ctr *Controller[Entity, Upsert]) upsert(c echo.Context, isCreate bool) err
if err != nil {
return err
}
repo, err := ctr.Repository()
repo, err := ctr.Repository(c)
if err != nil {
return err
}
@ -142,7 +140,7 @@ func (ctr *Controller[Entity, Upsert]) Delete(c echo.Context) error {
if err != nil {
return err
}
repo, err := ctr.Repository()
repo, err := ctr.Repository(c)
if err != nil {
return err
}
@ -159,7 +157,7 @@ func (ctr *Controller[Entity, Upsert]) Get(c echo.Context) error {
if err != nil {
return err
}
repo, err := ctr.Repository()
repo, err := ctr.Repository(c)
if err != nil {
return err
}
@ -172,7 +170,7 @@ func (ctr *Controller[Entity, Upsert]) Get(c echo.Context) error {
// List 获取数据列表
func (ctr *Controller[Entity, Upsert]) List(c echo.Context) error {
repo, err := ctr.Repository()
repo, err := ctr.Repository(c)
if err != nil {
return err
}

@ -0,0 +1,20 @@
package app
import "github.com/labstack/echo/v4"
type Service interface {
// Name 服务名称
Name() string
// Priority 优先级,用于启动和销毁的执行顺序
Priority() int
// Init 初始化服务
Init(ctx *Context) error
// Bootstrap 启动服务
Bootstrap() error
// Destroy 销毁服务
Destroy() error
}
type Routable interface {
InitRoutes(r *echo.Group)
}

@ -1,228 +0,0 @@
package ioc
import (
"errors"
"fmt"
"reflect"
)
type binding struct {
name string
typ reflect.Type
resolver any
shared bool
}
func (b *binding) make(c *Container) (reflect.Value, error) {
if v, exists := c.instances[b.typ][b.name]; exists {
return v, nil
}
val, err := c.Invoke(b.resolver)
if err != nil {
return reflect.Value{}, err
}
rv := val[0]
if len(val) == 2 {
err = val[1].Interface().(error)
if err != nil {
return reflect.Value{}, err
}
}
if b.shared {
if _, exists := c.instances[b.typ]; !exists {
c.instances[b.typ] = make(map[string]reflect.Value)
}
c.instances[b.typ][b.name] = rv
}
return rv, nil
}
func (b *binding) mustMake(c *Container) reflect.Value {
val, err := b.make(c)
if err != nil {
panic(err)
}
return val
}
type Container struct {
// 注册的工厂函数
factories map[reflect.Type]map[string]*binding
// 注册的共享实例
instances map[reflect.Type]map[string]reflect.Value
parent *Container
}
func New() *Container {
return &Container{
factories: make(map[reflect.Type]map[string]*binding),
instances: make(map[reflect.Type]map[string]reflect.Value),
parent: nil,
}
}
// Fork 分支
func (c *Container) Fork() *Container {
ioc := New()
ioc.parent = c
return ioc
}
// Bind 绑定值到容器,有效类型:
// - 接口的具体实现值
// - 结构体的实例
// - 类型的值(尽量不要使用原始类型,而应该使用元素类型的变体)
func (c *Container) Bind(instance any) {
c.NamedBind("", instance)
}
// NamedBind 绑定具名值到容器
func (c *Container) NamedBind(name string, instance any) {
//typ := InterfaceOf(instance)
typ := reflect.TypeOf(instance)
if _, ok := c.instances[typ]; !ok {
c.instances[typ] = make(map[string]reflect.Value)
}
c.instances[typ][name] = reflect.ValueOf(instance)
}
// Factory 绑定工厂函数
func (c *Container) Factory(factory any, shared ...bool) error {
return c.NamedFactory("", factory, shared...)
}
// NamedFactory 绑定具名工厂函数
func (c *Container) NamedFactory(name string, factory any, shared ...bool) error {
reflectedFactory := reflect.TypeOf(factory)
if reflectedFactory.Kind() != reflect.Func {
return errors.New("container: the factory must be a function")
}
if returnCount := reflectedFactory.NumOut(); returnCount == 0 || returnCount > 2 {
return errors.New("container: factory function signature is invalid - it must return abstract, or abstract and error")
}
// TODO(hupeh): 验证第二个参数必须是 error 接口
concreteType := reflectedFactory.Out(0)
for i := 0; i < reflectedFactory.NumIn(); i++ {
// 循环依赖
if reflectedFactory.In(i) == concreteType {
return fmt.Errorf("container: factory function signature is invalid - depends on abstract it returns")
}
}
if _, exists := c.factories[concreteType]; !exists {
c.factories[concreteType] = make(map[string]*binding)
}
bd := &binding{
name: name,
typ: concreteType,
resolver: factory,
shared: false,
}
for _, b := range shared {
bd.shared = b
}
c.factories[concreteType][name] = bd
return nil
}
// Resolve 完成的注入
func (c *Container) Resolve(i any) error {
v := reflect.ValueOf(i)
for v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return errors.New("must given a struct")
}
t := v.Type()
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
structField := t.Field(i)
inject, willInject := structField.Tag.Lookup("inject")
if !f.CanSet() {
if willInject {
return fmt.Errorf("container: cannot make %v field", t.Field(i).Name)
}
continue
}
ft := f.Type()
fv := c.NamedGet(inject, ft)
if !fv.IsValid() {
return fmt.Errorf("value not found for type %v", ft)
}
f.Set(fv)
}
return nil
}
// Get 获取指定类型的值
func (c *Container) Get(t reflect.Type) reflect.Value {
return c.NamedGet("", t)
}
// NamedGet 通过注入的名称获取指定类型的值
func (c *Container) NamedGet(name string, t reflect.Type) reflect.Value {
val, exists := c.instances[t][name]
if exists && val.IsValid() {
return val
}
if factory, exists := c.factories[t][name]; exists {
val = factory.mustMake(c)
}
if val.IsValid() || t.Kind() != reflect.Interface {
goto RESULT
}
// 使用共享值里面该接口的实现者
for k, v := range c.instances {
if k.Implements(t) {
for _, value := range v {
if value.IsValid() {
val = value
goto RESULT
}
}
break
}
}
// 使用工厂函数里面该接口的实现者
for k, v := range c.factories {
if k.Implements(t) {
for _, bd := range v {
if x := bd.mustMake(c); x.IsValid() {
val = x
goto RESULT
}
}
break
}
}
RESULT:
if !val.IsValid() && c.parent != nil {
val = c.parent.NamedGet(name, t)
}
return val
}
// Invoke 执行函数
func (c *Container) Invoke(f any) ([]reflect.Value, error) {
t := reflect.TypeOf(f)
if t.Kind() != reflect.Func {
return nil, errors.New("container: invalid function")
}
var in = make([]reflect.Value, t.NumIn()) //Panic if t is not kind of Func
for i := 0; i < t.NumIn(); i++ {
argType := t.In(i)
val := c.Get(argType)
if !val.IsValid() {
return nil, fmt.Errorf("value not found for type %v", argType)
}
in[i] = val
}
return reflect.ValueOf(f).Call(in), nil
}

@ -1,96 +0,0 @@
package ioc
import (
"errors"
"reflect"
)
var (
ErrValueNotFound = errors.New("ioc: value not found")
)
var global = New()
// Fork 分支
func Fork() *Container {
return global.Fork()
}
// Bind 绑定值到容器,有效类型:
//
// - 接口的具体实现值
// - 结构体的实例
// - 类型的值(尽量不要使用原始类型,而应该使用元素类型的变体)
func Bind(instance any) {
global.Bind(instance)
}
// NamedBind 绑定具名值到容器
func NamedBind(name string, instance any) {
global.NamedBind(name, instance)
}
// Factory 绑定工厂函数
func Factory(factory any, shared ...bool) error {
return global.Factory(factory, shared...)
}
func MustFactory(factory any, shared ...bool) {
err := Factory(factory, shared...)
if err != nil {
panic(err)
}
}
// NamedFactory 绑定具名工厂函数
func NamedFactory(name string, factory any, shared ...bool) error {
return global.NamedFactory(name, factory, shared...)
}
func MustNamedFactory(name string, factory any, shared ...bool) {
err := NamedFactory(name, factory, shared...)
if err != nil {
panic(err)
}
}
// Resolve 完成的注入
func Resolve(i any) error {
return global.Resolve(i)
}
// Get 获取指定类型的值
func Get[T any]() (*T, error) {
return NamedGet[T]("")
}
func MustGet[T any]() *T {
return MustNamedGet[T]("")
}
// NamedGet 通过注入的名称获取指定类型的值
func NamedGet[T any](name string) (*T, error) {
var abs T
t := reflect.TypeOf(&abs)
v := global.NamedGet(name, t)
if !v.IsValid() {
return nil, ErrValueNotFound
}
if x, ok := v.Interface().(*T); ok {
return x, nil
}
return nil, ErrValueNotFound
}
func MustNamedGet[T any](name string) *T {
v, err := NamedGet[T](name)
if err != nil {
panic(err)
}
return v
}
// Invoke 执行函数
func Invoke(f any) ([]reflect.Value, error) {
return global.Invoke(f)
}

@ -1,19 +0,0 @@
package ioc
import "reflect"
// InterfaceOf dereferences a pointer to an Interface type.
// It panics if value is not a pointer to an interface.
func InterfaceOf(value interface{}) reflect.Type {
t := reflect.TypeOf(value)
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Interface {
panic("the value is not a pointer to an interface. (*MyInterface)(nil)")
}
return t
}

@ -0,0 +1,607 @@
package is
import (
"encoding/json"
"net"
"net/url"
"os"
"reflect"
"strconv"
"strings"
"time"
)
// Email 验证给出的字符串是不是有效的邮箱地址
func Email(str string) bool {
return emailRegex.MatchString(str)
}
// E164 判断给出的字符串是否符合 e.164 规范的手机号码
func E164(str string) bool {
return e164Regex.MatchString(str)
}
// PhoneNumber 判断给出的字符串是否符合中国大陆规范的手机号码
func PhoneNumber(str string) bool {
return phoneNumberRegex.MatchString(str)
}
// Semver 判断给出的字符串是否符合语义化版本号规范
func Semver(str string) bool {
return semverRegex.MatchString(str)
}
// Base64 判断给出的字符串是否为base64数据
func Base64(str string) bool {
return base64Regex.MatchString(str)
}
// URL 判断给出的字符串是否为有效的URL
func URL(s string) bool {
var i int
// checks needed as of Go 1.6 because of change https://github.com/golang/go/commit/617c93ce740c3c3cc28cdd1a0d712be183d0b328#diff-6c2d018290e298803c0c9419d8739885L195
// emulate browser and strip the '#' suffix prior to validation. see issue-#237
if i = strings.Index(s, "#"); i > -1 {
s = s[:i]
}
if len(s) == 0 {
return false
}
u, err := url.ParseRequestURI(s)
if err != nil || u.Scheme == "" {
return false
}
return true
}
// Base64URL 判断给出的字符串是否为有效且安全的 base64URL
func Base64URL(str string) bool {
return base64URLRegex.MatchString(str)
}
// JWT is the validation function for validating if the current field's value is a valid JWT string.
func JWT(str string) bool {
return jWTRegex.MatchString(str)
}
// UUID5 is the validation function for validating if the field's value is a valid v5 UUID.
func UUID5(str string) bool {
return uUID5Regex.MatchString(str)
}
// UUID4 is the validation function for validating if the field's value is a valid v4 UUID.
func UUID4(str string) bool {
return uUID4Regex.MatchString(str)
}
// UUID3 is the validation function for validating if the field's value is a valid v3 UUID.
func UUID3(str string) bool {
return uUID3Regex.MatchString(str)
}
// UUID is the validation function for validating if the field's value is a valid UUID of any version.
func UUID(str string) bool {
return uUIDRegex.MatchString(str)
}
// ULID is the validation function for validating if the field's value is a valid ULID.
func ULID(str string) bool {
return uLIDRegex.MatchString(str)
}
// MD4 is the validation function for validating if the field's value is a valid MD4.
func MD4(str string) bool {
return md4Regex.MatchString(str)
}
// MD5 is the validation function for validating if the field's value is a valid MD5.
func MD5(str string) bool {
return md5Regex.MatchString(str)
}
// SHA256 is the validation function for validating if the field's value is a valid SHA256.
func SHA256(str string) bool {
return sha256Regex.MatchString(str)
}
// SHA384 is the validation function for validating if the field's value is a valid SHA384.
func SHA384(str string) bool {
return sha384Regex.MatchString(str)
}
// SHA512 is the validation function for validating if the field's value is a valid SHA512.
func SHA512(str string) bool {
return sha512Regex.MatchString(str)
}
// ASCII is the validation function for validating if the field's value is a valid ASCII character.
func ASCII(str string) bool {
return aSCIIRegex.MatchString(str)
}
// Alpha is the validation function for validating if the current field's value is a valid alpha value.
func Alpha(str string) bool {
return alphaRegex.MatchString(str)
}
// Alphanumeric is the validation function for validating if the current field's value is a valid alphanumeric value.
func Alphanumeric(str string) bool {
return alphaNumericRegex.MatchString(str)
}
// AlphaUnicode is the validation function for validating if the current field's value is a valid alpha unicode value.
func AlphaUnicode(str string) bool {
return alphaUnicodeRegex.MatchString(str)
}
// AlphanumericUnicode is the validation function for validating if the current field's value is a valid alphanumeric unicode value.
func AlphanumericUnicode(str string) bool {
return alphaUnicodeNumericRegex.MatchString(str)
}
// Numeric is the validation function for validating if the current field's value is a valid numeric value.
func Numeric[T any](t T) bool {
ctx := reflect.ValueOf(t)
switch ctx.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64:
return true
default:
return numericRegex.MatchString(ctx.String())
}
}
// Number is the validation function for validating if the current field's value is a valid number.
func Number[T any](t T) bool {
rv := reflect.ValueOf(t)
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64:
return true
default:
return numberRegex.MatchString(rv.String())
}
}
// Boolean is the validation function for validating if the current field's value can be safely converted to a boolean.
func Boolean[T any](t T) bool {
ref := reflect.ValueOf(t)
switch ref.Kind() {
case reflect.String:
switch ref.String() {
case "1", "yes", "YES", "Yes", "on", "ON", "On", "true", "TRUE", "True",
"0", "no", "NO", "No", "", "off", "OFF", "Off", "false", "FALSE", "False":
return true
default:
return false
}
case reflect.Int, reflect.Int32, reflect.Int64:
n := ref.Int()
return n == 0 || n == 1
case reflect.Uint, reflect.Uint32, reflect.Uint64:
n := ref.Uint()
return n == 0 || n == 1
case reflect.Bool:
return ref.Bool()
}
return false
}
// Default is the opposite of required aka HasValue
func Default(val any) bool {
return !HasValue(val)
}
// HasValue is the validation function for validating if the current field's value is not the default static value.
func HasValue(val any) bool {
rv := reflect.ValueOf(val)
switch rv.Kind() {
case reflect.Slice, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func:
return !rv.IsNil()
default:
return rv.IsValid() && rv.Interface() != reflect.Zero(rv.Type()).Interface()
}
}
// Hexadecimal is the validation function for validating if the current field's value is a valid hexadecimal.
func Hexadecimal(str string) bool {
return hexadecimalRegex.MatchString(str)
}
// HEXColor is the validation function for validating if the current field's value is a valid HEX color.
func HEXColor(str string) bool {
return hexColorRegex.MatchString(str)
}
// RGB is the validation function for validating if the current field's value is a valid RGB color.
func RGB(str string) bool {
return rgbRegex.MatchString(str)
}
// RGBA is the validation function for validating if the current field's value is a valid RGBA color.
func RGBA(str string) bool {
return rgbaRegex.MatchString(str)
}
// HSL is the validation function for validating if the current field's value is a valid HSL color.
func HSL(str string) bool {
return hslRegex.MatchString(str)
}
// HSLA is the validation function for validating if the current field's value is a valid HSLA color.
func HSLA(str string) bool {
return hslaRegex.MatchString(str)
}
func Color(str string) bool {
return HEXColor(str) || HSLA(str) || HSL(str) || RGB(str) || RGBA(str)
}
// Latitude is the validation function for validating if the field's value is a valid latitude coordinate.
func Latitude[T any](t T) bool {
ref := reflect.ValueOf(t)
var v string
switch ref.Kind() {
case reflect.String:
v = ref.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
v = strconv.FormatInt(ref.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v = strconv.FormatUint(ref.Uint(), 10)
case reflect.Float32:
v = strconv.FormatFloat(ref.Float(), 'f', -1, 32)
case reflect.Float64:
v = strconv.FormatFloat(ref.Float(), 'f', -1, 64)
default:
//fmt.Errorf("bad ref type %T", ref.Interface())
return false
}
return latitudeRegex.MatchString(v)
}
// Longitude is the validation function for validating if the field's value is a valid longitude coordinate.
func Longitude[T any](t T) bool {
ref := reflect.ValueOf(t)
var v string
switch ref.Kind() {
case reflect.String:
v = ref.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
v = strconv.FormatInt(ref.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v = strconv.FormatUint(ref.Uint(), 10)
case reflect.Float32:
v = strconv.FormatFloat(ref.Float(), 'f', -1, 32)
case reflect.Float64:
v = strconv.FormatFloat(ref.Float(), 'f', -1, 64)
default:
//fmt.Errorf("bad field type %T", ref.Interface())
return false
}
return longitudeRegex.MatchString(v)
}
// JSON is the validation function for validating if the current field's value is a valid json string.
func JSON[T any](t T) bool {
rv := reflect.ValueOf(t)
if rv.Type() == nilType {
return json.Valid(rv.Bytes())
}
if rv.Kind() == reflect.String {
return json.Valid([]byte(rv.String()))
}
return false
}
func Datetime(str, layout string) bool {
_, err := time.Parse(layout, str)
return err == nil
}
// Timezone is the validation function for validating if the current field's value is a valid time zone string.
func Timezone(str string) bool {
// empty value is converted to UTC by time.LoadLocation but disallow it as it is not a valid time zone name
if str == "" {
return false
}
// Local value is converted to the current system time zone by time.LoadLocation but disallow it as it is not a valid time zone name
if strings.ToLower(str) == "local" {
return false
}
_, err := time.LoadLocation(str)
return err == nil
}
// IPv4 is the validation function for validating if a value is a valid v4 IP address.
func IPv4(str string) bool {
ip := net.ParseIP(str)
return ip != nil && ip.To4() != nil
}
// IPv6 is the validation function for validating if the field's value is a valid v6 IP address.
func IPv6(str string) bool {
ip := net.ParseIP(str)
return ip != nil && ip.To4() == nil
}
// IP is the validation function for validating if the field's value is a valid v4 or v6 IP address.
func IP(str string) bool {
ip := net.ParseIP(str)
return ip != nil
}
// MAC is the validation function for validating if the field's value is a valid MAC address.
func MAC(str string) bool {
_, err := net.ParseMAC(str)
return err == nil
}
// Lowercase is the validation function for validating if the current field's value is a lowercase string.
func Lowercase(str string) bool {
if str == "" {
return false
}
return str == strings.ToLower(str)
}
// Uppercase is the validation function for validating if the current field's value is an uppercase string.
func Uppercase(str string) bool {
if str == "" {
return false
}
return str == strings.ToUpper(str)
}
// Empty checks if a value is empty or not.
// A value is considered empty if
// - integer, float: zero
// - bool: false
// - string, array: len() == 0
// - slice, map: nil or len() == 0
// - interface, pointer: nil or the referenced value is empty
func Empty[T any](t T) bool {
rv := reflect.ValueOf(t)
switch rv.Kind() {
case reflect.String, reflect.Array, reflect.Map, reflect.Slice:
return rv.Len() == 0
case reflect.Bool:
return !rv.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return rv.Uint() == 0
case reflect.Float32, reflect.Float64:
return rv.Float() == 0
case reflect.Invalid:
return true
case reflect.Interface, reflect.Ptr:
if rv.IsNil() {
return true
}
return Empty(rv.Elem().Interface())
case reflect.Struct:
v, ok := rv.Interface().(time.Time)
if ok && v.IsZero() {
return true
}
}
return false
}
func NotEmpty[T any](t T) bool {
return !Empty(t)
}
func URLEncoded(str string) bool {
return uRLEncodedRegex.MatchString(str)
}
func HTMLEncoded(str string) bool {
return hTMLEncodedRegex.MatchString(str)
}
func HTML(str string) bool {
return hTMLRegex.MatchString(str)
}
// File is the validation function for validating if the current field's value is a valid file path.
func File(val any) bool {
field := reflect.ValueOf(val)
switch field.Kind() {
case reflect.String:
fileInfo, err := os.Stat(field.String())
if err != nil {
return false
}
return !fileInfo.IsDir()
}
//fmt.Errorf("bad value type %T", field.Interface())
return false
}
// Dir is the validation function for validating if the current field's value is a valid directory.
func Dir(val any) bool {
field := reflect.ValueOf(val)
if field.Kind() == reflect.String {
fileInfo, err := os.Stat(field.String())
if err != nil {
return false
}
return fileInfo.IsDir()
}
//fmt.Errorf("bad field type %T", field.Interface())
return false
}
func OneOf(val any, vals []any) bool {
//rv := reflect.ValueOf(val)
//
//var v string
//switch rv.Kind() {
//case reflect.String:
// v = rv.String()
//case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
// v = strconv.FormatInt(rv.Int(), 10)
//case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
// v = strconv.FormatUint(rv.Uint(), 10)
//default:
// //fmt.Errorf("Bad rv type %T", rv.Interface())
// return false
//}
//for i := 0; i < len(vals); i++ {
// if vals[i] == v {
// return true
// }
//}
if len(vals) == 0 {
return false
}
for _, a := range vals {
if Equal(val, a) {
return true
}
}
return false
}
func Length(val any, length int, op string) bool {
if n := calcLength(val); n == -1 {
return false
} else {
return Compare(n, length, op)
}
}
func LengthBetween(val any, min, max int) bool {
if !Compare(min, max, "<") {
panic(ErrBadType)
} else if n, err := getLength(val, false); err != nil {
return false
} else {
return Compare(n, min, ">=") && Compare(n, max, "<=")
}
}
// Compare intX,floatX value by given op. returns `srcVal op(=,!=,<,<=,>,>=) dstVal`
//
// Usage:
//
// compare(2, 3, ">") // false
// compare(2, 1.3, ">") // true
// compare(2.2, 1.3, ">") // true
// compare(2.1, 2, ">") // true
func Compare(srcVal, dstVal any, op string) bool {
srv := reflect.ValueOf(srcVal)
switch srv.Kind() {
case reflect.Struct:
if srv.Type().ConvertibleTo(timeType) {
drv := reflect.ValueOf(dstVal)
if drv.Type().ConvertibleTo(timeType) {
at := srv.Convert(timeType).Interface().(time.Time)
bt := drv.Convert(timeType).Interface().(time.Time)
return compTime(at, bt, op)
}
}
case reflect.Bool:
drv := reflect.ValueOf(dstVal)
switch drv.Kind() {
case reflect.Bool:
return compBool(srv.Bool(), drv.Bool(), op)
case reflect.String:
if bl, err := strconv.ParseBool(drv.String()); err == nil {
return compBool(srv.Bool(), bl, op)
}
}
default:
if srcStr, ok := srcVal.(string); ok {
if dstStr, ok2 := dstVal.(string); ok2 {
return compString(srcStr, dstStr, op)
}
break
}
// float
if srcFlt, ok := srcVal.(float64); ok {
if dstFlt, err := toFloat(dstVal); err == nil {
return compNum(srcFlt, dstFlt, op)
}
break
}
if srcFlt, ok := srcVal.(float32); ok {
if dstFlt, err := toFloat(dstVal); err == nil {
return compNum(float64(srcFlt), dstFlt, op)
}
break
}
// as int64
if srcInt, err := toInt64(srcVal); err != nil {
break
} else if dstInt, ex := toInt64(dstVal); ex != nil {
break
} else {
return compNum(srcInt, dstInt, op)
}
}
switch op {
case "=":
return srcVal == dstVal
case "!=":
return srcVal != dstVal
default:
//ErrBadType
return false
}
}
// GreaterThan is the validation function for validating if the current field's value is greater than the param's value.
func GreaterThan(a, b any) bool {
return Compare(a, b, ">")
}
// GreaterEqualThan is the validation function for validating if the current field's value is greater than or equal to the param's value.
func GreaterEqualThan(a, b any) bool {
return Compare(a, b, ">=")
}
// LessThan is the validation function for validating if the current field's value is less than the param's value.
func LessThan(a, b any) bool {
return Compare(a, b, "<")
}
// LessEqualThan is the validation function for validating if the current field's value is less than or equal to the param's value.
func LessEqualThan(a, b any) bool {
return Compare(a, b, "<=")
}
// Equal is the validation function for validating if the current field's value is equal to the param's value.
func Equal(a, b any) bool {
return Compare(a, b, "=")
}
func NotEqual(a, b any) bool {
return Compare(a, b, "!=")
}
func Between(val, min, max any) bool {
if !Compare(min, max, ">") {
panic(ErrBadRange)
}
return Compare(val, min, ">=") && Compare(val, max, "<=")
}
func NotBetween(val, min, max any) bool {
if !Compare(min, max, ">") {
panic(ErrBadRange)
}
return Compare(val, min, "<") || Compare(val, max, ">")
}

@ -0,0 +1,79 @@
package is
import "regexp"
const (
alphaRegexString = "^[a-zA-Z]+$"
alphaNumericRegexString = "^[a-zA-Z0-9]+$"
alphaUnicodeRegexString = "^[\\p{L}]+$"
alphaUnicodeNumericRegexString = "^[\\p{L}\\p{N}]+$"
numericRegexString = "^[-+]?[0-9]+(?:\\.[0-9]+)?$"
numberRegexString = "^[0-9]+$"
hexadecimalRegexString = "^(0[xX])?[0-9a-fA-F]+$"
hexColorRegexString = "^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
rgbRegexString = "^rgb\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*\\)$"
rgbaRegexString = "^rgba\\(\\s*(?:(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])|(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%\\s*,\\s*(?:0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$"
hslRegexString = "^hsl\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*\\)$"
hslaRegexString = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$"
emailRegexString = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
e164RegexString = "^\\+[1-9]?[0-9]{7,14}$"
phoneNumberRegexString = "^(\\+?86)?1[0-9]{10}$"
base64RegexString = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
base64URLRegexString = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2}==|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{4})$"
uUID3RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
uUID4RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
uUID5RegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
uUIDRegexString = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
uLIDRegexString = "^[A-HJKMNP-TV-Z0-9]{26}$"
md4RegexString = "^[0-9a-f]{32}$"
md5RegexString = "^[0-9a-f]{32}$"
sha256RegexString = "^[0-9a-f]{64}$"
sha384RegexString = "^[0-9a-f]{96}$"
sha512RegexString = "^[0-9a-f]{128}$"
aSCIIRegexString = "^[\x00-\x7F]*$"
latitudeRegexString = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
longitudeRegexString = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
uRLEncodedRegexString = `^(?:[^%]|%[0-9A-Fa-f]{2})*$`
hTMLEncodedRegexString = `&#[x]?([0-9a-fA-F]{2})|(&gt)|(&lt)|(&quot)|(&amp)+[;]?`
hTMLRegexString = `<[/]?([a-zA-Z]+).*?>`
jWTRegexString = "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]*$"
semverRegexString = `^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` // numbered capture groups https://semver.org/
)
var (
alphaRegex = regexp.MustCompile(alphaRegexString)
alphaNumericRegex = regexp.MustCompile(alphaNumericRegexString)
alphaUnicodeRegex = regexp.MustCompile(alphaUnicodeRegexString)
alphaUnicodeNumericRegex = regexp.MustCompile(alphaUnicodeNumericRegexString)
numericRegex = regexp.MustCompile(numericRegexString)
numberRegex = regexp.MustCompile(numberRegexString)
hexadecimalRegex = regexp.MustCompile(hexadecimalRegexString)
hexColorRegex = regexp.MustCompile(hexColorRegexString)
rgbRegex = regexp.MustCompile(rgbRegexString)
rgbaRegex = regexp.MustCompile(rgbaRegexString)
hslRegex = regexp.MustCompile(hslRegexString)
hslaRegex = regexp.MustCompile(hslaRegexString)
e164Regex = regexp.MustCompile(e164RegexString)
phoneNumberRegex = regexp.MustCompile(phoneNumberRegexString)
emailRegex = regexp.MustCompile(emailRegexString)
base64Regex = regexp.MustCompile(base64RegexString)
base64URLRegex = regexp.MustCompile(base64URLRegexString)
uUID3Regex = regexp.MustCompile(uUID3RegexString)
uUID4Regex = regexp.MustCompile(uUID4RegexString)
uUID5Regex = regexp.MustCompile(uUID5RegexString)
uUIDRegex = regexp.MustCompile(uUIDRegexString)
uLIDRegex = regexp.MustCompile(uLIDRegexString)
md4Regex = regexp.MustCompile(md4RegexString)
md5Regex = regexp.MustCompile(md5RegexString)
sha256Regex = regexp.MustCompile(sha256RegexString)
sha384Regex = regexp.MustCompile(sha384RegexString)
sha512Regex = regexp.MustCompile(sha512RegexString)
aSCIIRegex = regexp.MustCompile(aSCIIRegexString)
latitudeRegex = regexp.MustCompile(latitudeRegexString)
longitudeRegex = regexp.MustCompile(longitudeRegexString)
uRLEncodedRegex = regexp.MustCompile(uRLEncodedRegexString)
hTMLEncodedRegex = regexp.MustCompile(hTMLEncodedRegexString)
hTMLRegex = regexp.MustCompile(hTMLRegexString)
jWTRegex = regexp.MustCompile(jWTRegexString)
semverRegex = regexp.MustCompile(semverRegexString)
)

@ -0,0 +1,211 @@
package is
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strconv"
"strings"
"time"
"unicode/utf8"
)
var (
ErrBadType = errors.New("bad value type")
ErrBadRange = errors.New("bad value range")
timeType = reflect.TypeOf(time.Time{})
nilType = reflect.TypeOf([]byte(nil))
)
// convert value to float64, return error on failed
func toFloat(in any) (f64 float64, err error) {
switch tVal := in.(type) {
case nil:
f64 = 0
case string:
f64, err = strconv.ParseFloat(strings.TrimSpace(tVal), 64)
case int:
f64 = float64(tVal)
case int8:
f64 = float64(tVal)
case int16:
f64 = float64(tVal)
case int32:
f64 = float64(tVal)
case int64:
f64 = float64(tVal)
case uint:
f64 = float64(tVal)
case uint8:
f64 = float64(tVal)
case uint16:
f64 = float64(tVal)
case uint32:
f64 = float64(tVal)
case uint64:
f64 = float64(tVal)
case float32:
f64 = float64(tVal)
case float64:
f64 = tVal
case time.Duration:
f64 = float64(tVal)
case json.Number:
f64, err = tVal.Float64()
default:
err = ErrBadType
}
return
}
// convert string to int64, return error on failed
func toInt64(in any) (i64 int64, err error) {
switch tVal := in.(type) {
case nil:
i64 = 0
case string:
i64, err = strconv.ParseInt(strings.TrimSpace(tVal), 10, 0)
case int:
i64 = int64(tVal)
case int8:
i64 = int64(tVal)
case int16:
i64 = int64(tVal)
case int32:
i64 = int64(tVal)
case int64:
i64 = tVal
case uint:
i64 = int64(tVal)
case uint8:
i64 = int64(tVal)
case uint16:
i64 = int64(tVal)
case uint32:
i64 = int64(tVal)
case uint64:
i64 = int64(tVal)
case float32:
i64 = int64(tVal)
case float64:
i64 = int64(tVal)
case time.Duration:
i64 = int64(tVal)
case json.Number:
i64, err = tVal.Int64()
default:
err = ErrBadType
}
return
}
// compString compare string, returns the first op second
func compString(first, second, op string) bool {
rs := strings.Compare(first, second)
if rs < 0 {
return op == "<" || op == "<="
} else if rs > 0 {
return op == ">" || op == ">="
} else {
return op == ">=" || op == "<=" || op == "="
}
}
func compTime(first, dstTime time.Time, op string) (ok bool) {
switch op {
case "<":
return first.Before(dstTime)
case "<=":
return first.Before(dstTime) || first.Equal(dstTime)
case ">":
return first.After(dstTime)
case ">=":
return first.After(dstTime) || first.Equal(dstTime)
case "=":
return first.Equal(dstTime)
case "!=":
return !first.Equal(dstTime)
}
return
}
func compNum[T int64 | float64 | uint64](first, second T, op string) bool {
switch op {
case "<":
return first < second
case "<=":
return first <= second
case ">":
return first > second
case ">=":
return first >= second
case "=":
return first == second
case "!=":
return first != second
}
return false
}
func compBool(first, second bool, op string) bool {
return compNum(boo2int(first), boo2int(second), op)
}
func boo2int(a bool) int64 {
if a {
return 1
} else {
return 0
}
}
// get reflect value length
func calcLength(val any) int {
v := reflect.Indirect(reflect.ValueOf(val))
// (u)int use width.
switch v.Kind() {
case reflect.String:
return len([]rune(v.String()))
case reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return v.Len()
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return len(strconv.FormatInt(int64(v.Uint()), 10))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return len(strconv.FormatInt(v.Int(), 10))
case reflect.Float32, reflect.Float64:
return len(fmt.Sprint(v.Interface()))
}
// cannot get length
return -1
}
func getLength(a any, rune bool) (int, error) {
field := reflect.ValueOf(a)
//if !field.IsValid() || field.IsNil() {
// return 0, nil
//}
switch field.Kind() {
case reflect.String:
if rune {
return utf8.RuneCountInString(field.String()), nil
}
return field.Len(), nil
case reflect.Slice, reflect.Map, reflect.Array:
return field.Len(), nil
case reflect.Ptr:
if field.Type().Elem().Kind() == reflect.Array {
// 类型声明中的长度
return field.Type().Elem().Len(), nil
}
}
return 0, ErrBadType
}

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

@ -0,0 +1,14 @@
package rsp
import (
"github.com/labstack/echo/v4"
)
// HTTPErrorHandler 实现 echo.HTTPErrorHandler 函数
func HTTPErrorHandler(err error, c echo.Context) {
if c.Response().Committed {
c.Logger().Error(err)
} else if err = Fail(c, err); err != nil {
c.Logger().Error(err)
}
}

@ -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,60 +110,84 @@ 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:
// 根据报头响应不同的格式
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 != "" {
@ -165,27 +195,32 @@ func respond(c echo.Context, o *response) error {
}
}
return c.JSONP(o.code, DefaultJsonpCallback, m)
case echo.MIMEApplicationJSON:
return c.JSON(o.code, m)
case echo.MIMEApplicationXML, echo.MIMETextXML:
case "xml":
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:
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 {

@ -0,0 +1,246 @@
package v
import (
"fmt"
"strings"
)
var emptyErrors []*Error
func init() {
emptyErrors = make([]*Error, 0)
}
// Error 验证错误结构体
type Error struct {
error error
code string
format string
params map[string]any
field string
label string
value any
}
// ErrorOption 错误配置函数签名
type ErrorOption func(*Error)
func merge(options []ErrorOption, presets ...ErrorOption) []ErrorOption {
return append(presets, options...)
}
// NewError 创建错误实例
func NewError(code string, options ...ErrorOption) *Error {
e := Error{code: code}
for _, option := range options {
option(&e)
}
return &e
}
// ErrorFormat 设置错误格式化字符串
func ErrorFormat(format string) ErrorOption {
return func(e *Error) {
e.format = format
}
}
// ErrorParam 设置验证参数
func ErrorParam(key string, value any) ErrorOption {
return func(e *Error) {
if e.params == nil {
e.params = make(map[string]any)
}
e.params[key] = value
}
}
// ErrorCode 设置错误代码
func ErrorCode(code string) ErrorOption {
return func(e *Error) {
e.code = code
}
}
// Code 返回错误代码
func (e *Error) Code() string {
return e.code
}
// Format 返回错误格式化模板
func (e *Error) Format() string {
return e.format
}
// Params 返回错误格式化蚕食
func (e *Error) Params() map[string]any {
p := map[string]any{}
if e.params != nil {
for k, v := range e.params {
p[k] = v
}
}
return p
}
// Field 返回字段名
func (e *Error) Field() string {
return e.field
}
// Label 返回错误标签
func (e *Error) Label() string {
return e.label
}
// Value 返回用于验证的值
func (e *Error) Value() any {
return e.value
}
// String 实现 fmt.Stringer 接口,返回格式化后的字符串
func (e *Error) String() string {
message := e.format
params := e.Params()
params["label"] = e.label
params["value"] = e.value
//params["field"] = e.field
// 定义了消息或翻译函数
if t, found := translations[e.code]; found {
if message == "" {
message = t.message
}
if t.trans != nil {
return t.trans(message, params)
}
}
// 设置了默认翻译函数
if defaultTranslator != nil {
return defaultTranslator(message, params)
}
for key, value := range params {
message = strings.ReplaceAll(message, "{"+key+"}", fmt.Sprintf("%v", value))
}
return message
}
// Error 实现内置错误接口(优先使用内部错误)
func (e *Error) Error() string {
if e.error != nil {
return e.error.Error()
}
return e.String()
}
// Errors 错误集
type Errors struct {
errors []*Error
}
// IsEmpty 是否存在错误
func (e *Errors) IsEmpty() bool {
if e == nil || e.errors == nil {
return true
}
return len(e.errors) == 0
}
// Add 添加一个错误
func (e *Errors) Add(err error) {
if err == nil {
return
}
if e.errors == nil {
e.errors = make([]*Error, 0)
}
if ex, ok := err.(*Errors); ok {
if !ex.IsEmpty() {
e.errors = append(e.errors, ex.errors...)
}
} else if ex, ok := err.(*Error); ok && ex != nil {
e.errors = append(e.errors, ex)
} else {
e.errors = append(e.errors, &Error{error: err})
}
}
// First 返回第一个错误实例,如果不存在则返回 nil
func (e *Errors) First() *Error {
if e.IsEmpty() {
return nil
}
return e.errors[0]
}
// Get 获取指定标签的错误列表,如果不存在将返回 nil
func (e *Errors) Get(field string) []*Error {
if e.IsEmpty() {
return nil
}
errs := make([]*Error, 0)
for _, err := range e.errors {
if err.field == field {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errs
}
return nil
}
func (e *Errors) All() []*Error {
if e == nil {
return emptyErrors
}
return e.errors
}
// ToMap 根据错误标签分组
func (e *Errors) ToMap() map[string][]*Error {
if e == nil || e.IsEmpty() {
return nil
}
errs := map[string][]*Error{}
for _, err := range e.errors {
if _, ok := errs[err.field]; !ok {
errs[err.field] = []*Error{}
}
errs[err.field] = append(errs[err.field], err)
}
return errs
}
func (e *Errors) String() string {
errsMap := e.ToMap()
if errsMap == nil {
return ""
}
var errors []string
for _, errs := range errsMap {
buf := strings.Builder{}
for i, err := range errs {
if i > 0 {
buf.WriteString("\n")
}
buf.WriteString(err.String())
}
errors = append(errors, buf.String())
}
return strings.Join(errors, "\n")
}
func (e *Errors) Error() string {
return e.String()
}
func isBuiltinError(err error) bool {
if _, ok := err.(*Error); ok {
return true
}
if _, ok := err.(*Errors); ok {
return true
}
return false
}

@ -0,0 +1,52 @@
package v
type Matcher struct {
field string
label string
value any
branches []branch
fallback func(valuer *Valuer) error
compare func(a, b any) bool
}
type branch struct {
value any
handle func(valuer *Valuer) error
}
func Match(value any, field, label string) *Matcher {
return &Matcher{
field: field,
label: label,
value: value,
branches: []branch{},
fallback: nil,
compare: func(a, b any) bool { return a == b },
}
}
func (m *Matcher) Branch(value any, handle func(valuer *Valuer) error) *Matcher {
m.branches = append(m.branches, branch{value: value, handle: handle})
return m
}
func (m *Matcher) Fallback(handle func(valuer *Valuer) error) *Matcher {
m.fallback = handle
return m
}
func (m *Matcher) Validate() error {
for _, b := range m.branches {
if m.compare(m.value, b.value) {
valuer := Value(m.value, m.field, m.label)
return b.handle(valuer)
}
}
if m.fallback != nil {
valuer := Value(m.value, m.field, m.label)
return m.fallback(valuer)
}
return nil
}

@ -0,0 +1,144 @@
package v
import (
"fmt"
"reflect"
)
// Translator 翻译函数签名
type Translator func(message string, params map[string]any) string
var (
// 默认翻译函数
defaultTranslator Translator
// 预置的翻译信息
translations = map[string]struct {
message string
trans Translator
}{
"required": {message: "{label}为必填字段"},
"required_if": {message: "{label}为必填字段"},
"typeof": {trans: typeof},
"is_email": {message: "{label}不是有效的电子邮箱地址"},
"is_e164": {message: "{label}不是有效的 e.164 手机号码"},
"is_phone_number": {message: "{label}不是有效的手机号码"},
"is_url": {message: "{label}不是有效的链接"},
"is_url_encoded": {message: "{label}不是有效的链接"},
"is_base64_url": {message: "{label}不是有效的BASE64链接"},
"is_semver": {message: "{label}不是有效的语义化版本号"},
"is_jwt": {message: "{label}不是有效的权限令牌"},
"is_uuid": {message: "{label}不是有效的UUID字符串"},
"is_uuid3": {message: "{label}不是有效的V3版UUID字符串"},
"is_uuid4": {message: "{label}不是有效的V4版UUID字符串"},
"is_uuid5": {message: "{label}不是有效的V5版UUID字符串"},
"is_ulid": {message: "{label}不是有效的ULID字符串"},
"is_md4": {message: ""},
"is_md5": {message: ""},
"is_sha256": {message: ""},
"is_sha384": {message: ""},
"is_sha512": {message: ""},
"is_ascii": {message: "{label}只能包含ASCII字符"},
"is_alpha": {message: "{label}只能包含字母"},
"is_alphanumeric": {message: "{label}只能包含字母和数字"},
"is_alpha_unicode": {message: "{label}只能包含字母和Unicode字符"},
"is_alphanumeric_unicode": {message: "{label}只能包含字母数字和Unicode字符"},
"is_numeric": {message: "{label}必须是一个有效的数值"},
"is_number": {message: "{label}必须是一个有效的数字"},
"is_bool": {message: "{label}必须是一个有效的布尔值"},
"is_hexadecimal": {message: "{label}必须是一个有效的十六进制"},
"is_hexcolor": {message: "{label}必须是一个有效的十六进制颜色"},
"is_rgb": {message: "{label}必须是一个有效的RGB颜色"},
"is_rgba": {message: "{label}必须是一个有效的RGBA颜色"},
"is_hsl": {message: "{label}必须是一个有效的RGB颜色"},
"is_hsla": {message: "{label}必须是一个有效的HSLA颜色"},
"is_color": {message: "{label}必须是一个有效的颜色"},
"is_latitude": {message: "{label}必须包含有效的纬度坐标"},
"is_longitude": {message: "{label}必须包含有效的经度坐标"},
"is_json": {message: "{label}必须是一个JSON字符串"},
"is_base64": {message: "{label}必须是一个有效的Base64字符串"},
"is_html": {message: "{label}必须是一个有效的网页内容"},
"is_html_encoded": {message: "{label}必须是一个被转义的网页内容"},
"is_datetime": {message: "{label}的格式必须是{layout}"},
"is_timezone": {message: "{label}必须是一个有效的时区"},
"is_ipv4": {message: "{label}必须是一个有效的IPv4地址"},
"is_ipv6": {message: "{label}必须是一个有效的IPv6地址"},
"is_ip": {message: "{label}必须是一个有效的IP地址"},
"is_mac": {message: "{label}必须是一个有效的MAC地址"},
"is_file": {message: "{label}必须是一个有效的文件"},
"is_dir": {message: "{label}必须是一个有效的目录"},
"is_lower": {message: "{label}必须是小写字母"},
"is_upper": {message: "{label}必须是大写字母"},
"contains": {message: "{label}必须包含文本'{substr}'"},
"contains_any": {message: "{label}必须包含至少一个以下字符'{chars}'"},
"contains_rune": {message: "{label}必须包含字符'{rune}'"},
"excludes": {message: "{label}不能包含文本'{substr}'"},
"excludes_all": {message: "{label}不能包含以下任何字符'{chars}'"},
"excludes_rune": {message: "{label}不能包含'{rune}'"},
"ends_with": {message: "{label}必须以文本'{suffix}'结尾"},
"ends_not_with": {message: "{label}不能以文本'{suffix}'结尾"},
"starts_with": {message: "{label}必须以文本'{prefix}'开头"},
"starts_not_with": {message: "{label}不能以文本'{prefix}'开头"},
"one_of": {message: "{label}必须是[{items}]中的一个"},
"not_empty": {message: "{label}不能为空"},
"length": {message: "{label}长度必须是{length}"},
"min_length": {message: "{label}最小长度为{min}"},
"max_length": {message: "max_length"},
"length_between": {message: "{label}长度必须大于或等于{min}且小于或等于{max}"},
"greater_than": {message: "{label}必须大于{min}"},
"greater_equal_than": {message: "{label}必须大于或等于{min}"},
"equal": {message: "{label}必须等于{another}"},
"not_equal": {message: "{label}不能等于{another}"},
"less_equal_than": {message: "{label}必须小于或等于{max}"},
"less_than": {message: "{label}必须小于{max}"},
"between": {message: "{label}必须大于或等于{min}且小于或等于{max}"},
"not_between": {message: "{label}必须小于{min}或大于{max}"},
"some": {message: "{label}至少有一个子项通过验证"},
"every": {message: "{label}的所有子项必须通过验证"},
"entity_exists": {message: "{label}不存在"},
"entity_not_exists": {message: "{label}已经存在"},
"index_by": {message: "参数不完整"},
}
types = map[reflect.Kind]string{
reflect.Bool: "布尔值",
reflect.Int: "整数",
reflect.Int8: "整数",
reflect.Int16: "整数",
reflect.Int32: "整数",
reflect.Int64: "整数",
reflect.Uint: "正整数",
reflect.Uint8: "正整数",
reflect.Uint16: "正整数",
reflect.Uint32: "正整数",
reflect.Uint64: "正整数",
reflect.Uintptr: "正整数",
reflect.Float32: "浮点数",
reflect.Float64: "浮点数",
reflect.Complex64: "复数",
reflect.Complex128: "复数",
reflect.Array: "数组",
reflect.Map: "字典(Mapper)",
reflect.Slice: "切片(Slice)",
reflect.String: "字符串",
reflect.Struct: "结构体",
}
)
func typeof(_ string, params map[string]any) string {
kind := params["kind"].(reflect.Kind)
if msg, ok := types[kind]; ok {
return fmt.Sprintf("%s不是有效的%s", params["label"], msg)
} else {
return fmt.Sprintf("%s格式验证失败", params["label"])
}
}
// SetDefaultTranslator 设置默认翻译函数
func SetDefaultTranslator(translator Translator) {
defaultTranslator = translator
}
// DefaultTranslator 返回默认翻译函数
func DefaultTranslator() Translator {
return defaultTranslator
}

@ -0,0 +1,112 @@
package v
import (
"errors"
"sorbet/pkg/is"
"strings"
)
// Validatable 验证功能接口
type Validatable interface {
Validate() error
}
// Checker 功能验证函数签名
type Checker func() error
// Validate 实现 Validatable 接口
func (c Checker) Validate() error {
return c()
}
// Wrap 将值和值的验证函数包装成验证器
func Wrap(value any, check func(any) error) Checker {
return func() error {
return check(value)
}
}
// Every 每一项都要验证通过
func Every(validators ...Validatable) Checker {
return func() error {
for _, validator := range validators {
if err := validator.Validate(); err != nil {
return err
}
}
return nil
}
}
// Some 任意一项验证通过即可
func Some(validators ...Validatable) Checker {
return func() error {
errs := &Errors{}
var hasOk bool
for _, validator := range validators {
err := validator.Validate()
if err != nil {
errs.Add(err)
} else {
hasOk = true
}
}
if hasOk || errs.IsEmpty() {
return nil
}
buf := strings.Builder{}
buf.WriteString("一下错误至少满足一项:\n")
for _, line := range strings.Split(errs.Error(), "\n") {
buf.WriteString(" " + line + "\n")
}
return errors.New(strings.TrimSpace(buf.String()))
}
}
// Validate 执行多个验证器
func Validate(validations ...Validatable) error {
var errs Errors
for _, validation := range validations {
if validation == nil {
continue
}
if err := validation.Validate(); err != nil {
errs.Add(err)
}
}
if errs.IsEmpty() {
return nil
}
return &errs
}
// IndexBy 分组验证,只要其中一组验证通过就返回
func IndexBy(index *int, values [][]any, options ...ErrorOption) Checker {
return func() error {
for i, items := range values {
count := 0
for _, item := range items {
if is.Empty(item) {
break
}
count++
}
if count > 0 && count == len(items) {
*index = i
return nil
}
}
return NewError("index_by", options...)
}
}
// Map 通过 map 构建值验证器
func Map(data map[string]any) func(name, label string) *Valuer {
return func(name, label string) *Valuer {
if val, ok := data[name]; ok {
return Value(val, name, label)
} else {
return Value(nil, name, label)
}
}
}

@ -0,0 +1,696 @@
package v
import (
"fmt"
"reflect"
"sorbet/pkg/is"
"strings"
)
// Ruler 规则验证函数签名
type Ruler func(any) error
// Valuer 基本值验证器
type Valuer struct {
field string // 字段名称,如:username
label string // 数据标签,对应字段名,如:用户名
value any // 参与验证的值
requires []Checker // 空值验证器列表
rules []Ruler // 参与验证的规则列表
}
// Value 创建一条验证器
func Value(value any, field, label string) *Valuer {
return &Valuer{
field: field,
label: label,
value: value,
requires: []Checker{},
rules: []Ruler{},
}
}
// Validate 实现验证器接口
func (v *Valuer) Validate() error {
if is.Empty(v.value) {
for _, require := range v.requires {
if err := require(); err != nil {
return err
}
}
return nil
}
// simple nil
value := v.value
if value == nil {
return nil
} else if reflect.TypeOf(value).Kind() == reflect.Ptr {
rv := reflect.ValueOf(value)
if rv.IsNil() {
return nil
} else {
value = rv.Elem().Interface()
}
}
// call rules
for _, rule := range v.rules {
if err := rule(value); err != nil {
return err
}
}
return nil
}
func (v *Valuer) mistake(err error, options ...ErrorOption) *Error {
if m, ok := err.(*Error); ok {
return m
}
e := &Error{error: err}
e.field = v.field
e.label = v.label
e.value = v.value
for _, option := range options {
option(e)
}
return e
}
func (v *Valuer) newError(code string, options []ErrorOption) *Error {
e := NewError(code, options...)
e.label = v.label
e.value = v.value
e.field = v.field
return e
}
func (v *Valuer) addRule(rule Ruler) *Valuer {
v.rules = append(v.rules, rule)
return v
}
func (v *Valuer) simple(code string, check func(any) bool, options []ErrorOption) *Valuer {
return v.addRule(func(val any) error {
if check(val) {
return nil
}
return v.newError(code, options)
})
}
func (v *Valuer) string(code string, check func(string) bool, options []ErrorOption) *Valuer {
return v.simple(code, func(a any) bool { return check(toString(a)) }, options)
}
func (v *Valuer) Custom(code string, check func(val any) any, options ...ErrorOption) *Valuer {
v.rules = append(v.rules, func(val any) error {
if res := check(val); res == false {
return v.newError(code, options) // 验证失败
} else if res == true || res == nil {
return nil // 验证成功
} else if err, ok := res.(error); ok {
if isBuiltinError(err) {
return err
}
m := v.newError(code, options)
m.error = err
//if str := err.Error(); m.format != "" && str != "" {
// m.format = fmt.Sprintf("%s(%s)", m.format, str)
//}
return m
} else {
panic("must return a bool, a nil or a error pointer")
}
})
return v
}
// Required 值是否必须(值不为空)
func (v *Valuer) Required(options ...ErrorOption) *Valuer {
v.requires = append(v.requires, func() error {
return v.newError("required", options)
})
return v
}
// RequiredIf 满足条件必须
func (v *Valuer) RequiredIf(condition bool, options ...ErrorOption) *Valuer {
v.requires = append(v.requires, func() error {
if condition {
return v.newError("required_if", options)
}
return nil
})
return v
}
// RequiredWith 依赖其它值判断是否必须
func (v *Valuer) RequiredWith(values []any, options ...ErrorOption) *Valuer {
v.requires = append(v.requires, func() error {
for _, value := range values {
if !is.Empty(value) {
return v.newError("required_with", options)
}
}
return nil
})
return v
}
func (v *Valuer) When(condition bool, then func(*Valuer)) *Valuer {
if condition && then != nil {
v.addRule(func(a any) error {
x := Value(v.value, v.field, v.label)
then(x)
return x.Validate()
})
}
return v
}
func (v *Valuer) Match(handle func(m *Matcher)) *Valuer {
return v.addRule(func(a any) error {
m := Match(v.value, v.field, v.label)
handle(m)
return m.Validate()
})
}
func (v *Valuer) Typeof(kind reflect.Kind, options ...ErrorOption) *Valuer {
return v.addRule(func(val any) error {
if reflect.TypeOf(val).Kind() != kind {
options = merge(options, ErrorParam("kind", kind))
return v.newError("typeof", options)
}
return nil
})
}
func (v *Valuer) IsString(options ...ErrorOption) *Valuer {
options = append(options, ErrorCode("is_string"))
return v.Typeof(reflect.String, options...)
}
func (v *Valuer) IsEmail(options ...ErrorOption) *Valuer {
return v.string("is_email", is.Email, options)
}
func (v *Valuer) IsE164(options ...ErrorOption) *Valuer {
return v.string("is_e164", is.E164, options)
}
func (v *Valuer) IsPhoneNumber(options ...ErrorOption) *Valuer {
return v.string("is_phone_number", is.PhoneNumber, options)
}
func (v *Valuer) IsURL(options ...ErrorOption) *Valuer {
return v.string("is_url", is.URL, options)
}
func (v *Valuer) IsURLEncoded(options ...ErrorOption) *Valuer {
return v.string("is_url_encoded", is.URLEncoded, options)
}
func (v *Valuer) IsBase64URL(options ...ErrorOption) *Valuer {
return v.string("is_base64_url", is.Base64URL, options)
}
func (v *Valuer) IsSemver(options ...ErrorOption) *Valuer {
return v.string("is_semver", is.Semver, options)
}
func (v *Valuer) IsJwt(options ...ErrorOption) *Valuer {
return v.string("is_jwt", is.JWT, options)
}
func (v *Valuer) IsUUID(options ...ErrorOption) *Valuer {
return v.string("is_uuid", is.UUID, options)
}
func (v *Valuer) IsUUID5(options ...ErrorOption) *Valuer {
return v.string("is_uuid5", is.UUID5, options)
}
func (v *Valuer) IsUUID4(options ...ErrorOption) *Valuer {
return v.string("is_uuid4", is.UUID4, options)
}
func (v *Valuer) IsUUID3(options ...ErrorOption) *Valuer {
return v.string("is_uuid3", is.UUID3, options)
}
func (v *Valuer) IsULID(options ...ErrorOption) *Valuer {
return v.string("is_ulid", is.ULID, options)
}
func (v *Valuer) IsMD4(options ...ErrorOption) *Valuer {
return v.string("is_md4", is.MD4, options)
}
func (v *Valuer) IsMD5(options ...ErrorOption) *Valuer {
return v.string("is_md5", is.MD5, options)
}
func (v *Valuer) IsSHA256(options ...ErrorOption) *Valuer {
return v.string("is_sha256", is.SHA256, options)
}
func (v *Valuer) IsSHA384(options ...ErrorOption) *Valuer {
return v.string("is_sha384", is.SHA384, options)
}
func (v *Valuer) IsSHA512(options ...ErrorOption) *Valuer {
return v.string("is_sha512", is.SHA512, options)
}
func (v *Valuer) IsAscii(options ...ErrorOption) *Valuer {
return v.string("is_ascii", is.ASCII, options)
}
func (v *Valuer) IsAlpha(options ...ErrorOption) *Valuer {
return v.string("is_alpha", is.Alpha, options)
}
func (v *Valuer) IsAlphanumeric(options ...ErrorOption) *Valuer {
return v.string("is_alphanumeric", is.Alphanumeric, options)
}
func (v *Valuer) IsAlphaUnicode(options ...ErrorOption) *Valuer {
return v.string("is_alpha_unicode", is.AlphaUnicode, options)
}
func (v *Valuer) IsAlphanumericUnicode(options ...ErrorOption) *Valuer {
return v.string("is_alphanumeric_unicode", is.AlphanumericUnicode, options)
}
func (v *Valuer) IsNumeric(options ...ErrorOption) *Valuer {
return v.simple("is_numeric", is.Numeric[any], options)
}
func (v *Valuer) IsNumber(options ...ErrorOption) *Valuer {
return v.simple("is_number", is.Number[any], options)
}
func (v *Valuer) IsBool(options ...ErrorOption) *Valuer {
return v.simple("is_bool", is.Boolean[any], options)
}
func (v *Valuer) IsHexadecimal(options ...ErrorOption) *Valuer {
return v.string("is_hexadecimal", is.Hexadecimal, options)
}
func (v *Valuer) IsHexColor(options ...ErrorOption) *Valuer {
return v.string("is_hexcolor", is.HEXColor, options)
}
func (v *Valuer) IsRgb(options ...ErrorOption) *Valuer {
return v.string("is_rgb", is.RGB, options)
}
func (v *Valuer) IsRgba(options ...ErrorOption) *Valuer {
return v.string("is_rgba", is.RGBA, options)
}
func (v *Valuer) IsHsl(options ...ErrorOption) *Valuer {
return v.string("is_hsl", is.HSL, options)
}
func (v *Valuer) IsHsla(options ...ErrorOption) *Valuer {
return v.string("is_hsla", is.HSLA, options)
}
func (v *Valuer) IsColor(options ...ErrorOption) *Valuer {
return v.string("is_color", is.Color, options)
}
func (v *Valuer) IsLatitude(options ...ErrorOption) *Valuer {
return v.simple("is_latitude", is.Latitude[any], options)
}
func (v *Valuer) IsLongitude(options ...ErrorOption) *Valuer {
return v.simple("is_longitude", is.Longitude[any], options)
}
func (v *Valuer) IsJson(options ...ErrorOption) *Valuer {
return v.simple("is_json", is.JSON[any], options)
}
func (v *Valuer) IsBase64(options ...ErrorOption) *Valuer {
return v.string("is_base64", is.Base64, options)
}
func (v *Valuer) IsHTML(options ...ErrorOption) *Valuer {
return v.string("is_html", is.HTML, options)
}
func (v *Valuer) IsHTMLEncoded(options ...ErrorOption) *Valuer {
return v.string("is_html_encoded", is.HTMLEncoded, options)
}
func (v *Valuer) IsDatetime(layout string, options ...ErrorOption) *Valuer {
return v.simple(
"is_datetime",
func(a any) bool { return is.Datetime(toString(a), layout) },
append([]ErrorOption{ErrorParam("layout", layout)}, options...),
)
}
func (v *Valuer) IsTimezone(options ...ErrorOption) *Valuer {
return v.string("is_timezone", is.Timezone, options)
}
func (v *Valuer) IsIPv4(options ...ErrorOption) *Valuer {
return v.string("is_ipv4", is.IPv4, options)
}
func (v *Valuer) IsIPv6(options ...ErrorOption) *Valuer {
return v.string("is_ipv6", is.IPv6, options)
}
func (v *Valuer) IsIP(options ...ErrorOption) *Valuer {
return v.string("is_ip", is.IP, options)
}
func (v *Valuer) IsMAC(options ...ErrorOption) *Valuer {
return v.string("is_mac", is.MAC, options)
}
func (v *Valuer) IsFile(options ...ErrorOption) *Valuer {
return v.simple("is_file", is.File, options)
}
func (v *Valuer) IsDir(options ...ErrorOption) *Valuer {
return v.simple("is_file", is.Dir, options)
}
func (v *Valuer) IsLower(options ...ErrorOption) *Valuer {
return v.string("is_lower", is.Lowercase, options)
}
func (v *Valuer) IsUpper(options ...ErrorOption) *Valuer {
return v.string("is_upper", is.Uppercase, options)
}
func (v *Valuer) Contains(substr string, options ...ErrorOption) *Valuer {
return v.simple(
"contains",
func(a any) bool { return strings.Contains(toString(a), substr) },
merge(options, ErrorParam("substr", substr)),
)
}
func (v *Valuer) ContainsAny(chars string, options ...ErrorOption) *Valuer {
return v.simple(
"contains_any",
func(a any) bool { return strings.ContainsAny(toString(a), chars) },
merge(options, ErrorParam("chars", chars)),
)
}
func (v *Valuer) ContainsRune(rune rune, options ...ErrorOption) *Valuer {
return v.simple(
"contains_rune",
func(a any) bool { return strings.ContainsRune(toString(a), rune) },
merge(options, ErrorParam("rune", rune)),
)
}
func (v *Valuer) Excludes(substr string, options ...ErrorOption) *Valuer {
return v.simple(
"excludes",
func(val any) bool { return !strings.Contains(toString(val), substr) },
merge(options, ErrorParam("substr", substr)),
)
}
func (v *Valuer) ExcludesAll(chars string, options ...ErrorOption) *Valuer {
return v.simple(
"excludes_all",
func(val any) bool { return !strings.ContainsAny(toString(val), chars) },
merge(options, ErrorParam("chars", chars)),
)
}
func (v *Valuer) ExcludesRune(rune rune, options ...ErrorOption) *Valuer {
return v.simple(
"excludes_rune",
func(val any) bool { return !strings.ContainsRune(toString(val), rune) },
merge(options, ErrorParam("rune", rune)),
)
}
func (v *Valuer) EndsWith(suffix string, options ...ErrorOption) *Valuer {
return v.simple(
"ends_with",
func(a any) bool { return strings.HasSuffix(toString(a), suffix) },
merge(options, ErrorParam("suffix", suffix)),
)
}
func (v *Valuer) EndsNotWith(suffix string, options ...ErrorOption) *Valuer {
return v.simple(
"ends_not_with",
func(val any) bool { return !strings.HasSuffix(toString(val), suffix) },
merge(options, ErrorParam("suffix", suffix)),
)
}
func (v *Valuer) StartsWith(prefix string, options ...ErrorOption) *Valuer {
return v.simple(
"starts_with",
func(a any) bool { return strings.HasPrefix(toString(a), prefix) },
merge(options, ErrorParam("prefix", prefix)),
)
}
func (v *Valuer) StartsNotWith(prefix string, options ...ErrorOption) *Valuer {
return v.simple(
"starts_not_with",
func(val any) bool { return !strings.HasPrefix(toString(val), prefix) },
merge(options, ErrorParam("prefix", prefix)),
)
}
func (v *Valuer) OneOf(items []any, options ...ErrorOption) *Valuer {
return v.simple(
"one_of",
func(value any) bool { return is.OneOf(value, items) },
merge(options, ErrorParam("items", items)),
)
}
func (v *Valuer) NotEmpty(options ...ErrorOption) *Valuer {
return v.simple("not_empty", is.NotEmpty[any], options)
}
func (v *Valuer) Length(n int, options ...ErrorOption) *Valuer {
return v.simple(
"length",
func(a any) bool { return is.Length(a, n, "=") },
merge(options, ErrorParam("length", n)),
)
}
func (v *Valuer) MinLength(min int, options ...ErrorOption) *Valuer {
return v.simple(
"min_length",
func(a any) bool { return is.Length(a, min, ">=") },
merge(options, ErrorParam("min", min)),
)
}
func (v *Valuer) MaxLength(max int, options ...ErrorOption) *Valuer {
return v.simple(
"max_length",
func(a any) bool { return is.Length(a, max, "<=") },
merge(options, ErrorParam("max", max)),
)
}
func (v *Valuer) LengthBetween(min, max int, options ...ErrorOption) *Valuer {
return v.simple(
"length_between",
func(a any) bool { return is.LengthBetween(a, min, max) },
merge(options, ErrorParam("min", min), ErrorParam("max", max)),
)
}
func (v *Valuer) GreaterThan(min any, options ...ErrorOption) *Valuer {
return v.simple(
"greater_than",
func(a any) bool { return is.GreaterThan(a, min) },
merge(options, ErrorParam("min", min)),
)
}
func (v *Valuer) GreaterEqualThan(n any, options ...ErrorOption) *Valuer {
return v.simple(
"greater_equal_than",
func(a any) bool { return is.GreaterEqualThan(a, n) },
merge(options, ErrorParam("min", n)),
)
}
func (v *Valuer) Equal(another any, options ...ErrorOption) *Valuer {
return v.simple(
"equal",
func(a any) bool { return is.Equal(a, another) },
merge(options, ErrorParam("another", another)),
)
}
func (v *Valuer) NotEqual(another any, options ...ErrorOption) *Valuer {
return v.simple(
"not_equal",
func(a any) bool { return is.NotEqual(a, another) },
merge(options, ErrorParam("another", another)),
)
}
func (v *Valuer) LessEqualThan(max any, options ...ErrorOption) *Valuer {
return v.simple(
"less_equal_than",
func(a any) bool { return is.LessEqualThan(a, max) },
merge(options, ErrorParam("max", max)),
)
}
func (v *Valuer) LessThan(max any, options ...ErrorOption) *Valuer {
return v.simple(
"less_than",
func(a any) bool { return is.LessThan(a, max) },
merge(options, ErrorParam("max", max)),
)
}
func (v *Valuer) Between(min, max any, options ...ErrorOption) *Valuer {
return v.simple(
"between",
func(a any) bool { return is.Between(a, min, max) },
merge(options, ErrorParam("min", min), ErrorParam("max", max)),
)
}
func (v *Valuer) NotBetween(min, max any, options ...ErrorOption) *Valuer {
return v.simple(
"not_between",
func(a any) bool { return is.NotBetween(a, min, max) },
merge(options, ErrorParam("min", min), ErrorParam("max", max)),
)
}
type Item struct {
Key any
Index int
Value any
}
func (v *Valuer) itemize(handle func(item *Item) any, every bool, options []ErrorOption) *Valuer {
check := func(item *Item) (bool, error) {
res := handle(item)
if x, ok := res.(Validatable); ok {
if err := x.Validate(); res != nil {
return false, err
} else {
return !every, nil
}
}
if res == true || res == nil {
if !every {
// some 成功
return true, nil
}
return false, nil
}
if res == false && every {
if every {
// every 失败
return false, v.newError("every", options)
}
return false, nil
}
if err, ok := res.(error); ok {
// 自定义错误
return false, v.mistake(err, options...)
}
panic(fmt.Errorf("expect a bool, a Validatable or a error, got %+v", res))
}
v.addRule(func(a any) error {
rv := reflect.Indirect(reflect.ValueOf(a))
rt := rv.Type()
switch k := rt.Kind(); k {
case reflect.Array, reflect.Slice:
for i := 0; i < rv.Len(); i++ {
skip, err := check(&Item{
Key: nil,
Index: i,
Value: rv.Index(i).Interface(),
})
if err != nil {
return v.mistake(err)
}
if skip {
break
}
}
case reflect.Map:
iter := rv.MapRange()
for iter.Next() {
skip, err := check(&Item{
Key: iter.Key().String(),
Value: iter.Value().Interface(),
})
if err != nil {
return v.mistake(err)
}
if skip {
break
}
}
case reflect.Struct:
for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)
skip, err := check(&Item{
Key: field.Name,
Value: rv.FieldByName(field.Name).Interface(),
})
if err != nil {
return v.mistake(err)
}
if skip {
break
}
}
default:
panic("invalid value")
}
if every {
return nil
}
return v.newError("some", options)
})
return v
}
func (v *Valuer) Every(handle func(item *Item) any, options ...ErrorOption) *Valuer {
return v.itemize(handle, true, options)
}
func (v *Valuer) Some(handle func(item *Item) any, options ...ErrorOption) *Valuer {
return v.itemize(handle, false, options)
}
func toString(val any) string {
if str, ok := val.(string); ok {
return str
}
return fmt.Sprintf("%v", val)
}
Loading…
Cancel
Save