diff --git a/oap.go b/oap.go index 10e8f76..389f56c 100644 --- a/oap.go +++ b/oap.go @@ -10,6 +10,11 @@ import ( "gopkg.in/yaml.v3" ) +const ( + apolloTag = "apollo" + apolloNamespaceTag = "apollo_namespace" +) + type UnmarshalFunc func([]byte, interface{}) error var registryForUnmarshal = map[string]UnmarshalFunc{ @@ -25,32 +30,89 @@ func SetUnmarshalFunc(name string, f UnmarshalFunc) { func Decode(ptr interface{}, client agollo.Client, keyOpts map[string][]agollo.OpOption) error { v := reflect.ValueOf(ptr).Elem() + if v.Kind() != reflect.Struct { + return nil + } + + return decodeStruct(ptr, client, nil, keyOpts) +} + +func decodeStruct(ptr interface{}, client agollo.Client, opts []agollo.OpOption, keyOpts map[string][]agollo.OpOption) error { + v := reflect.ValueOf(ptr).Elem() + if v.Kind() != reflect.Struct { + return nil + } + for i := 0; i < v.NumField(); i++ { structField := v.Type().Field(i) - tag := structField.Tag - apolloRawKey := tag.Get("apollo") - apolloKeyParts := strings.Split(apolloRawKey, ",") - apolloKey := apolloKeyParts[0] - - apolloVal := client.GetString(apolloKey, keyOpts[apolloKey]...) - val := reflect.New(structField.Type) - - // 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) - } - - v.FieldByName(structField.Name).Set(val.Elem()) - } + field := v.FieldByName(structField.Name) + + if err := decode(ptr, structField, field, client, opts, keyOpts); err != nil { + return err } + } + + return nil +} + +func decode(ptr interface{}, structField reflect.StructField, field reflect.Value, client agollo.Client, opts []agollo.OpOption, keyOpts map[string][]agollo.OpOption) error { + tag := structField.Tag + apolloRawKey := tag.Get(apolloTag) + apolloKeyParts := strings.Split(apolloRawKey, ",") + apolloKey := apolloKeyParts[0] + + // OpOptions + kopts := keyOpts[apolloKey] + newOpts := make([]agollo.OpOption, len(opts)+len(kopts)) + + copy(newOpts, opts) + copy(newOpts[len(opts):], kopts) - if err := yaml.Unmarshal([]byte(apolloVal), val.Interface()); err != nil { - return fmt.Errorf("unmarshal %s error: %w", apolloVal, err) + // using namespace + if ns := tag.Get(apolloNamespaceTag); ns != "" { + newOpts = append(newOpts, agollo.WithNamespace(ns)) + } + + val := reflect.New(structField.Type) + + // nested struct fields + if apolloKey == "" { + if err := decodeStruct(val.Interface(), client, newOpts, keyOpts); err != nil { + return fmt.Errorf("Decode %s error: %w", structField.Name, err) + } + + if field.CanSet() { + field.Set(val.Elem()) } - v.FieldByName(structField.Name).Set(val.Elem()) + return nil + } + + // get config content + apolloVal := client.GetString(apolloKey, newOpts...) + + // 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) + } + + if field.CanSet() { + field.Set(val.Elem()) + } + + return nil + } + } + + // parse value via yaml + if err := yaml.Unmarshal([]byte(apolloVal), val.Interface()); err != nil { + return fmt.Errorf("unmarshal %s error: %w", apolloVal, err) + } + + if field.CanSet() { + field.Set(val.Elem()) } return nil diff --git a/oap_test.go b/oap_test.go index 8126a74..0d543f5 100644 --- a/oap_test.go +++ b/oap_test.go @@ -176,15 +176,80 @@ func TestDecode_StructSlice(t *testing.T) { Age int `yaml:"age"` } + t.Run("using yaml", func(t *testing.T) { + config := struct { + Users []user `apollo:"users,yaml"` + }{} + + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("users")).Return("- name: Alice\n age: 18") + + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) + + assert.Equal(t, []user{{Name: "Alice", Age: 18}}, config.Users) + }) + + t.Run("raw", func(t *testing.T) { + config := struct { + Users []user `apollo:"users"` + }{} + + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("users")).Return("- name: Alice\n age: 18") + + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) + + assert.Equal(t, []user{{Name: "Alice", Age: 18}}, config.Users) + }) +} + +func TestDecode_NonTag(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + config := struct { - Users []user `apollo:"users"` + Foo string + unexported interface{} }{} client := NewMockClient(ctrl) - client.EXPECT().GetString(gomock.Eq("users")).Return("- name: Alice\n age: 18") err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) require.NoError(t, err) +} + +func TestDecode_WithNamespace(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + config := struct { + Foo string `apollo:"foo" apollo_namespace:"ns"` + }{} - assert.Equal(t, []user{{Name: "Alice", Age: 18}}, config.Users) + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("foo"), gomock.Any()).Return("bar") + + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) +} + +func TestDecode_NestedWithNamespace(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + type FooConfig struct { + Foo string `apollo:"foo"` + } + + config := struct { + FooConfig `apollo_namespace:"ns"` + }{} + + client := NewMockClient(ctrl) + client.EXPECT().GetString(gomock.Eq("foo"), gomock.Any()).Return("bar") + + err := oap.Decode(&config, client, make(map[string][]agollo.OpOption)) + require.NoError(t, err) }