aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--envcfg.go40
-rw-r--r--mapper.go86
-rw-r--r--nesting_test.go52
-rw-r--r--overwriting_test.go67
-rw-r--r--tagging_test.go70
-rw-r--r--typeconv_test.go71
6 files changed, 386 insertions, 0 deletions
diff --git a/envcfg.go b/envcfg.go
new file mode 100644
index 0000000..b91ea13
--- /dev/null
+++ b/envcfg.go
@@ -0,0 +1,40 @@
1// Package envcfg provides environment variable mapping to structs.
2//
3// Can be used to read configuration parameters from the environment.
4//
5// Fields for which environment variables can be found are overwritten, otherwise they are left to their previous
6// value.
7//
8// Can be used, for example, after gcfg to override settings provided in a configuration file.
9package envcfg
10
11import (
12 "errors"
13 "reflect"
14)
15
16const (
17 TAG = "env"
18 ABS_TAG = "absenv"
19 SEP = "_"
20)
21
22type node struct {
23 parent *node
24 value *reflect.Value
25 properties *reflect.StructField
26}
27
28var ErrInvalidConfigStruct = errors.New("invalid parameter: must map to a struct")
29
30func ReadInto(cfgStruct interface{}) (interface{}, []error) {
31 s := reflect.ValueOf(cfgStruct).Elem()
32
33 if s.Kind() != reflect.Struct {
34 return nil, []error{ErrInvalidConfigStruct}
35 }
36
37 _, errs := setStructFields(node{nil, &s, nil})
38
39 return cfgStruct, errs
40}
diff --git a/mapper.go b/mapper.go
new file mode 100644
index 0000000..ca6885b
--- /dev/null
+++ b/mapper.go
@@ -0,0 +1,86 @@
1package envcfg
2
3import (
4 "os"
5 "reflect"
6 "strconv"
7 "strings"
8)
9
10func getEnvName(n node) string {
11 name := n.properties.Tag.Get(TAG)
12 if name == "" {
13 name = n.properties.Name
14 }
15
16 abs, _ := strconv.ParseBool(n.properties.Tag.Get(ABS_TAG))
17 if !abs && n.parent != nil {
18 if n.parent.properties != nil {
19 parentName := getEnvName(*n.parent)
20 name = parentName + SEP + name
21 }
22 }
23
24 return strings.ToUpper(name)
25}
26
27func setValue(n node, v string) (node, error) {
28 switch n.value.Kind() {
29 case reflect.String:
30 n.value.SetString(v)
31
32 case reflect.Bool:
33 boolVal, err := strconv.ParseBool(v)
34 if err != nil {
35 return n, err
36 }
37 n.value.SetBool(boolVal)
38
39 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
40 intVal, err := strconv.ParseInt(v, 0, n.value.Type().Bits())
41 if err != nil {
42 return n, err
43 }
44 n.value.SetInt(intVal)
45
46 case reflect.Float32, reflect.Float64:
47 floatVal, err := strconv.ParseFloat(v, n.value.Type().Bits())
48 if err != nil {
49 return n, err
50 }
51 n.value.SetFloat(floatVal)
52 }
53
54 return n, nil
55}
56
57func setFieldValue(n node) (node, error) {
58 if n.value.Kind() == reflect.Struct {
59 setStructFields(n)
60 return n, nil
61 }
62
63 v := os.Getenv(getEnvName(n))
64 if v != "" {
65 return setValue(n, v)
66 }
67
68 return n, nil
69}
70
71func setStructFields(n node) (node, []error) {
72 t := n.value.Type()
73 errs := []error{}
74
75 for i := 0; i < n.value.NumField(); i++ {
76 v := n.value.Field(i)
77 p := t.Field(i)
78
79 _, err := setFieldValue(node{&n, &v, &p})
80 if err != nil {
81 errs = append(errs, err)
82 }
83 }
84
85 return n, errs
86}
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 @@
1package envcfg
2
3import (
4 "os"
5 "testing"
6)
7
8type nestedStruct struct {
9 SubStruct struct {
10 Field string
11 }
12}
13
14func TestNestedMapping(t *testing.T) {
15 const ENV_KEY = "SUBSTRUCT_FIELD"
16 const ENV_VAL = "Remember: testing is the future!"
17
18 os.Clearenv()
19 os.Setenv(ENV_KEY, ENV_VAL)
20
21 s := nestedStruct{}
22
23 ReadInto(&s)
24
25 if s.SubStruct.Field != ENV_VAL {
26 t.Errorf("expected '%s', got '%s'", ENV_VAL, s.SubStruct.Field)
27 }
28}
29
30type deeplyNestedStruct struct {
31 SubStruct struct {
32 SubSubStruct struct {
33 Field string
34 }
35 }
36}
37
38func TestDeeplyNestedMapping(t *testing.T) {
39 const ENV_KEY = "SUBSTRUCT_SUBSUBSTRUCT_FIELD"
40 const ENV_VAL = "Remember: testing is the future!"
41
42 os.Clearenv()
43 os.Setenv(ENV_KEY, ENV_VAL)
44
45 s := deeplyNestedStruct{}
46
47 ReadInto(&s)
48
49 if s.SubStruct.SubSubStruct.Field != ENV_VAL {
50 t.Errorf("expected '%s', got '%s'", ENV_VAL, s.SubStruct.SubSubStruct.Field)
51 }
52}
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 @@
1package envcfg
2
3import (
4 "os"
5 "testing"
6)
7
8func TestKeeping(t *testing.T) {
9 const ORIG_VAL = "Remember: testing is the future!"
10
11 os.Clearenv()
12
13 s := struct{ Field string }{ORIG_VAL}
14
15 ReadInto(&s)
16
17 if s.Field != ORIG_VAL {
18 t.Errorf("expected '%s', got '%s'", ORIG_VAL, s.Field)
19 }
20}
21
22func TestOverwriting(t *testing.T) {
23 const ENV_KEY = "FIELD"
24 const ENV_VAL = "Remember: testing is the future!"
25 const ORIG_VAL = "Testing is pointless!"
26
27 os.Clearenv()
28 os.Setenv(ENV_KEY, ENV_VAL)
29
30 s := struct{ Field string }{ORIG_VAL}
31
32 ReadInto(&s)
33
34 if s.Field != ENV_VAL {
35 t.Errorf("expected '%s', got '%s'", ENV_VAL, s.Field)
36 }
37}
38
39type superStruct struct {
40 SubStruct nestedFields
41}
42
43type nestedFields struct {
44 KeepMe string
45 OverwriteMe string
46}
47
48func TestMultiOverwriting(t *testing.T) {
49 const ENV_KEY = "SUBSTRUCT_OVERWRITEME"
50 const ENV_VAL = "Remember: testing is the future!"
51 const ORIG_VAL = "Testing is pointless!"
52
53 os.Clearenv()
54 os.Setenv(ENV_KEY, ENV_VAL)
55
56 s := superStruct{nestedFields{ORIG_VAL, ORIG_VAL}}
57
58 ReadInto(&s)
59
60 if s.SubStruct.KeepMe != ORIG_VAL {
61 t.Errorf("expected '%s', got '%s'", ORIG_VAL, s.SubStruct.KeepMe)
62 }
63
64 if s.SubStruct.OverwriteMe != ENV_VAL {
65 t.Errorf("expected '%s', got '%s'", ENV_VAL, s.SubStruct.OverwriteMe)
66 }
67}
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 @@
1package envcfg
2
3import (
4 "os"
5 "testing"
6)
7
8type taggedStruct struct {
9 Field string `env:"CUSTOM_POTATOE"`
10}
11
12func TestTaggedField(t *testing.T) {
13 const ENV_KEY = "CUSTOM_POTATOE"
14 const ENV_VAL = "Remember: testing is the future!"
15
16 os.Clearenv()
17 os.Setenv(ENV_KEY, ENV_VAL)
18
19 s := taggedStruct{}
20
21 ReadInto(&s)
22
23 if s.Field != ENV_VAL {
24 t.Errorf("expected '%s', got '%s'", ENV_VAL, s.Field)
25 }
26}
27
28type taggedSubStruct struct {
29 SubStruct struct {
30 Field string
31 } `env:"POTATOE"`
32}
33
34func TestTaggedSubStruct(t *testing.T) {
35 const ENV_KEY = "POTATOE_FIELD"
36 const ENV_VAL = "Remember: testing is the future!"
37
38 os.Clearenv()
39 os.Setenv(ENV_KEY, ENV_VAL)
40
41 s := taggedSubStruct{}
42
43 ReadInto(&s)
44
45 if s.SubStruct.Field != ENV_VAL {
46 t.Errorf("expected '%s', got '%s'", ENV_VAL, s.SubStruct.Field)
47 }
48}
49
50type absTaggedField struct {
51 SubStruct struct {
52 Field string `env:"POTATOE" absenv:"true"`
53 }
54}
55
56func TestAbsTaggedField(t *testing.T) {
57 const ENV_KEY = "POTATOE"
58 const ENV_VAL = "Remember: testing is the future!"
59
60 os.Clearenv()
61 os.Setenv(ENV_KEY, ENV_VAL)
62
63 s := absTaggedField{}
64
65 ReadInto(&s)
66
67 if s.SubStruct.Field != ENV_VAL {
68 t.Errorf("expected '%s', got '%s'", ENV_VAL, s.SubStruct.Field)
69 }
70}
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 @@
1package envcfg
2
3import (
4 "fmt"
5 "os"
6 "testing"
7)
8
9func TestStringMapping(t *testing.T) {
10 const ENV_KEY = "FIELD"
11 const ENV_VAL = "Remember: testing is the future!"
12
13 os.Clearenv()
14 os.Setenv(ENV_KEY, ENV_VAL)
15
16 s := struct{ Field string }{}
17
18 ReadInto(&s)
19
20 if s.Field != ENV_VAL {
21 t.Errorf("expected '%s', got '%s'", ENV_VAL, s.Field)
22 }
23}
24
25func TestBoolMapping(t *testing.T) {
26 const ENV_KEY = "FIELD"
27 const ENV_VAL = true
28
29 os.Clearenv()
30 os.Setenv(ENV_KEY, fmt.Sprintf("%t", ENV_VAL))
31
32 s := struct{ Field bool }{}
33
34 ReadInto(&s)
35
36 if s.Field != ENV_VAL {
37 t.Errorf("expected '%t', got '%t'", ENV_VAL, s.Field)
38 }
39}
40
41func TestIntMapping(t *testing.T) {
42 const ENV_KEY = "FIELD"
43 const ENV_VAL = 42
44
45 os.Clearenv()
46 os.Setenv(ENV_KEY, fmt.Sprintf("%d", ENV_VAL))
47
48 s := struct{ Field int }{}
49
50 ReadInto(&s)
51
52 if s.Field != ENV_VAL {
53 t.Errorf("expected '%d', got '%d'", ENV_VAL, s.Field)
54 }
55}
56
57func TestFloatMapping(t *testing.T) {
58 const ENV_KEY = "FIELD"
59 const ENV_VAL = 13.37
60
61 os.Clearenv()
62 os.Setenv(ENV_KEY, fmt.Sprintf("%f", ENV_VAL))
63
64 s := struct{ Field float32 }{}
65
66 ReadInto(&s)
67
68 if s.Field != ENV_VAL {
69 t.Errorf("expected '%f', got '%f'", ENV_VAL, s.Field)
70 }
71}