diff --git a/README.md b/README.md index 83caa57..ef6e7f8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Convert between go structures to environment variable and back (for structured c There are many go packages that are doing environment to go struct config (for instance https://github.com/kelseyhightower/envconfig) but I didn't find one doing the inverse and we needed to set a bunch of environment variables for shell and other tools to get some configuration structured as JSON and Go object, so this was born. For symetry the reverse was also added (history of commit on https://github.com/fortio/dflag/pull/50/commits) +Standalone package with 0 dependencies outside of the go standard library. + The unit test has a fairly extensive example on how ```go type FooConfig struct { diff --git a/env.go b/env.go index 1a8b30b..42f4697 100644 --- a/env.go +++ b/env.go @@ -16,7 +16,7 @@ // shell-compatible output for environment variable definitions. // // The package leverages reflection to dynamically handle arbitrary struct types, -// and logs its operations and errors using the 'fortio.org/log' package. +// and has 0 dependencies. package struct2env import ( @@ -26,8 +26,6 @@ import ( "strconv" "strings" "unicode" - - "fortio.org/log" ) // Split strings into words, using CamelCase/camelCase/CAMELCase rules. @@ -141,20 +139,23 @@ func SerializeValue(value interface{}) string { // If the field is exportable and the tag is missing we'll use the field name // converted to UPPER_SNAKE_CASE (using CamelCaseToUpperSnakeCase()) as the // environment variable name. -func StructToEnvVars(s interface{}) []KeyValue { - return structToEnvVars("", s) +func StructToEnvVars(s interface{}) ([]KeyValue, []error) { + var allErrors []error + var allKeyValVals []KeyValue + return structToEnvVars(allKeyValVals, allErrors, "", s) } -func structToEnvVars(prefix string, s interface{}) []KeyValue { - var envVars []KeyValue +// Appends additional results and errors to incoming envVars and allErrors and return them (for recursion). +func structToEnvVars(envVars []KeyValue, allErrors []error, prefix string, s interface{}) ([]KeyValue, []error) { v := reflect.ValueOf(s) // if we're passed a pointer to a struct instead of the struct, let that work too if v.Kind() == reflect.Ptr { v = v.Elem() } if v.Kind() != reflect.Struct { - log.Errf("Unexpected kind %v, expected a struct", v.Kind()) - return envVars + err := fmt.Errorf("unexpected kind %v, expected a struct", v.Kind()) + allErrors = append(allErrors, err) + return envVars, allErrors } t := v.Type() for i := 0; i < t.NumField(); i++ { @@ -165,7 +166,7 @@ func structToEnvVars(prefix string, s interface{}) []KeyValue { } if fieldType.Anonymous { // Recurse - envVars = append(envVars, structToEnvVars("", v.Field(i).Interface())...) + envVars, allErrors = structToEnvVars(envVars, allErrors, "", v.Field(i).Interface()) continue } if tag == "" { @@ -180,11 +181,11 @@ func structToEnvVars(prefix string, s interface{}) []KeyValue { stringValue = SerializeValue(fieldValue.Interface()) } case reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: - log.LogVf("Skipping field %s of type %v, not supported", fieldType.Name, fieldType.Type) + // log.LogVf("Skipping field %s of type %v, not supported", fieldType.Name, fieldType.Type) continue case reflect.Struct: // Recurse with prefix - envVars = append(envVars, structToEnvVars(tag+"_", fieldValue.Interface())...) + envVars, allErrors = structToEnvVars(envVars, allErrors, tag+"_", fieldValue.Interface()) continue default: value := fieldValue.Interface() @@ -192,7 +193,7 @@ func structToEnvVars(prefix string, s interface{}) []KeyValue { } envVars = append(envVars, KeyValue{Key: prefix + tag, Value: stringValue}) } - return envVars + return envVars, allErrors } func setPointer(fieldValue reflect.Value) reflect.Value { @@ -204,18 +205,18 @@ func setPointer(fieldValue reflect.Value) reflect.Value { return fieldValue.Elem() } -func checkEnv(envName, fieldName string, fieldValue reflect.Value) *string { +func checkEnv(envName, fieldName string, fieldValue reflect.Value) (*string, error) { val, found := os.LookupEnv(envName) if !found { - log.LogVf("%q not set for %s", envName, fieldName) - return nil + // log.LogVf("%q not set for %s", envName, fieldName) + return nil, nil //nolint:nilnil } - log.Infof("Found %s=%q to set %s", envName, val, fieldName) + // log.Infof("Found %s=%q to set %s", envName, val, fieldName) if !fieldValue.CanSet() { - log.Errf("Can't set %s (found %s=%q)", fieldName, envName, val) - return nil + err := fmt.Errorf("can't set %s (found %s=%q)", fieldName, envName, val) + return nil, err } - return &val + return &val, nil } func SetFromEnv(prefix string, s interface{}) []error { @@ -255,12 +256,16 @@ func setFromEnv(allErrors []error, prefix string, s interface{}) []error { if fieldValue.CanAddr() { // Check if we can get the address SetFromEnv(envName+"_", fieldValue.Addr().Interface()) } else { - log.Errf("Cannot take the address of %s to recurse", fieldType.Name) + err := fmt.Errorf("cannot take the address of %s to recurse", fieldType.Name) + allErrors = append(allErrors, err) } continue } - - val := checkEnv(envName, fieldType.Name, fieldValue) + val, err := checkEnv(envName, fieldType.Name, fieldValue) + if err != nil { + allErrors = append(allErrors, err) + continue + } if val == nil { continue } @@ -271,7 +276,6 @@ func setFromEnv(allErrors []error, prefix string, s interface{}) []error { kind = fieldValue.Type().Elem().Kind() fieldValue = setPointer(fieldValue) } - var err error switch kind { //nolint: exhaustive // we have default: for the other cases case reflect.String: fieldValue.SetString(envVal) diff --git a/env_test.go b/env_test.go index 917e7fb..f37c481 100644 --- a/env_test.go +++ b/env_test.go @@ -2,11 +2,10 @@ package struct2env_test import ( "os" + "reflect" "strings" "testing" - "fortio.org/assert" - "fortio.org/log" "fortio.org/struct2env" ) @@ -37,7 +36,9 @@ func TestSplitByCase(t *testing.T) { } for _, test := range tests { got := struct2env.SplitByCase(test.in) - assert.Equal(t, got, test.out, "mismatch for", test.in) + if !reflect.DeepEqual(got, test.out) { + t.Errorf("mismatch for %q: got %v expected %v", test.in, got, test.out) + } } } @@ -122,7 +123,6 @@ type FooConfig struct { } func TestStructToEnvVars(t *testing.T) { - log.SetLogLevelQuiet(log.Verbose) intV := 199 foo := FooConfig{ Foo: "a\nfoo with \" quotes and \\ and '", @@ -140,11 +140,17 @@ func TestStructToEnvVars(t *testing.T) { } foo.InnerA = "inner a" foo.InnerB = "inner b" - empty := struct2env.StructToEnvVars(42) // error/empty + empty, errors := struct2env.StructToEnvVars(42) // error/empty if len(empty) != 0 { t.Errorf("expected empty, got %v", empty) } - envVars := struct2env.StructToEnvVars(&foo) + if len(errors) != 1 { + t.Errorf("expected errors, got %v", errors) + } + envVars, errors := struct2env.StructToEnvVars(&foo) + if len(errors) != 0 { + t.Errorf("expected no error, got %v", errors) + } if len(envVars) != 11 { t.Errorf("expected 11 env vars, got %d: %+v", len(envVars), envVars) } @@ -169,7 +175,6 @@ export TST_FOO TST_BAR TST_A_SPECIAL_BLAH TST_A_BOOL TST_HTTP_SERVER TST_INT_POI } func TestSetFromEnv(t *testing.T) { - log.SetLogLevelQuiet(log.Verbose) foo := FooConfig{} envs := []struct { k string @@ -190,13 +195,9 @@ func TestSetFromEnv(t *testing.T) { if len(errors) != 0 { t.Errorf("Unexpectedly got errors :%v", errors) } - assert.Equal(t, foo.Foo, "another\nfoo") - assert.Equal(t, foo.Bar, "bar") - assert.Equal(t, foo.RecurseHere.InnerB, "in1") - assert.Equal(t, foo.Blah, 31) - assert.Equal(t, foo.ABool, true) - assert.NotEqual(t, foo.FloatPointer, nil) - assert.Equal(t, *foo.FloatPointer, 5.75) - assert.NotEqual(t, foo.IntPointer, nil) - assert.Equal(t, *foo.IntPointer, 73) + if foo.Foo != "another\nfoo" || foo.Bar != "bar" || foo.RecurseHere.InnerB != "in1" || foo.Blah != 31 || foo.ABool != true || + foo.FloatPointer == nil || *foo.FloatPointer != 5.75 || + foo.IntPointer == nil || *foo.IntPointer != 73 { + t.Errorf("Mismatch in object values, got: %+v", foo) + } } diff --git a/go.mod b/go.mod index e10f9b1..adfd05e 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,3 @@ module fortio.org/struct2env go 1.20 - -require ( - fortio.org/assert v1.2.0 - fortio.org/log v1.11.0 -) diff --git a/go.sum b/go.sum index 0791c9e..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +0,0 @@ -fortio.org/assert v1.2.0 h1:XscfvR8yp4xW7OMCvNbCsieRFDxlwdEcb69+JZRp6LA= -fortio.org/assert v1.2.0/go.mod h1:039mG+/iYDPO8Ibx8TrNuJCm2T2SuhwRI3uL9nHTTls= -fortio.org/log v1.11.0 h1:w7ueGPGbXz0A3+ApMz/5Q9gwEMrwSo/ohTlLo2Um6dU= -fortio.org/log v1.11.0/go.mod h1:u/8/2lyczXq52aT5Nw6reD+3cR6m/EbS2jBiIYhgiTU=