Golangでの有名バリデーションライブラリにgo-playground/validatorというのがあります。ライセンスはMITで使いやすい。
このライブラリを使ってYAMLファイルのバリデーションを実装したのですが、そこそこ悩まされました。
本ライブラリはYAML等のバリデーションに特化したものではなく、汎用のバリデーションライブラリです。 結果として、根底的な思想はStructに対するバリデーションであり、「どこでエラーが発生したのか」はフィールド名で返却されます。
package validator import ( "testing" pgvalidator "github.com/go-playground/validator/v10" "github.com/stretchr/testify/assert" ) type Parent struct { Name string `yaml:"name" validate:"required"` Children []*Child `yaml:"children" validate:"dive,required"` } type Child struct { Name string `yaml:"child_name,fuga,piyo" validate:"required"` Age int `yaml:"child_age,fuga,piyo" validate:"required,gte=0"` } func TestField(t *testing.T) { p := &Parent{ Name: "Parent", Children: []*Child{ {Name: "Taro", Age: 10}, {Name: "Jiro", Age: -5}, }, } if err := pgvalidator.New().Struct(p); err != nil { if assert.Error(t, err) { for _, ve := range err.(pgvalidator.ValidationErrors) { // "child_age" ではなく "Age" で返却される assert.Equal(t, "Age", ve.Field()) assert.Equal(t, "Parent.Children[1].Age", ve.Namespace()) } } } }
Structのバリデーションであればそれで良いのですが、YAMLファイルのバリデーションであればyaml
タグから取得したいですよね。
この用途のためには、RegisterTagNameFuncを使います。この関数にはStructField
が渡ってきますので、そこからyaml
タグの内容を取り出せば良い。
v.RegisterTagNameFunc(func(f reflect.StructField) string { name := strings.SplitN(f.Tag.Get("yaml"), ",", 2)[0] if name == "-" { return "" } return name })
実際にこれを挟むと、以下のようなテストコードがパスするようになります。
func TestField(t *testing.T) { p := &Parent{ Name: "Parent", Children: []*Child{ {Name: "Taro", Age: 10}, {Name: "Jiro", Age: -5}, }, } v := pgvalidator.New() v.RegisterTagNameFunc(func(f reflect.StructField) string { name := strings.SplitN(f.Tag.Get("yaml"), ",", 2)[0] if name == "-" { return "" } return name }) if err := v.Struct(p); err != nil { if assert.Error(t, err) { for _, ve := range err.(pgvalidator.ValidationErrors) { assert.Equal(t, "child_age", ve.Field()) assert.Equal(t, "Parent.children[1].child_age", ve.Namespace()) } } } }
バリデーションエラーとなった項目は、YAMLファイルのルートからのパスで特定したいのでNamespace()
を使いたい。一方で気になるのは最後のve.Namespace()
でParent
というstruct名が返却されていることですね。
YAMLのルート要素となるStruct名には興味がありませんので、以下のようなコードで最初のStruct名を省きます。
field := strings.SplitN(ve.Namespace(), ".", 2)[1]
というわけで、最終的にはこんなコードを使うことにしました。
func TestField(t *testing.T) { p := &Parent{ Name: "Parent", Children: []*Child{ {Name: "Taro", Age: 10}, {Name: "Jiro", Age: -5}, }, } v := pgvalidator.New() v.RegisterTagNameFunc(func(f reflect.StructField) string { name := strings.SplitN(f.Tag.Get("yaml"), ",", 2)[0] if name == "-" { return "" } return name }) if err := v.Struct(p); err != nil { if assert.Error(t, err) { for _, ve := range err.(pgvalidator.ValidationErrors) { assert.Equal(t, "child_age", ve.Field()) assert.Equal(t, "children[1].child_age", strings.SplitN(ve.Namespace(), ".", 2)[1]) } } } }