Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto validate config #4607

Merged
merged 1 commit into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions core/conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,11 @@ func Load(file string, v any, opts ...Option) error {
return loader([]byte(os.ExpandEnv(string(content))), v)
}

return loader(content, v)
if err = loader(content, v); err != nil {
return err
}

return validate(v)
}

// LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable.
Expand All @@ -85,7 +89,12 @@ func LoadFromJsonBytes(content []byte, v any) error {

lowerCaseKeyMap := toLowerCaseKeyMap(m, info)

return mapping.UnmarshalJsonMap(lowerCaseKeyMap, v, mapping.WithCanonicalKeyFunc(toLowerCase))
if err = mapping.UnmarshalJsonMap(lowerCaseKeyMap, v,
mapping.WithCanonicalKeyFunc(toLowerCase)); err != nil {
return err
}

return validate(v)
}

// LoadConfigFromJsonBytes loads config into v from content json bytes.
Expand Down
77 changes: 63 additions & 14 deletions core/conf/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package conf

import (
"errors"
"os"
"reflect"
"testing"
Expand Down Expand Up @@ -40,9 +41,8 @@ func TestConfigJson(t *testing.T) {
for _, test := range tests {
test := test
t.Run(test, func(t *testing.T) {
tmpfile, err := createTempFile(test, text)
tmpfile, err := createTempFile(t, test, text)
assert.Nil(t, err)
defer os.Remove(tmpfile)

var val struct {
A string `json:"a"`
Expand Down Expand Up @@ -82,9 +82,8 @@ c = "${FOO}"
d = "abcd!@#$112"
`
t.Setenv("FOO", "2")
tmpfile, err := createTempFile(".toml", text)
tmpfile, err := createTempFile(t, ".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)

var val struct {
A string `json:"a"`
Expand All @@ -105,9 +104,8 @@ b = 1
c = "FOO"
d = "abcd"
`
tmpfile, err := createTempFile(".toml", text)
tmpfile, err := createTempFile(t, ".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)

var val struct {
A string `json:"a"`
Expand All @@ -127,9 +125,8 @@ func TestConfigWithLower(t *testing.T) {
text := `a = "foo"
b = 1
`
tmpfile, err := createTempFile(".toml", text)
tmpfile, err := createTempFile(t, ".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)

var val struct {
A string `json:"a"`
Expand Down Expand Up @@ -207,9 +204,8 @@ c = "${FOO}"
d = "abcd!@#112"
`
t.Setenv("FOO", "2")
tmpfile, err := createTempFile(".toml", text)
tmpfile, err := createTempFile(t, ".toml", text)
assert.Nil(t, err)
defer os.Remove(tmpfile)

var val struct {
A string `json:"a"`
Expand Down Expand Up @@ -241,9 +237,8 @@ func TestConfigJsonEnv(t *testing.T) {
for _, test := range tests {
test := test
t.Run(test, func(t *testing.T) {
tmpfile, err := createTempFile(test, text)
tmpfile, err := createTempFile(t, test, text)
assert.Nil(t, err)
defer os.Remove(tmpfile)

var val struct {
A string `json:"a"`
Expand Down Expand Up @@ -1217,11 +1212,44 @@ Name = "bar"
})
}

func Test_LoadBadConfig(t *testing.T) {
type Config struct {
Name string `json:"name,options=foo|bar"`
}

file, err := createTempFile(t, ".json", `{"name": "baz"}`)
assert.NoError(t, err)

var c Config
err = Load(file, &c)
assert.Error(t, err)
}

func Test_getFullName(t *testing.T) {
assert.Equal(t, "a.b", getFullName("a", "b"))
assert.Equal(t, "a", getFullName("", "a"))
}

func TestValidate(t *testing.T) {
t.Run("normal config", func(t *testing.T) {
var c mockConfig
err := LoadFromJsonBytes([]byte(`{"val": "hello", "number": 8}`), &c)
assert.NoError(t, err)
})

t.Run("error no int", func(t *testing.T) {
var c mockConfig
err := LoadFromJsonBytes([]byte(`{"val": "hello"}`), &c)
assert.Error(t, err)
})

t.Run("error no string", func(t *testing.T) {
var c mockConfig
err := LoadFromJsonBytes([]byte(`{"number": 8}`), &c)
assert.Error(t, err)
})
}

func Test_buildFieldsInfo(t *testing.T) {
type ParentSt struct {
Name string
Expand Down Expand Up @@ -1311,13 +1339,13 @@ func Test_buildFieldsInfo(t *testing.T) {
}
}

func createTempFile(ext, text string) (string, error) {
func createTempFile(t *testing.T, ext, text string) (string, error) {
tmpFile, err := os.CreateTemp(os.TempDir(), hash.Md5Hex([]byte(text))+"*"+ext)
if err != nil {
return "", err
}

if err := os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil {
if err = os.WriteFile(tmpFile.Name(), []byte(text), os.ModeTemporary); err != nil {
return "", err
}

Expand All @@ -1326,5 +1354,26 @@ func createTempFile(ext, text string) (string, error) {
return "", err
}

t.Cleanup(func() {
_ = os.Remove(filename)
})

return filename, nil
}

type mockConfig struct {
Val string
Number int
}

func (m mockConfig) Validate() error {
if len(m.Val) == 0 {
return errors.New("val is empty")
}

if m.Number == 0 {
return errors.New("number is zero")
}

return nil
}
12 changes: 12 additions & 0 deletions core/conf/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package conf

import "github.com/zeromicro/go-zero/core/validation"

// validate validates the value if it implements the Validator interface.
func validate(v any) error {
if val, ok := v.(validation.Validator); ok {
return val.Validate()
}

return nil
}
81 changes: 81 additions & 0 deletions core/conf/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package conf

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
)

type mockType int

func (m mockType) Validate() error {
if m < 10 {
return errors.New("invalid value")
}

return nil
}

type anotherMockType int

func Test_validate(t *testing.T) {
tests := []struct {
name string
v any
wantErr bool
}{
{
name: "invalid",
v: mockType(5),
wantErr: true,
},
{
name: "valid",
v: mockType(10),
wantErr: false,
},
{
name: "not validator",
v: anotherMockType(5),
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validate(tt.v)
assert.Equal(t, tt.wantErr, err != nil)
})
}
}

type mockVal struct {
}

func (m mockVal) Validate() error {
return errors.New("invalid value")
}

func Test_validateValPtr(t *testing.T) {
tests := []struct {
name string
v any
wantErr bool
}{
{
name: "invalid",
v: mockVal{},
},
{
name: "invalid value",
v: &mockVal{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Error(t, validate(tt.v))
})
}
}
Loading