From 45bc099bb60cbff33757cf0f7c0492786fb5ec95 Mon Sep 17 00:00:00 2001 From: hupeh Date: Thu, 12 Oct 2023 16:36:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/v/error.go | 246 +++++++++++++++ pkg/v/matcher.go | 52 ++++ pkg/v/translations.go | 144 +++++++++ pkg/v/v.go | 112 +++++++ pkg/v/valuer.go | 696 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1250 insertions(+) create mode 100644 pkg/v/error.go create mode 100644 pkg/v/matcher.go create mode 100644 pkg/v/translations.go create mode 100644 pkg/v/v.go create mode 100644 pkg/v/valuer.go diff --git a/pkg/v/error.go b/pkg/v/error.go new file mode 100644 index 0000000..ee08c38 --- /dev/null +++ b/pkg/v/error.go @@ -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 +} diff --git a/pkg/v/matcher.go b/pkg/v/matcher.go new file mode 100644 index 0000000..51d94d2 --- /dev/null +++ b/pkg/v/matcher.go @@ -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 +} diff --git a/pkg/v/translations.go b/pkg/v/translations.go new file mode 100644 index 0000000..e0da4e8 --- /dev/null +++ b/pkg/v/translations.go @@ -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 +} diff --git a/pkg/v/v.go b/pkg/v/v.go new file mode 100644 index 0000000..7ba5679 --- /dev/null +++ b/pkg/v/v.go @@ -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) + } + } +} diff --git a/pkg/v/valuer.go b/pkg/v/valuer.go new file mode 100644 index 0000000..a177365 --- /dev/null +++ b/pkg/v/valuer.go @@ -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) +}