diff --git a/oap.go b/oap.go index acbe25b..10e8f76 100644 --- a/oap.go +++ b/oap.go @@ -4,22 +4,18 @@ import ( "encoding/json" "fmt" "reflect" - "strconv" "strings" "github.com/philchia/agollo/v4" "gopkg.in/yaml.v3" ) -type ( - UnmarshalFunc = func([]byte, interface{}) error - KindHandler = func(rawtag string, expectFieldType reflect.Type, client agollo.Client, opts ...agollo.OpOption) (*reflect.Value, error) -) +type UnmarshalFunc func([]byte, interface{}) error -var ( - registryForUnmarshal = make(map[string]UnmarshalFunc) - registryForKindHandler = make(map[reflect.Kind]KindHandler) -) +var registryForUnmarshal = map[string]UnmarshalFunc{ + "json": json.Unmarshal, + "yaml": yaml.Unmarshal, +} // You can use custom unmarshal for strcut type filed. // Predfined JSON&YAML. @@ -27,135 +23,6 @@ func SetUnmarshalFunc(name string, f UnmarshalFunc) { registryForUnmarshal[name] = f } -// You can add your custom process func to relect.Kind type, or support oap not support type. -func SetKindHanlderFunc(kind reflect.Kind, f KindHandler) { - registryForKindHandler[kind] = f -} - -func GetUnmarshalFunc(name string) (UnmarshalFunc, bool) { - f, ok := registryForUnmarshal[name] - return f, ok -} - -func GetKindHanlderFunc(kind reflect.Kind) (KindHandler, bool) { - f, ok := registryForKindHandler[kind] - return f, ok -} - -func init() { - registryForUnmarshal["json"] = json.Unmarshal - registryForUnmarshal["yaml"] = yaml.Unmarshal - - registryForKindHandler[reflect.Bool] = boolHandler - registryForKindHandler[reflect.String] = stringHandler - registryForKindHandler[reflect.Int] = intHandler - registryForKindHandler[reflect.Float32] = float32Handler - registryForKindHandler[reflect.Float64] = float64Handler - registryForKindHandler[reflect.Struct] = structHandler - registryForKindHandler[reflect.Ptr] = structHandler -} - -func boolHandler(rawtag string, expectFieldType reflect.Type, client agollo.Client, opts ...agollo.OpOption) (*reflect.Value, error) { - confV := client.GetString(rawtag, opts...) - filedV := false - if strings.ToLower(confV) == "true" { - filedV = true - } - valueToSet := reflect.ValueOf(filedV) - return &valueToSet, nil -} - -func stringHandler(rawtag string, expectFieldType reflect.Type, client agollo.Client, opts ...agollo.OpOption) (*reflect.Value, error) { - confV := client.GetString(rawtag, opts...) - valueToSet := reflect.ValueOf(confV) - return &valueToSet, nil -} - -func float32Handler(rawtag string, expectFieldType reflect.Type, client agollo.Client, opts ...agollo.OpOption) (*reflect.Value, error) { - confV := client.GetString(rawtag, opts...) - - var filedV float32 - - float64V, err := strconv.ParseFloat(confV, 32) - if err != nil { - return nil, err - } - filedV = float32(float64V) - - valueToSet := reflect.ValueOf(filedV) - return &valueToSet, nil -} - -func float64Handler(rawtag string, expectFieldType reflect.Type, client agollo.Client, opts ...agollo.OpOption) (*reflect.Value, error) { - confV := client.GetString(rawtag, opts...) - - var filedV float64 - - float64V, err := strconv.ParseFloat(confV, 32) - if err != nil { - return nil, err - } - filedV = float64V - - valueToSet := reflect.ValueOf(filedV) - return &valueToSet, nil -} - -func structWithMarhsallHandler(rawtag string, expectFieldType reflect.Type, client agollo.Client, opts ...agollo.OpOption) (*reflect.Value, error) { - apolloKeyParts := strings.Split(rawtag, ",") - apolloKey := apolloKeyParts[0] - - confV := client.GetString(apolloKey, opts...) - - var unmarshalType string - if len(apolloKeyParts) == 2 { - unmarshalType = apolloKeyParts[1] - } - unmarshalFunc, ok := GetUnmarshalFunc(unmarshalType) - if !ok { - return nil, fmt.Errorf("unmarshalType=`%v` from rawtag=`%v` not suported yet", unmarshalType, rawtag) - } - v := reflect.New(expectFieldType) - newP := v.Interface() - if err := unmarshalFunc([]byte(confV), newP); err != nil { - return nil, err - } - valueToSet := reflect.Indirect(reflect.ValueOf(newP)) - return &valueToSet, nil -} - -func structWithEmptyKeyHandler(rawtag string, expectFieldType reflect.Type, client agollo.Client, opts ...agollo.OpOption) (*reflect.Value, error) { - v := reflect.New(expectFieldType) - newP := v.Interface() - if err := Decode(newP, client, make(map[string][]agollo.OpOption)); err != nil { - return nil, err - } - valueToSet := reflect.Indirect(reflect.ValueOf(newP)) - return &valueToSet, nil -} - -func structHandler(apolloKey string, expectFieldType reflect.Type, client agollo.Client, opts ...agollo.OpOption) (*reflect.Value, error) { - if apolloKey == "" { - return structWithEmptyKeyHandler(apolloKey, expectFieldType, client, opts...) - } - return structWithMarhsallHandler(apolloKey, expectFieldType, client, opts...) -} - -func intHandler(apolloKey string, expectFieldType reflect.Type, client agollo.Client, opts ...agollo.OpOption) (*reflect.Value, error) { - confV := client.GetString(apolloKey, opts...) - - var filedV int - - int64V, err := strconv.ParseInt(confV, 10, 64) - if err != nil { - return nil, err - } - filedV = int(int64V) - - valueToSet := reflect.ValueOf(filedV) - return &valueToSet, nil -} - func Decode(ptr interface{}, client agollo.Client, keyOpts map[string][]agollo.OpOption) error { v := reflect.ValueOf(ptr).Elem() for i := 0; i < v.NumField(); i++ { @@ -165,28 +32,26 @@ func Decode(ptr interface{}, client agollo.Client, keyOpts map[string][]agollo.O apolloKeyParts := strings.Split(apolloRawKey, ",") apolloKey := apolloKeyParts[0] - // Check struct field type if supported yet - filedTypeKind := structField.Type.Kind() - handler, ok := registryForKindHandler[filedTypeKind] - if !ok { - continue - } + apolloVal := client.GetString(apolloKey, keyOpts[apolloKey]...) + val := reflect.New(structField.Type) - var valueToSetPtr *reflect.Value - var valueToSetErr error + // use unmarshaller function + if len(apolloKeyParts) > 1 { + if unmarshallerFunc, ok := registryForUnmarshal[apolloKeyParts[1]]; ok { + if err := unmarshallerFunc([]byte(apolloVal), val.Interface()); err != nil { + return fmt.Errorf("%s unmarshal %s error: %w", apolloKeyParts[1], apolloKey, err) + } - // use opts if provieded - if opts, ok := keyOpts[apolloKey]; ok { - valueToSetPtr, valueToSetErr = handler(apolloRawKey, structField.Type, client, opts...) - } else { - valueToSetPtr, valueToSetErr = handler(apolloRawKey, structField.Type, client) + v.FieldByName(structField.Name).Set(val.Elem()) + } } - if valueToSetErr != nil { - return valueToSetErr + + if err := yaml.Unmarshal([]byte(apolloVal), val.Interface()); err != nil { + return fmt.Errorf("unmarshal %s error: %w", apolloVal, err) } - // get value from ptr - valueToSet := valueToSetPtr - v.FieldByName(structField.Name).Set(valueToSet.Convert(structField.Type)) + + v.FieldByName(structField.Name).Set(val.Elem()) } + return nil } diff --git a/oap_test.go b/oap_test.go index 1fb9325..8126a74 100644 --- a/oap_test.go +++ b/oap_test.go @@ -1,83 +1,190 @@ package oap_test import ( - "net/url" "testing" + "time" gomock "github.com/golang/mock/gomock" "github.com/philchia/agollo/v4" "github.com/ringsaturn/oap" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -type DemoConfig struct { - Foo string `apollo:"foo"` - Hello string `apollo:"hello"` - Float32Field float32 `apollo:"float32Field"` - Float64Field float64 `apollo:"float64Field"` - BoolField bool `apollo:"boolField"` - Substruct struct { - X string `json:"x"` - Y int `json:"y"` - } `apollo:"substruct,json"` - SubstructFromYAML struct { - X string `yaml:"x"` - Y int `yaml:"y"` - } `apollo:"substructFromYAML,yaml"` - SubstructWithInnerKeyDef struct { - X string `apollo:"SubstructWithInnerKeyDef.X"` - Y string `apollo:"SubstructWithInnerKeyDef.Y"` - URLField *url.URL `apollo:"SubstructWithInnerKeyDef.URL,url"` - } +func TestDecode_String(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + type CustomStringType string + + config := struct { + String string `apollo:"foo"` + CustomStringType CustomStringType `apollo:"bar"` + }{} + + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("foo")).Return("hello") + client.EXPECT().GetString(gomock.Eq("bar")).Return("hello") + + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) + + assert.Equal(t, "hello", config.String) + assert.Equal(t, CustomStringType("hello"), config.CustomStringType) +} + +func TestDecode_Int(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config := struct { + Int int `apollo:"int"` + Int8 int8 `apollo:"int8"` + Int16 int16 `apollo:"int16"` + Int32 int32 `apollo:"int32"` + Int64 int64 `apollo:"int64"` + Uint uint `apollo:"uint"` + Uint8 uint8 `apollo:"uint8"` + Uint16 uint16 `apollo:"uint16"` + Uint32 uint32 `apollo:"uint32"` + Uint64 uint64 `apollo:"uint64"` + Duration time.Duration `apollo:"duration"` + }{} + + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("int")).Return("1") + client.EXPECT().GetString(gomock.Eq("int8")).Return("1") + client.EXPECT().GetString(gomock.Eq("int16")).Return("1") + client.EXPECT().GetString(gomock.Eq("int32")).Return("1") + client.EXPECT().GetString(gomock.Eq("int64")).Return("1") + client.EXPECT().GetString(gomock.Eq("uint")).Return("1") + client.EXPECT().GetString(gomock.Eq("uint8")).Return("1") + client.EXPECT().GetString(gomock.Eq("uint16")).Return("1") + client.EXPECT().GetString(gomock.Eq("uint32")).Return("1") + client.EXPECT().GetString(gomock.Eq("uint64")).Return("1") + client.EXPECT().GetString(gomock.Eq("duration")).Return("1m") + + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) + + assert.Equal(t, 1, config.Int) + assert.Equal(t, int8(1), config.Int8) + assert.Equal(t, int16(1), config.Int16) + assert.Equal(t, int32(1), config.Int32) + assert.Equal(t, int64(1), config.Int64) + assert.Equal(t, uint(1), config.Uint) + assert.Equal(t, uint8(1), config.Uint8) + assert.Equal(t, uint16(1), config.Uint16) + assert.Equal(t, uint32(1), config.Uint32) + assert.Equal(t, uint64(1), config.Uint64) + assert.Equal(t, time.Minute, config.Duration) +} + +func TestDecode_Float(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config := struct { + Float32 float32 `apollo:"float32"` + Float64 float64 `apollo:"float64"` + }{} + + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("float32")).Return("1.1") + client.EXPECT().GetString(gomock.Eq("float64")).Return("1.1") + + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) + + assert.Equal(t, float32(1.1), config.Float32) + assert.Equal(t, float64(1.1), config.Float64) +} + +func TestDecode_Bool(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config := struct { + Bool bool `apollo:"bool"` + }{} + + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("bool")).Return("true") + + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) + + assert.Equal(t, true, config.Bool) } -var testJSONText string = `{"x": "123", "y": 0}` -var yamlText string = ` -x: "fffff" -y: 12313212 -` +func TestDecode_JSONStruct(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + type user struct { + Name string `json:"name"` + Age int `json:"age"` + } + + config := struct { + User user `apollo:"user"` + UserPtr *user `apollo:"user_ptr"` + }{} + + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("user")).Return(`{"name":"Alice","age":18}`) + client.EXPECT().GetString(gomock.Eq("user_ptr")).Return(`{"name":"Alice","age":18}`) + + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) -func TestDo(t *testing.T) { + assert.Equal(t, user{Name: "Alice", Age: 18}, config.User) + assert.Equal(t, &user{Name: "Alice", Age: 18}, config.UserPtr) +} + +func TestDecode_YAMLStruct(t *testing.T) { ctrl := gomock.NewController(t) + defer ctrl.Finish() + + type user struct { + Name string `yaml:"name"` + Age int `yaml:"age"` + } + + config := struct { + User user `apollo:"user"` + UserPtr *user `apollo:"user_ptr"` + }{} + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("user")).Return("name: Alice\nage: 18") + client.EXPECT().GetString(gomock.Eq("user_ptr")).Return("name: Alice\nage: 18") + + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) - client.EXPECT().GetString(gomock.Eq("foo")).Return("bar").MaxTimes(1) - client.EXPECT().GetString(gomock.Eq("hello")).Return("hello").MaxTimes(1) - client.EXPECT().GetString(gomock.Eq("float32Field")).Return("3.14").MaxTimes(1) - client.EXPECT().GetString(gomock.Eq("float64Field")).Return("3.14159265").MaxTimes(1) - client.EXPECT().GetString(gomock.Eq("boolField")).Return("true").MaxTimes(1) - client.EXPECT().GetString(gomock.Eq("substruct")).Return(testJSONText).MaxTimes(1) - client.EXPECT().GetString(gomock.Eq("substructFromYAML")).Return(yamlText).MaxTimes(1) - client.EXPECT().GetString(gomock.Eq("SubstructWithInnerKeyDef.X")).Return("balabala").MaxTimes(1) - client.EXPECT().GetString(gomock.Eq("SubstructWithInnerKeyDef.Y")).Return("habahaba").MaxTimes(1) - client.EXPECT().GetString(gomock.Eq("SubstructWithInnerKeyDef.URL")).Return("http://example.com").MaxTimes(1) - - oap.SetUnmarshalFunc("url", func(b []byte, i interface{}) error { - u, err := url.Parse(string(b)) - if err != nil { - return err - } - urlV := i.(**url.URL) - *urlV = &*u - return nil - }) - - conf := &DemoConfig{} - if err := oap.Decode(conf, client, make(map[string][]agollo.OpOption)); err != nil { - panic(err) + assert.Equal(t, user{Name: "Alice", Age: 18}, config.User) + assert.Equal(t, &user{Name: "Alice", Age: 18}, config.UserPtr) +} + +func TestDecode_StructSlice(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + type user struct { + Name string `yaml:"name"` + Age int `yaml:"age"` } - assert.Equal(t, "bar", conf.Foo) - assert.Equal(t, "hello", conf.Hello) - assert.Equal(t, float32(3.14), conf.Float32Field) - assert.InDelta(t, float64(3.14159265), conf.Float64Field, 0.0000001) - assert.Equal(t, true, conf.BoolField) - assert.Equal(t, "123", conf.Substruct.X) + config := struct { + Users []user `apollo:"users"` + }{} + + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("users")).Return("- name: Alice\n age: 18") - assert.Equal(t, "fffff", conf.SubstructFromYAML.X) - assert.Equal(t, 12313212, conf.SubstructFromYAML.Y) + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) - assert.Equal(t, "balabala", conf.SubstructWithInnerKeyDef.X) - assert.Equal(t, "habahaba", conf.SubstructWithInnerKeyDef.Y) - assert.Equal(t, "example.com", conf.SubstructWithInnerKeyDef.URLField.Host) + assert.Equal(t, []user{{Name: "Alice", Age: 18}}, config.Users) }