Skip to content

Commit

Permalink
removed dependency on assert and log, 0 dependencies now
Browse files Browse the repository at this point in the history
  • Loading branch information
ldemailly committed Nov 5, 2023
1 parent a3f8334 commit d7f01af
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 49 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
52 changes: 28 additions & 24 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -26,8 +26,6 @@ import (
"strconv"
"strings"
"unicode"

"fortio.org/log"
)

// Split strings into words, using CamelCase/camelCase/CAMELCase rules.
Expand Down Expand Up @@ -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++ {
Expand All @@ -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 == "" {
Expand All @@ -180,19 +181,19 @@ 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()
stringValue = SerializeValue(value)
}
envVars = append(envVars, KeyValue{Key: prefix + tag, Value: stringValue})
}
return envVars
return envVars, allErrors
}

func setPointer(fieldValue reflect.Value) reflect.Value {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand Down
33 changes: 17 additions & 16 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package struct2env_test

import (
"os"
"reflect"
"strings"
"testing"

"fortio.org/assert"
"fortio.org/log"
"fortio.org/struct2env"
)

Expand Down Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -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 '",
Expand All @@ -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)
}
Expand All @@ -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
Expand All @@ -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)
}
}
5 changes: 0 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
module fortio.org/struct2env

go 1.20

require (
fortio.org/assert v1.2.0
fortio.org/log v1.11.0
)
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=

0 comments on commit d7f01af

Please sign in to comment.