Skip to content

Commit

Permalink
feat: allow use label on struct tag for define
Browse files Browse the repository at this point in the history
  • Loading branch information
inhere committed Mar 2, 2022
1 parent 70c4e8b commit e5174a0
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 52 deletions.
53 changes: 33 additions & 20 deletions data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,12 @@ func (d *StructData) parseRulesFromTag(v *Validation) {
d.FilterTag = gOpt.FilterTag
}

fMap := make(map[string]string, 0)
fOutMap := make(map[string]string, 0)

vv := d.value
vt := d.valueTpy
recursiveFunc = func(vv reflect.Value, vt reflect.Type, preStrName string, parentIsAnonymous bool) {
// preStrName - the parent field name.
recursiveFunc = func(vv reflect.Value, vt reflect.Type, parentFName string, parentIsAnonymous bool) {
for i := 0; i < vt.NumField(); i++ {
fValue := removeValuePtr(vv).Field(i)
fv := vt.Field(i)
Expand All @@ -283,10 +284,10 @@ func (d *StructData) parseRulesFromTag(v *Validation) {
continue
}

if preStrName == "" {
if parentFName == "" {
d.fieldNames[name] = fieldAtTopStruct
} else {
name = preStrName + "." + name
name = parentFName + "." + name
if parentIsAnonymous {
d.fieldNames[name] = fieldAtAnonymous
} else {
Expand All @@ -306,26 +307,33 @@ func (d *StructData) parseRulesFromTag(v *Validation) {
v.FilterRule(name, fRule)
}

// load field translate name
// preferred to use LabelTag. eg: `label:"display name"`
// and then use FieldTag. eg: `json:"user_name"`
fName := ""
if gOpt.LabelTag != "" {
fName = fv.Tag.Get(gOpt.LabelTag)
}
if fName == "" && gOpt.FieldTag != "" {
fName = fv.Tag.Get(gOpt.FieldTag)
// load field output name by FieldTag. eg: `json:"user_name"`
outName := ""
if gOpt.FieldTag != "" {
outName = fv.Tag.Get(gOpt.FieldTag)
}

// add pre field display name to fName
if fName != "" {
if preStrName != "" {
if preFName, ok := fMap[preStrName]; ok {
fName = preFName + "." + fName
if outName != "" {
if parentFName != "" {
if pOutName, ok := fOutMap[parentFName]; ok {
outName = pOutName + "." + outName
}
}

fMap[name] = fName
fOutMap[name] = outName
}

// load field translate name
// preferred to use label tag name. eg: `label:"display name"`
// and then use field output name. eg: `json:"user_name"`
if gOpt.LabelTag != "" {
lName := fv.Tag.Get(gOpt.LabelTag)
if lName == "" {
lName = outName
}

v.trans.addLabelName(name, lName)
}

// load custom error messages.
Expand All @@ -343,6 +351,11 @@ func (d *StructData) parseRulesFromTag(v *Validation) {
continue
}

// feat: only collect sub-struct rule on current field has rule.
if vRule == "" && gOpt.CheckSubOnParentMarked {
continue
}

switch ft.Kind() {
case reflect.Struct:
recursiveFunc(fValue, ft, name, fv.Anonymous)
Expand Down Expand Up @@ -394,8 +407,8 @@ func (d *StructData) parseRulesFromTag(v *Validation) {

recursiveFunc(removeValuePtr(vv), vt, "", false)

if len(fMap) > 0 {
v.Trans().AddFieldMap(fMap)
if len(fOutMap) > 0 {
v.Trans().AddFieldMap(fOutMap)
}
}

Expand Down
54 changes: 54 additions & 0 deletions issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,60 @@ func TestIssue_92(t *testing.T) {
assert.True(t, ok)
}

// https://github.com/gookit/validate/issues/98
func TestIssue_98(t *testing.T) {
// MenuActionResource 菜单动作关联资源对象
type MenuActionResource struct {
ID uint64 `json:"id"` // 唯一标识
ActionID uint64 `json:"action_id"` // 菜单动作ID
Method string `json:"method" validate:"required"` // 资源请求方式(支持正则)
Path string `json:"path" validate:"required"` // 资源请求路径(支持/:id匹配)
}

// MenuActionResources 菜单动作关联资源管理列表
type MenuActionResources []*MenuActionResource

// MenuAction 菜单动作对象
type MenuAction struct {
Code string `json:"code" validate:"required"` // 动作编号
Name string `json:"name" validate:"required"` // 动作名称
Resources MenuActionResources `json:"resources,omitempty"` // 资源列表
}

// MenuActions 菜单动作管理列表
type MenuActions []*MenuAction
type MenuCreateRequest struct {
Name string `json:"name" validate:"required"`
Sequence int `json:"sequence"` // 排序值
Icon string `json:"icon"` // 菜单图标
Router string `json:"router"` // 访问路由
ParentID uint64 `json:"parent_id"` // 父级ID
IsShow int `json:"is_show" validate:"in:0,1"` // 是否显示(1:显示 0:隐藏)
Status int `json:"status" validate:"in:0,1"` // 状态(1:启用 0:禁用)
Memo string `json:"memo"` // 备注
MenuActions MenuActions `json:"actions,omitempty"`
}

req := &MenuCreateRequest{
Name: "users",
MenuActions: []*MenuAction{
{
Code: "code01",
Name: "name01",
Resources: []*MenuActionResource{
{},
},
},
},
}
v := validate.Struct(req)

ok := v.Validate()

dump.Println(v.Errors)
assert.True(t, ok)
}

// https://github.com/gookit/validate/issues/103
func TestIssue_103(t *testing.T) {
type Example struct {
Expand Down
54 changes: 31 additions & 23 deletions messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func BuiltinMessages() map[string]string {

// Translator definition
type Translator struct {
// field name for output as Errors key.
// the field output name, use for Errors key.
// format: {"field": "output name"}
fieldMap map[string]string
// the field translate name in message.
Expand Down Expand Up @@ -294,36 +294,40 @@ func (t *Translator) LabelMap() map[string]string {
return t.labelMap
}

// AddMessages data to translator
func (t *Translator) AddMessages(data map[string]string) {
for n, m := range data {
t.messages[n] = m
// AddFieldMap config field output name data.
// If you want to display in the field with the original field is not the same
func (t *Translator) AddFieldMap(fieldMap map[string]string) {
for name, outName := range fieldMap {
t.fieldMap[name] = outName
}
}

// AddFieldMap config field data.
// If you want to display in the field with the original field is not the same
func (t *Translator) AddFieldMap(labels map[string]string) {
t.AddLabelMap(labels)
func (t *Translator) addLabelName(field, labelName string) {
if labelName != "" {
t.labelMap[field] = labelName
}
}

// AddLabelMap config field translate data map.
// If you want to display in the field with the original field is not the same
func (t *Translator) AddLabelMap(fieldMap map[string]string) {
for name, showName := range fieldMap {
t.labelMap[name] = showName
for name, labelName := range fieldMap {
t.addLabelName(name, labelName)
}
}

// AddMessage to translator
func (t *Translator) AddMessage(key, msg string) {
t.messages[key] = msg
// HasField name in the t.fieldMap.
func (t *Translator) HasField(field string) bool {
_, ok := t.labelMap[field]
return ok
}

// HasField name in the t.labelMap.
// Deprecated
func (t *Translator) HasField(field string) bool {
return t.HasLabel(field)
// FieldName get in the t.fieldMap
func (t *Translator) FieldName(field string) string {
if trName, ok := t.fieldMap[field]; ok {
field = trName
}
return field
}

// HasLabel name in the t.labelMap
Expand All @@ -340,12 +344,16 @@ func (t *Translator) LabelName(field string) string {
return field
}

// FieldName get in the t.fieldMap
func (t *Translator) FieldName(field string) string {
if trName, ok := t.fieldMap[field]; ok {
field = trName
// AddMessages data to translator
func (t *Translator) AddMessages(data map[string]string) {
for n, m := range data {
t.messages[n] = m
}
return field
}

// AddMessage to translator
func (t *Translator) AddMessage(key, msg string) {
t.messages[key] = msg
}

// HasMessage key in the t.messages
Expand Down
66 changes: 66 additions & 0 deletions messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"testing"

"github.com/gookit/goutil/dump"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -132,4 +133,69 @@ func TestMessageOnStruct(t *testing.T) {
v = Struct(s3)
is.False(v.Validate())
is.Equal("出生日期有误", v.Errors.One())

s4 := &struct {
Name string `validate:"string" json:"name"`
BirthDay string `validate:"date|maxlen:20" json:"birth_day" message:"出生日期有误"`
}{
"tom",
"invalid",
}

v = Struct(s4)
is.False(v.Validate())
is.Equal("出生日期有误", v.Errors.One())
}

// with field tag: json
func TestMessageOnStruct_withFieldTag(t *testing.T) {
is := assert.New(t)
s1 := &struct {
Name string `validate:"string" json:"name"`
BirthDay string `validate:"date|maxlen:20" json:"birth_day" message:"出生日期有误"`
}{
"tom",
"invalid",
}

v := Struct(s1)
is.False(v.Validate())
is.Equal("出生日期有误", v.Errors.One())
}

func TestMessageOnStruct_withNested(t *testing.T) {
is := assert.New(t)
type subSt struct {
Tags []string `json:"tags"`
Key1 string
}

s1 := &struct {
Name string `validate:"string" json:"name"`
BirthDay string `validate:"date|maxlen:20" json:"birth_day" label:"birth day" message:"{field} 出生日期有误"`
SubSt subSt
}{
"tom",
"invalid",
subSt{
Key1: "abc",
},
}

v := Struct(s1)
tr := v.Trans()
dump.V(tr.FieldMap(), tr.LabelMap())
is.Contains(tr.FieldMap(), "BirthDay")
is.Contains(tr.FieldMap(), "SubSt.Tags")
is.Equal("birth_day", tr.FieldName("BirthDay"))
is.Equal("tags", tr.FieldName("SubSt.Tags"))

is.Contains(tr.LabelMap(), "BirthDay")
is.Contains(tr.LabelMap(), "SubSt.Tags")
is.Equal("birth day", tr.LabelName("BirthDay"))
is.Equal("tags", tr.LabelName("SubSt.Tags"))

is.False(v.Validate())
dump.V(v.Errors)
is.Equal("birth day 出生日期有误", v.Errors.One())
}
4 changes: 4 additions & 0 deletions rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ func (r *Rule) errorMessage(field, validator string, v *Validation) (msg string)
// v.StringRule("age", "required|int|min:12", "toInt")
func (v *Validation) StringRule(field, rule string, filterRule ...string) *Validation {
rule = strings.TrimSpace(rule)
if rule == "" {
return v
}

rules := stringSplit(strings.Trim(rule, "|:"), "|")
for _, validator := range rules {
validator = strings.Trim(validator, ":")
Expand Down
15 changes: 8 additions & 7 deletions validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,16 @@ func (ms MS) String() string {

// GlobalOption settings for validate
type GlobalOption struct {
// FilterTag name in the struct tags.
// FilterTag name in the struct tags. default: filter
FilterTag string
// ValidateTag in the struct tags.
// ValidateTag in the struct tags. default: validate
ValidateTag string
// FieldTag name in the struct tags.
// use for define output field name/translate fallback. default: json
// FieldTag the output field name in the struct tags.
// it as placeholder on error message.
// default: json
FieldTag string
// LabelTag display name in the struct tags.
// use for define field translate. default: label
// LabelTag the display name in the struct tags.
// use for define field translate name on error. default: label
LabelTag string
// MessageTag define error message for the field.
MessageTag string
Expand All @@ -71,7 +72,7 @@ type GlobalOption struct {
CheckDefault bool
// CheckZero Whether validate the default zero value. (intX,uintX: 0, string: "")
CheckZero bool
// ErrKeyFmt
// ErrKeyFmt config. TODO
//
// allow:
// - 0 use struct field name as key. (for compatible)
Expand Down
4 changes: 2 additions & 2 deletions validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ func TestStruct(t *testing.T) {
// test trans
v.Trans().AddMessage("custom", "message0")
is.True(v.Trans().HasMessage("custom"))
is.Contains(v.Trans().FieldMap(), "Name")
// is.Contains(v.Trans().FieldMap(), "Name")
is.Contains(v.Trans().LabelMap(), "Name")
is.Equal("Name", v.Trans().LabelName("Name"))
is.Equal("User Name", v.Trans().LabelName("Name"))

// validate
v.StopOnError = false
Expand Down

0 comments on commit e5174a0

Please sign in to comment.