From cfd8366a20ddc2d890be8037f1de0f958e612bf1 Mon Sep 17 00:00:00 2001 From: Boris Ershov Date: Wed, 13 Mar 2019 01:23:34 +0700 Subject: [PATCH] Improved struct tags Added ability to set `default` and `required` tags for sub structs fields within arrays, slices and maps. --- README.md | 2 +- conf.go | 305 +++++++++++++++++++++++++++------------------- conf_json_test.go | 134 ++++++++++++-------- conf_yaml_test.go | 134 ++++++++++++-------- 4 files changed, 355 insertions(+), 220 deletions(-) diff --git a/README.md b/README.md index 70aead3..9819600 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ To describe configuration file structure you simply need to define the struct in - `conf`: defines custom name for an option - `conf_extraopts`: provides advanced settings for option. This tag may have the following values: - `required`: option with this tag is mandatory. If it is set, but corresponding option is not defined in the config file, it will cause an error. - - `default`: determines default value for the option. Can be set only for _Int*_, _Uint*_, _Bool_ and _String_ (not within the arrays, maps or slices) types. + - `default`: determines default value for the option. - **ENV variables as option values** You may specify the option value as `ENV:VARIABLE_NAME`. It will use the value of the relative environment variable (i.e. _VARIABLE_NAME_) as value for that option. diff --git a/conf.go b/conf.go index f952bb4..064f130 100644 --- a/conf.go +++ b/conf.go @@ -14,9 +14,6 @@ import ( "gopkg.in/yaml.v2" ) -// ConfigType is a loadable config type -type ConfigType int - // Available types for loadable config const ( ConfigTypeYAML = 0 @@ -34,6 +31,9 @@ const ( regexpEnv = "ENV:(.*)" ) +// ConfigType is a loadable config type +type ConfigType int + // Settings struct contains settings config load type Settings struct { @@ -49,13 +49,18 @@ type Settings struct { // UnknownDeny if true fails with an error if config file contains fields that no matching in the result interface UnknownDeny bool + + md mapstructure.Metadata +} + +type defaultValue struct { + value string + isSet bool } // Load reads config func Load(conf interface{}, s Settings) error { - var md mapstructure.Metadata - // Check `conf` is a pointer if reflect.TypeOf(conf).Kind() != reflect.Ptr { return fmt.Errorf("config load internal error: `conf` must be a pointer") @@ -81,15 +86,10 @@ func Load(conf interface{}, s Settings) error { return fmt.Errorf("config error: unknown config type") } - // Set options default values - if err := setDefaults(reflect.ValueOf(conf)); err != nil { - return fmt.Errorf("config error: %v", err) - } - config := &mapstructure.DecoderConfig{ WeaklyTypedInput: s.WeaklyTypes, - Metadata: &md, - DecodeHook: decodeFromString, + Metadata: &s.md, + DecodeHook: s.decodeFromString, Result: conf, TagName: tagConfName, } @@ -104,112 +104,104 @@ func Load(conf interface{}, s Settings) error { return fmt.Errorf("config error: %v", err) } - if err := checkUsedRequredOpts(reflect.ValueOf(conf), "", md.Keys); err != nil { + // Set options default values + if err := s.setDefaults(reflect.ValueOf(conf), "", defaultValue{"", false}); err != nil { return fmt.Errorf("config error: %v", err) } - if s.UnknownDeny == true && len(md.Unused) > 0 { - return fmt.Errorf("config error: unknown option '%s'", md.Unused[0]) - } - - return nil -} - -// decodeFromString decodes values from string to other types. -// Able to use field values in format `ENV:VARIABLE_NAME` to get values from ENV variables. -func decodeFromString(f reflect.Type, t reflect.Type, v interface{}) (interface{}, error) { - - var s string - - if f.Kind() != reflect.String { - return v, nil - } - - var r = regexp.MustCompile(regexpEnv) - - result := r.FindStringSubmatch(v.(string)) - - if result != nil { - s = os.Getenv(result[1]) - if s == "" { - return v, fmt.Errorf("empty ENV variable '%s'", result[1]) - } - } else { - s = v.(string) + if err := s.checkUsedRequredOpts(reflect.ValueOf(conf), ""); err != nil { + return fmt.Errorf("config error: %v", err) } - return convFromString(s, t) -} - -// convFromString converts string value to other type in accordance to `t` -func convFromString(s string, t reflect.Type) (interface{}, error) { - - switch t.Kind() { - case reflect.Bool: - return strconv.ParseBool(s) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return strconv.ParseInt(s, 0, t.Bits()) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return strconv.ParseUint(s, 0, t.Bits()) - case reflect.Float32: - return strconv.ParseFloat(s, 32) - case reflect.Float64: - return strconv.ParseFloat(s, 64) + if err := s.checkUnknownOpts(); err != nil { + return fmt.Errorf("config error: %v", err) } - return s, nil + return nil } // setDefaults sets the default values from tags. -// Only for _Int*_, _Uint*_, _Bool_ and _String_ (not within the arrays, maps or slices) types default values are available. -func setDefaults(val reflect.Value) error { +func (s *Settings) setDefaults(val reflect.Value, parentName string, dv defaultValue) error { // Check val is pointer if val.Kind() == reflect.Ptr { val = val.Elem() } - // Check val is struct - if val.Type().Kind() != reflect.Struct { - return fmt.Errorf("internal error, must be a pointer to struct") - } - // Check val is writable if val.CanSet() == false { return fmt.Errorf("internal error, object is not writable") } - for i := 0; i < val.NumField(); i++ { + switch val.Type().Kind() { + case reflect.Struct: + for i := 0; i < val.NumField(); i++ { + vf := val.Field(i) + tf := val.Type().Field(i) + + elName := parentName + if elName != "" { + elName = strings.Join([]string{elName, s.fieldNameNormalize(tf)}, ".") + } else { + elName = s.fieldNameNormalize(tf) + } + + v, isSet := s.tagValGet(tf.Tag.Get(tagConfExtraOptsName), tagConfDefaultName) - vf := val.Field(i) - tf := val.Type().Field(i) + if err := s.setDefaults(vf, elName, defaultValue{v, isSet}); err != nil { + return err + } + } + case reflect.Slice, reflect.Array: + for i := 0; i < val.Len(); i++ { + vf := val.Index(i) + + elName := fmt.Sprintf("%s[%d]", parentName, i) + + if err := s.setDefaults(vf, elName, defaultValue{"", false}); err != nil { + return err + } + } + case reflect.Map: + for _, k := range val.MapKeys() { + vf := val.MapIndex(k) + + // Create copy of element to make it writable + t := reflect.Indirect(reflect.New(vf.Type())) + t.Set(reflect.ValueOf(vf.Interface())) + + elName := fmt.Sprintf("%s[%s]", parentName, k) + + if err := s.setDefaults(t, elName, defaultValue{"", false}); err != nil { + return err + } + + val.SetMapIndex(k, t) + } + + default: - if s, ok := tagValGet(tf.Tag.Get(tagConfExtraOptsName), tagConfDefaultName); ok == true { + // If default value set for this element and this option not used in conf file, fill it with default value + if dv.isSet == true && s.optIsUsed(parentName, s.md.Keys) == false { - d, err := convFromString(s, tf.Type) + d, err := s.convFromString(dv.value, val.Type()) if err != nil { return err } - switch tf.Type.Kind() { + switch val.Type().Kind() { case reflect.Bool: - vf.SetBool(d.(bool)) + val.SetBool(d.(bool)) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - vf.SetInt(d.(int64)) + val.SetInt(d.(int64)) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - vf.SetUint(d.(uint64)) + val.SetUint(d.(uint64)) case reflect.Float32, reflect.Float64: - vf.SetFloat(d.(float64)) + val.SetFloat(d.(float64)) case reflect.String: - vf.SetString(d.(string)) + val.SetString(d.(string)) default: - return fmt.Errorf("internal error, default value not available for this field `%s`", tf.Name) - } - } - - if vf.Kind() == reflect.Struct { - if err := setDefaults(vf); err != nil { - return err + return fmt.Errorf("internal error, default value not available for this field type `%s`", parentName) } } } @@ -217,52 +209,54 @@ func setDefaults(val reflect.Value) error { return nil } -// fieldNameNormalize returns either name from tag if specified, or struct field name as is -func fieldNameNormalize(tf reflect.StructField) string { - - tag := tf.Tag.Get(tagConfName) - - s := tagValIndexGet(tag, 0) - if s != "" { - return s - } - - return tf.Name -} - // checkUsedRequredOpts checks that config file contains all requirement options -func checkUsedRequredOpts(val reflect.Value, parentName string, usedOpts []string) error { +func (s *Settings) checkUsedRequredOpts(val reflect.Value, parentName string) error { // Check val is pointer if val.Kind() == reflect.Ptr { val = val.Elem() } - // Check val is struct - if val.Type().Kind() != reflect.Struct { - return fmt.Errorf("must be a pointer to struct") - } - - for i := 0; i < val.NumField(); i++ { + switch val.Type().Kind() { + case reflect.Struct: + for i := 0; i < val.NumField(); i++ { + vf := val.Field(i) + tf := val.Type().Field(i) + + elName := parentName + if elName != "" { + elName = strings.Join([]string{elName, s.fieldNameNormalize(tf)}, ".") + } else { + elName = s.fieldNameNormalize(tf) + } - vf := val.Field(i) - tf := val.Type().Field(i) + tag := tf.Tag.Get(tagConfExtraOptsName) - s := parentName + if s.tagKeyCheck(tag, tagConfRequiredName) == true && s.optIsUsed(elName, s.md.Keys) == false { + return fmt.Errorf("required option '%s' is not specified", elName) + } - if s != "" { - s = strings.Join([]string{s, fieldNameNormalize(tf)}, ".") - } else { - s = fieldNameNormalize(tf) + if err := s.checkUsedRequredOpts(vf, elName); err != nil { + return err + } } + case reflect.Slice, reflect.Array: + for i := 0; i < val.Len(); i++ { + vf := val.Index(i) + + elName := fmt.Sprintf("%s[%d]", parentName, i) - tag := tf.Tag.Get(tagConfExtraOptsName) - if tagKeyCheck(tag, tagConfRequiredName) == true && optIsUsed(s, usedOpts) == false { - return fmt.Errorf("required option '%s' is not specified", s) + if err := s.checkUsedRequredOpts(vf, elName); err != nil { + return err + } } + case reflect.Map: + for _, k := range val.MapKeys() { + vf := val.MapIndex(k) - if vf.Kind() == reflect.Struct { - if err := checkUsedRequredOpts(vf, s, usedOpts); err != nil { + elName := fmt.Sprintf("%s[%s]", parentName, k) + + if err := s.checkUsedRequredOpts(vf, elName); err != nil { return err } } @@ -271,8 +265,73 @@ func checkUsedRequredOpts(val reflect.Value, parentName string, usedOpts []strin return nil } +func (s *Settings) checkUnknownOpts() error { + if s.UnknownDeny == true && len(s.md.Unused) > 0 { + return fmt.Errorf("unknown option '%s'", s.md.Unused[0]) + } + return nil +} + +// decodeFromString decodes values from string to other types. +// Able to use field values in format `ENV:VARIABLE_NAME` to get values from ENV variables. +func (s *Settings) decodeFromString(f reflect.Type, t reflect.Type, v interface{}) (interface{}, error) { + + var str string + + if f.Kind() != reflect.String { + return v, nil + } + + var r = regexp.MustCompile(regexpEnv) + + result := r.FindStringSubmatch(v.(string)) + + if result != nil { + str = os.Getenv(result[1]) + if str == "" { + return v, fmt.Errorf("empty ENV variable '%s'", result[1]) + } + } else { + str = v.(string) + } + + return s.convFromString(str, t) +} + +// convFromString converts string value to other type in accordance to `t` +func (s *Settings) convFromString(str string, t reflect.Type) (interface{}, error) { + + switch t.Kind() { + case reflect.Bool: + return strconv.ParseBool(str) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return strconv.ParseInt(str, 0, t.Bits()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return strconv.ParseUint(str, 0, t.Bits()) + case reflect.Float32: + return strconv.ParseFloat(str, 32) + case reflect.Float64: + return strconv.ParseFloat(str, 64) + } + + return str, nil +} + +// fieldNameNormalize returns either name from tag if specified, or struct field name as is +func (s *Settings) fieldNameNormalize(tf reflect.StructField) string { + + tag := tf.Tag.Get(tagConfName) + + str := s.tagValIndexGet(tag, 0) + if str != "" { + return str + } + + return tf.Name +} + // optIsUsed checks that string slice `usedOpts` contains `opt` -func optIsUsed(opt string, usedOpts []string) bool { +func (s *Settings) optIsUsed(opt string, usedOpts []string) bool { for _, v := range usedOpts { if v == opt { @@ -284,7 +343,7 @@ func optIsUsed(opt string, usedOpts []string) bool { } // tagPartsMakeMap prepairs map for tag pairs -func tagPartsMakeMap(tag string) map[string]string { +func (s *Settings) tagPartsMakeMap(tag string) map[string]string { tm := make(map[string]string) @@ -303,9 +362,9 @@ func tagPartsMakeMap(tag string) map[string]string { } // tagKeyCheck cheks that `tag` contains `key` -func tagKeyCheck(tag string, key string) bool { +func (s *Settings) tagKeyCheck(tag string, key string) bool { - tm := tagPartsMakeMap(tag) + tm := s.tagPartsMakeMap(tag) if _, ok := tm[key]; ok { return true @@ -315,16 +374,16 @@ func tagKeyCheck(tag string, key string) bool { } // tagValGet gets from `tag` value for `key` -func tagValGet(tag string, key string) (string, bool) { +func (s *Settings) tagValGet(tag string, key string) (string, bool) { - tm := tagPartsMakeMap(tag) + tm := s.tagPartsMakeMap(tag) v, ok := tm[key] return v, ok } // tagConfGetName gets raw value (without splitting by '=') from tag by index -func tagValIndexGet(tag string, i int) string { +func (s *Settings) tagValIndexGet(tag string, i int) string { p := strings.Split(tag, ",") diff --git a/conf_json_test.go b/conf_json_test.go index 35c5c21..19842b9 100644 --- a/conf_json_test.go +++ b/conf_json_test.go @@ -4,45 +4,50 @@ import ( "encoding/json" "io/ioutil" "os" - "strconv" "testing" ) const ( - testJSONTmpConfPath = "/tmp/nxs-go-conf_test_json.conf" - testJSONValName = "Test JSON Name" - testJSONValAge = 18 - testJSONValJobAddress = "Test JSON Address" - testJSONValJobNameEnvVar = "TEST_JSON_CONF_JOB_NAME" - testJSONValJobNameEnvVal = "Test JSON Job name" - testJSONValJobSalaryEnvVar = "TEST_JSON_CONF_JOB_SALARY" - testJSONValJobSalaryEnvVal = 1.2 + testJSONTmpConfPath = "/tmp/nxs-go-conf_test_json.conf" + testJSONValString = "Test String" + testJSONValString1 = "Test String1" + testJSONValString2 = "Test String2" + testJSONValString3 = "Test String3" + testJSONValInt = 123 + testJSONValMapKey1 = "map_key1" + testJSONValMapKey2 = "map_key2" + testJSONValMapKey3 = "map_key3" + testJSONValStringEnvVar = "TEST_JSON_CONF_STRING" ) type tConfJSONIn struct { - Name string `json:"name,omitempty"` - Age int `json:"age,omitempty"` - Job tConfJSONInJob `json:"job,omitempty"` - FavoriteDishes []string `json:"favorite_dishes"` + StringTest string `json:"string_test,omitempty"` + IntTest int `json:"int_test,omitempty"` + StructsTest StructJSONTest `json:"struct_test,omitempty"` + StructsSliceTest []StructJSONTest `json:"struct_slice_test,omitempty"` + StructsMapTest map[string]StructJSONTest `json:"struct_map_test,omitempty"` + StringsSliceTest []string `json:"strings_slice_test"` } -type tConfJSONInJob struct { - Name string `json:"name,omitempty"` - Address string `json:"address,omitempty"` - Salary string `json:"salary,omitempty"` +type StructJSONTest struct { + StringTest string `json:"string_test,omitempty"` } func TestJSONFormat(t *testing.T) { type tConfOut struct { - Name string `conf:"name" conf_extraopts:"required"` - Age int `conf_extraopts:"default=18"` - Job struct { - Name string `conf:"name" conf_extraopts:"required"` - Address string `conf:"address" conf_extraopts:"default=Test JSON Address"` - Salary float64 `conf:"salary" conf_extraopts:"default=1.1"` - } `conf:"job" conf_extraopts:"required"` - FavoriteDishes []string `conf:"favorite_dishes"` + StringTest string `conf:"string_test" conf_extraopts:"required"` + IntTest int `conf:"int_test" conf_extraopts:"default=18"` + StructsTest struct { + StringTest string `conf:"string_test" conf_extraopts:"required"` + } `conf:"struct_test" conf_extraopts:"required"` + StructsSliceTest []struct { + StringTest string `conf:"string_test" conf_extraopts:"default=Test String"` + } `conf:"struct_slice_test" conf_extraopts:"required"` + StructsMapTest map[string]struct { + StringTest string `conf:"string_test" conf_extraopts:"default=Test String"` + } `conf:"struct_map_test" conf_extraopts:"required"` + StringsSliceTest []string `conf:"strings_slice_test"` } var c tConfOut @@ -65,45 +70,79 @@ func TestJSONFormat(t *testing.T) { // Check loaded data // Check specified string data - if c.Name != testJSONValName { - t.Fatal("Incorrect loaded data: Name") + if c.StringTest != testJSONValString { + t.Fatal("Incorrect loaded data: StringTest") } // Check default int value - if c.Age != testJSONValAge { - t.Fatal("Incorrect loaded data: Age") + if c.IntTest != testJSONValInt { + t.Fatal("Incorrect loaded data: IntTest") } - // Check specified string ENV data - if c.Job.Name != testJSONValJobNameEnvVal { - t.Fatal("Incorrect loaded data: Job.Name") + // Check substruct field + if c.StructsTest.StringTest != testJSONValString { + t.Fatal("Incorrect loaded data: StructsTest.StringTest") } - // Check default string value - if c.Job.Address != testJSONValJobAddress { - t.Fatal("Incorrect loaded data: Job.Address") + // Check substructs slice size + if len(c.StructsSliceTest) != 3 { + t.Fatal("Incorrect loaded data: StructsSliceTest") } - // Check specified float ENV data - if c.Job.Salary != testJSONValJobSalaryEnvVal { - t.Fatal("Incorrect loaded data: Job.Salary") + // Check substruct map string field + if c.StructsMapTest[testJSONValMapKey1].StringTest != testJSONValString1 { + t.Fatal("Incorrect loaded data: StructsMapTest[map_key1].StringTest") } - // Check string slices - if len(c.FavoriteDishes) != 2 { - t.Fatal("Incorrect loaded data: FavoriteDishes") + // Check substruct map string field ENV data + if c.StructsMapTest[testJSONValMapKey2].StringTest != testJSONValString2 { + t.Fatal("Incorrect loaded data: StructsMapTest[map_key2].StringTest") + } + + // Check substruct map string field default data + if c.StructsMapTest[testJSONValMapKey3].StringTest != testJSONValString { + t.Fatal("Incorrect loaded data: StructsMapTest[map_key3].StringTest") + } + + // Check string slice size + if len(c.StringsSliceTest) != 3 { + t.Fatal("Incorrect loaded data: StringsSliceTest") } } func testPrepareJSONConfig(t *testing.T) { c := tConfJSONIn{ - Name: testJSONValName, - Job: tConfJSONInJob{ - Name: "ENV:" + testJSONValJobNameEnvVar, - Salary: "ENV:" + testJSONValJobSalaryEnvVar, + StringTest: testJSONValString, + IntTest: testJSONValInt, + StructsTest: StructJSONTest{ + StringTest: testJSONValString, + }, + StructsSliceTest: []StructJSONTest{ + { + StringTest: testJSONValString1, + }, + { + StringTest: testJSONValString2, + }, + { + StringTest: testJSONValString3, + }, + }, + StructsMapTest: map[string]StructJSONTest{ + testJSONValMapKey1: StructJSONTest{ + StringTest: testJSONValString1, + }, + testJSONValMapKey2: StructJSONTest{ + StringTest: "ENV:" + testJSONValStringEnvVar, + }, + testJSONValMapKey3: StructJSONTest{}, + }, + StringsSliceTest: []string{ + testJSONValString1, + testJSONValString2, + testJSONValString3, }, - FavoriteDishes: []string{"apples", "ice cream"}, } s, err := json.Marshal(&c) @@ -116,6 +155,5 @@ func testPrepareJSONConfig(t *testing.T) { } // Set ENV variables - os.Setenv(testJSONValJobNameEnvVar, testJSONValJobNameEnvVal) - os.Setenv(testJSONValJobSalaryEnvVar, strconv.FormatFloat(testJSONValJobSalaryEnvVal, 'f', 3, 64)) + os.Setenv(testJSONValStringEnvVar, testJSONValString2) } diff --git a/conf_yaml_test.go b/conf_yaml_test.go index e30635c..04114fe 100644 --- a/conf_yaml_test.go +++ b/conf_yaml_test.go @@ -3,47 +3,52 @@ package conf import ( "io/ioutil" "os" - "strconv" "testing" "gopkg.in/yaml.v2" ) const ( - testYAMLTmpConfPath = "/tmp/nxs-go-conf_test_yaml.conf" - testYAMLValName = "Test YAML Name" - testYAMLValAge = 19 - testYAMLValJobAddress = "Test YAML Address" - testYAMLValJobNameEnvVar = "TEST_YAML_CONF_JOB_NAME" - testYAMLValJobNameEnvVal = "Test YAML Job name" - testYAMLValJobSalaryEnvVar = "TEST_YAML_CONF_JOB_SALARY" - testYAMLValJobSalaryEnvVal = 1.4 + testYAMLTmpConfPath = "/tmp/nxs-go-conf_test_yaml.conf" + testYAMLValString = "Test String" + testYAMLValString1 = "Test String1" + testYAMLValString2 = "Test String2" + testYAMLValString3 = "Test String3" + testYAMLValInt = 123 + testYAMLValMapKey1 = "map_key1" + testYAMLValMapKey2 = "map_key2" + testYAMLValMapKey3 = "map_key3" + testYAMLValStringEnvVar = "TEST_YAML_CONF_STRING" ) type tConfYAMLIn struct { - Name string `yaml:"name,omitempty"` - Age int `yaml:"age,omitempty"` - Job tConfYAMLInJob `yaml:"job,omitempty"` - FavoriteDishes []string `yaml:"favorite_dishes"` + StringTest string `yaml:"string_test,omitempty"` + IntTest int `yaml:"int_test,omitempty"` + StructsTest StructYAMLTest `yaml:"struct_test,omitempty"` + StructsSliceTest []StructYAMLTest `yaml:"struct_slice_test,omitempty"` + StructsMapTest map[string]StructYAMLTest `yaml:"struct_map_test,omitempty"` + StringsSliceTest []string `yaml:"strings_slice_test"` } -type tConfYAMLInJob struct { - Name string `yaml:"name,omitempty"` - Address string `yaml:"address,omitempty"` - Salary string `yaml:"salary,omitempty"` +type StructYAMLTest struct { + StringTest string `yaml:"string_test,omitempty"` } func TestYAMLFormat(t *testing.T) { type tConfOut struct { - Name string `conf:"name" conf_extraopts:"required"` - Age int `conf_extraopts:"default=19"` - Job struct { - Name string `conf:"name" conf_extraopts:"required"` - Address string `conf:"address" conf_extraopts:"default=Test YAML Address"` - Salary float64 `conf:"salary" conf_extraopts:"default=1.3"` - } `conf:"job" conf_extraopts:"required"` - FavoriteDishes []string `conf:"favorite_dishes"` + StringTest string `conf:"string_test" conf_extraopts:"required"` + IntTest int `conf:"int_test" conf_extraopts:"default=18"` + StructsTest struct { + StringTest string `conf:"string_test" conf_extraopts:"required"` + } `conf:"struct_test" conf_extraopts:"required"` + StructsSliceTest []struct { + StringTest string `conf:"string_test" conf_extraopts:"default=Test String"` + } `conf:"struct_slice_test" conf_extraopts:"required"` + StructsMapTest map[string]struct { + StringTest string `conf:"string_test" conf_extraopts:"default=Test String"` + } `conf:"struct_map_test" conf_extraopts:"required"` + StringsSliceTest []string `conf:"strings_slice_test"` } var c tConfOut @@ -66,45 +71,79 @@ func TestYAMLFormat(t *testing.T) { // Check loaded data // Check specified string data - if c.Name != testYAMLValName { - t.Fatal("Incorrect loaded data: Name") + if c.StringTest != testYAMLValString { + t.Fatal("Incorrect loaded data: StringTest") } // Check default int value - if c.Age != testYAMLValAge { - t.Fatal("Incorrect loaded data: Age") + if c.IntTest != testYAMLValInt { + t.Fatal("Incorrect loaded data: IntTest") } - // Check specified string ENV data - if c.Job.Name != testYAMLValJobNameEnvVal { - t.Fatal("Incorrect loaded data: Job.Name") + // Check substruct field + if c.StructsTest.StringTest != testYAMLValString { + t.Fatal("Incorrect loaded data: StructsTest.StringTest") } - // Check default string value - if c.Job.Address != testYAMLValJobAddress { - t.Fatal("Incorrect loaded data: Job.Address") + // Check substructs slice size + if len(c.StructsSliceTest) != 3 { + t.Fatal("Incorrect loaded data: StructsSliceTest") } - // Check specified float ENV data - if c.Job.Salary != testYAMLValJobSalaryEnvVal { - t.Fatal("Incorrect loaded data: Job.Salary") + // Check substruct map string field + if c.StructsMapTest[testYAMLValMapKey1].StringTest != testYAMLValString1 { + t.Fatal("Incorrect loaded data: StructsMapTest[map_key1].StringTest") } - // Check string slices - if len(c.FavoriteDishes) != 2 { - t.Fatal("Incorrect loaded data: FavoriteDishes") + // Check substruct map string field ENV data + if c.StructsMapTest[testYAMLValMapKey2].StringTest != testYAMLValString2 { + t.Fatal("Incorrect loaded data: StructsMapTest[map_key2].StringTest") + } + + // Check substruct map string field default data + if c.StructsMapTest[testYAMLValMapKey3].StringTest != testYAMLValString { + t.Fatal("Incorrect loaded data: StructsMapTest[map_key3].StringTest") + } + + // Check string slice size + if len(c.StringsSliceTest) != 3 { + t.Fatal("Incorrect loaded data: StringsSliceTest") } } func testPrepareYAMLConfig(t *testing.T) { c := tConfYAMLIn{ - Name: testYAMLValName, - Job: tConfYAMLInJob{ - Name: "ENV:" + testYAMLValJobNameEnvVar, - Salary: "ENV:" + testYAMLValJobSalaryEnvVar, + StringTest: testYAMLValString, + IntTest: testYAMLValInt, + StructsTest: StructYAMLTest{ + StringTest: testYAMLValString, + }, + StructsSliceTest: []StructYAMLTest{ + { + StringTest: testYAMLValString1, + }, + { + StringTest: testYAMLValString2, + }, + { + StringTest: testYAMLValString3, + }, + }, + StructsMapTest: map[string]StructYAMLTest{ + testYAMLValMapKey1: StructYAMLTest{ + StringTest: testYAMLValString1, + }, + testYAMLValMapKey2: StructYAMLTest{ + StringTest: "ENV:" + testYAMLValStringEnvVar, + }, + testYAMLValMapKey3: StructYAMLTest{}, + }, + StringsSliceTest: []string{ + testYAMLValString1, + testYAMLValString2, + testYAMLValString3, }, - FavoriteDishes: []string{"apples", "ice cream"}, } s, err := yaml.Marshal(&c) @@ -117,6 +156,5 @@ func testPrepareYAMLConfig(t *testing.T) { } // Set ENV variables - os.Setenv(testYAMLValJobNameEnvVar, testYAMLValJobNameEnvVal) - os.Setenv(testYAMLValJobSalaryEnvVar, strconv.FormatFloat(testYAMLValJobSalaryEnvVal, 'f', 3, 64)) + os.Setenv(testYAMLValStringEnvVar, testYAMLValString2) }