From 8e99baedf2c3e433750c2f51681f3c9d15d412be Mon Sep 17 00:00:00 2001 From: Pacien TRAN-GIRARD Date: Sat, 14 Feb 2015 11:24:17 +0100 Subject: First version --- envcfg.go | 40 +++++++++++++++++++++++++ mapper.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++ nesting_test.go | 52 ++++++++++++++++++++++++++++++++ overwriting_test.go | 67 +++++++++++++++++++++++++++++++++++++++++ tagging_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++ typeconv_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 386 insertions(+) create mode 100644 envcfg.go create mode 100644 mapper.go create mode 100644 nesting_test.go create mode 100644 overwriting_test.go create mode 100644 tagging_test.go create mode 100644 typeconv_test.go diff --git a/envcfg.go b/envcfg.go new file mode 100644 index 0000000..b91ea13 --- /dev/null +++ b/envcfg.go @@ -0,0 +1,40 @@ +// Package envcfg provides environment variable mapping to structs. +// +// Can be used to read configuration parameters from the environment. +// +// Fields for which environment variables can be found are overwritten, otherwise they are left to their previous +// value. +// +// Can be used, for example, after gcfg to override settings provided in a configuration file. +package envcfg + +import ( + "errors" + "reflect" +) + +const ( + TAG = "env" + ABS_TAG = "absenv" + SEP = "_" +) + +type node struct { + parent *node + value *reflect.Value + properties *reflect.StructField +} + +var ErrInvalidConfigStruct = errors.New("invalid parameter: must map to a struct") + +func ReadInto(cfgStruct interface{}) (interface{}, []error) { + s := reflect.ValueOf(cfgStruct).Elem() + + if s.Kind() != reflect.Struct { + return nil, []error{ErrInvalidConfigStruct} + } + + _, errs := setStructFields(node{nil, &s, nil}) + + return cfgStruct, errs +} diff --git a/mapper.go b/mapper.go new file mode 100644 index 0000000..ca6885b --- /dev/null +++ b/mapper.go @@ -0,0 +1,86 @@ +package envcfg + +import ( + "os" + "reflect" + "strconv" + "strings" +) + +func getEnvName(n node) string { + name := n.properties.Tag.Get(TAG) + if name == "" { + name = n.properties.Name + } + + abs, _ := strconv.ParseBool(n.properties.Tag.Get(ABS_TAG)) + if !abs && n.parent != nil { + if n.parent.properties != nil { + parentName := getEnvName(*n.parent) + name = parentName + SEP + name + } + } + + return strings.ToUpper(name) +} + +func setValue(n node, v string) (node, error) { + switch n.value.Kind() { + case reflect.String: + n.value.SetString(v) + + case reflect.Bool: + boolVal, err := strconv.ParseBool(v) + if err != nil { + return n, err + } + n.value.SetBool(boolVal) + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + intVal, err := strconv.ParseInt(v, 0, n.value.Type().Bits()) + if err != nil { + return n, err + } + n.value.SetInt(intVal) + + case reflect.Float32, reflect.Float64: + floatVal, err := strconv.ParseFloat(v, n.value.Type().Bits()) + if err != nil { + return n, err + } + n.value.SetFloat(floatVal) + } + + return n, nil +} + +func setFieldValue(n node) (node, error) { + if n.value.Kind() == reflect.Struct { + setStructFields(n) + return n, nil + } + + v := os.Getenv(getEnvName(n)) + if v != "" { + return setValue(n, v) + } + + return n, nil +} + +func setStructFields(n node) (node, []error) { + t := n.value.Type() + errs := []error{} + + for i := 0; i < n.value.NumField(); i++ { + v := n.value.Field(i) + p := t.Field(i) + + _, err := setFieldValue(node{&n, &v, &p}) + if err != nil { + errs = append(errs, err) + } + } + + return n, errs +} diff --git a/nesting_test.go b/nesting_test.go new file mode 100644 index 0000000..3fcc059 --- /dev/null +++ b/nesting_test.go @@ -0,0 +1,52 @@ +package envcfg + +import ( + "os" + "testing" +) + +type nestedStruct struct { + SubStruct struct { + Field string + } +} + +func TestNestedMapping(t *testing.T) { + const ENV_KEY = "SUBSTRUCT_FIELD" + const ENV_VAL = "Remember: testing is the future!" + + os.Clearenv() + os.Setenv(ENV_KEY, ENV_VAL) + + s := nestedStruct{} + + ReadInto(&s) + + if s.SubStruct.Field != ENV_VAL { + t.Errorf("expected '%s', got '%s'", ENV_VAL, s.SubStruct.Field) + } +} + +type deeplyNestedStruct struct { + SubStruct struct { + SubSubStruct struct { + Field string + } + } +} + +func TestDeeplyNestedMapping(t *testing.T) { + const ENV_KEY = "SUBSTRUCT_SUBSUBSTRUCT_FIELD" + const ENV_VAL = "Remember: testing is the future!" + + os.Clearenv() + os.Setenv(ENV_KEY, ENV_VAL) + + s := deeplyNestedStruct{} + + ReadInto(&s) + + if s.SubStruct.SubSubStruct.Field != ENV_VAL { + t.Errorf("expected '%s', got '%s'", ENV_VAL, s.SubStruct.SubSubStruct.Field) + } +} diff --git a/overwriting_test.go b/overwriting_test.go new file mode 100644 index 0000000..1ed5daa --- /dev/null +++ b/overwriting_test.go @@ -0,0 +1,67 @@ +package envcfg + +import ( + "os" + "testing" +) + +func TestKeeping(t *testing.T) { + const ORIG_VAL = "Remember: testing is the future!" + + os.Clearenv() + + s := struct{ Field string }{ORIG_VAL} + + ReadInto(&s) + + if s.Field != ORIG_VAL { + t.Errorf("expected '%s', got '%s'", ORIG_VAL, s.Field) + } +} + +func TestOverwriting(t *testing.T) { + const ENV_KEY = "FIELD" + const ENV_VAL = "Remember: testing is the future!" + const ORIG_VAL = "Testing is pointless!" + + os.Clearenv() + os.Setenv(ENV_KEY, ENV_VAL) + + s := struct{ Field string }{ORIG_VAL} + + ReadInto(&s) + + if s.Field != ENV_VAL { + t.Errorf("expected '%s', got '%s'", ENV_VAL, s.Field) + } +} + +type superStruct struct { + SubStruct nestedFields +} + +type nestedFields struct { + KeepMe string + OverwriteMe string +} + +func TestMultiOverwriting(t *testing.T) { + const ENV_KEY = "SUBSTRUCT_OVERWRITEME" + const ENV_VAL = "Remember: testing is the future!" + const ORIG_VAL = "Testing is pointless!" + + os.Clearenv() + os.Setenv(ENV_KEY, ENV_VAL) + + s := superStruct{nestedFields{ORIG_VAL, ORIG_VAL}} + + ReadInto(&s) + + if s.SubStruct.KeepMe != ORIG_VAL { + t.Errorf("expected '%s', got '%s'", ORIG_VAL, s.SubStruct.KeepMe) + } + + if s.SubStruct.OverwriteMe != ENV_VAL { + t.Errorf("expected '%s', got '%s'", ENV_VAL, s.SubStruct.OverwriteMe) + } +} diff --git a/tagging_test.go b/tagging_test.go new file mode 100644 index 0000000..2fe2e33 --- /dev/null +++ b/tagging_test.go @@ -0,0 +1,70 @@ +package envcfg + +import ( + "os" + "testing" +) + +type taggedStruct struct { + Field string `env:"CUSTOM_POTATOE"` +} + +func TestTaggedField(t *testing.T) { + const ENV_KEY = "CUSTOM_POTATOE" + const ENV_VAL = "Remember: testing is the future!" + + os.Clearenv() + os.Setenv(ENV_KEY, ENV_VAL) + + s := taggedStruct{} + + ReadInto(&s) + + if s.Field != ENV_VAL { + t.Errorf("expected '%s', got '%s'", ENV_VAL, s.Field) + } +} + +type taggedSubStruct struct { + SubStruct struct { + Field string + } `env:"POTATOE"` +} + +func TestTaggedSubStruct(t *testing.T) { + const ENV_KEY = "POTATOE_FIELD" + const ENV_VAL = "Remember: testing is the future!" + + os.Clearenv() + os.Setenv(ENV_KEY, ENV_VAL) + + s := taggedSubStruct{} + + ReadInto(&s) + + if s.SubStruct.Field != ENV_VAL { + t.Errorf("expected '%s', got '%s'", ENV_VAL, s.SubStruct.Field) + } +} + +type absTaggedField struct { + SubStruct struct { + Field string `env:"POTATOE" absenv:"true"` + } +} + +func TestAbsTaggedField(t *testing.T) { + const ENV_KEY = "POTATOE" + const ENV_VAL = "Remember: testing is the future!" + + os.Clearenv() + os.Setenv(ENV_KEY, ENV_VAL) + + s := absTaggedField{} + + ReadInto(&s) + + if s.SubStruct.Field != ENV_VAL { + t.Errorf("expected '%s', got '%s'", ENV_VAL, s.SubStruct.Field) + } +} diff --git a/typeconv_test.go b/typeconv_test.go new file mode 100644 index 0000000..c851b5b --- /dev/null +++ b/typeconv_test.go @@ -0,0 +1,71 @@ +package envcfg + +import ( + "fmt" + "os" + "testing" +) + +func TestStringMapping(t *testing.T) { + const ENV_KEY = "FIELD" + const ENV_VAL = "Remember: testing is the future!" + + os.Clearenv() + os.Setenv(ENV_KEY, ENV_VAL) + + s := struct{ Field string }{} + + ReadInto(&s) + + if s.Field != ENV_VAL { + t.Errorf("expected '%s', got '%s'", ENV_VAL, s.Field) + } +} + +func TestBoolMapping(t *testing.T) { + const ENV_KEY = "FIELD" + const ENV_VAL = true + + os.Clearenv() + os.Setenv(ENV_KEY, fmt.Sprintf("%t", ENV_VAL)) + + s := struct{ Field bool }{} + + ReadInto(&s) + + if s.Field != ENV_VAL { + t.Errorf("expected '%t', got '%t'", ENV_VAL, s.Field) + } +} + +func TestIntMapping(t *testing.T) { + const ENV_KEY = "FIELD" + const ENV_VAL = 42 + + os.Clearenv() + os.Setenv(ENV_KEY, fmt.Sprintf("%d", ENV_VAL)) + + s := struct{ Field int }{} + + ReadInto(&s) + + if s.Field != ENV_VAL { + t.Errorf("expected '%d', got '%d'", ENV_VAL, s.Field) + } +} + +func TestFloatMapping(t *testing.T) { + const ENV_KEY = "FIELD" + const ENV_VAL = 13.37 + + os.Clearenv() + os.Setenv(ENV_KEY, fmt.Sprintf("%f", ENV_VAL)) + + s := struct{ Field float32 }{} + + ReadInto(&s) + + if s.Field != ENV_VAL { + t.Errorf("expected '%f', got '%f'", ENV_VAL, s.Field) + } +} -- cgit v1.2.3