commit 347641156968f105f33ac4813a1f404a97af47d7 Author: Stanislav N. aka pztrn Date: Tue Sep 17 18:39:37 2019 +0500 Initial commit. diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..61d4ebc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "go.testFlags": [ + "-test.v", + "-cover" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d390350 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# SEC. Take a break and configure your application with ease. + +**SEC** stands for "Simple Environment Configuration" and provides really simple way to configure your application. + +After googling around Go applications configuration management packages that able to take parameters from environment configuration I came to a conclusion that there is none packages that able to do everything I want and yet have readable and testable source code. + +Key intentions to create SEC: + +* Parse configuration into structure with support of infinitely nested structures. +* Works properly with interfaces. +* No goto's. +* 100% code coverage. +* No external dependencies (only testify for tests). +* Readable code and proper variables naming. +* Debug mode + +This list might be updated if new key intention arrives :). + +SEC was written under impression from https://github.com/vrischmann/envconfig/. + +## Installation + +Go modules and dep are supported. Other package managers might or might not work, MRs are welcome! + +## Usage + +SEC is designed to be easy to use parser, so there is only one requirement - passed data should be a structure. You cannot do something like: + +```go +var Data string +sec.Parse(&Data, nil) +``` + +This will throw errors, as any type you'll pass, except for pointer to structure. + +SEC is unable to parse embedded unexported things except structures due to inability to get embedded field's address. Embed only structures, please. + +### Debug + +To get additional debug output set ``SEC_DEBUG`` environment variable to ``true``. If invalid boolean value will be passed it'll output error about that. + + Debug output uses standart log package. This may change in future. \ No newline at end of file diff --git a/compose_tree.go b/compose_tree.go new file mode 100644 index 0000000..d847134 --- /dev/null +++ b/compose_tree.go @@ -0,0 +1,91 @@ +package sec + +import ( + // stdlib + "reflect" + "strings" +) + +// Composes full tree for every structure member. +func composeTree(value reflect.Value, prefix string) { + typeOf := value.Type() + + // Compose prefix for everything below current field. + var curPrefix string + if prefix != "" { + curPrefix = prefix + if !strings.HasSuffix(curPrefix, "_") { + curPrefix += "_" + } + } + + for i := 0; i < value.NumField(); i++ { + fieldToProcess := value.Field(i) + fieldToProcessType := typeOf.Field(i) + + // If currently processed field - interface, then we should + // get underlying value. + //if fieldToProcess.Kind() == reflect.Interface { + // fieldToProcess = fieldToProcess.Elem() + //} + + // In 99% of cases we will get uninitialized things we should + // initialize. + switch fieldToProcess.Kind() { + case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan: + if fieldToProcess.IsNil() { + printDebug("Field '%s' is nil, initializing new one", fieldToProcessType.Name) + + // We should use only exported fields as unexported aren't + // settable using 'reflect' package. Can be possibly solved + // using unsafe pointers? + if fieldToProcess.CanSet() { + fieldToProcess.Set(reflect.New(fieldToProcess.Type().Elem())) + } else { + printDebug("Field '%s' is unexported and will be ignored", fieldToProcessType.Name) + continue + } + fieldToProcess = fieldToProcess.Elem() + } + } + + printDebug("Field: '%s', type: %s, (anonymous or embedded: %t)", fieldToProcessType.Name, fieldToProcess.Type().Kind().String(), fieldToProcessType.Anonymous) + + // Dealing with embedded things. + if fieldToProcessType.Anonymous { + // We should not allow anything other than struct. + if fieldToProcess.Kind() != reflect.Struct { + printDebug("Field is embedded, but not a struct (%s), which cannot be used", fieldToProcess.Kind().String()) + continue + } + } + + if fieldToProcess.Kind() != reflect.Struct && !fieldToProcess.CanSet() { + printDebug("Field '%s' can't be set, skipping", fieldToProcessType.Name) + continue + } + + printDebug("All underlying elements will have prefix '%s'", curPrefix) + + // Hello, I'm recursion and I'm here to make you happy. + // I'll be launched only for structures to get their fields. + if fieldToProcess.Kind() == reflect.Struct { + newElementPrefix := curPrefix + if !fieldToProcessType.Anonymous { + newElementPrefix = strings.ToUpper(newElementPrefix + typeOf.Field(i).Name) + } + composeTree(fieldToProcess, newElementPrefix) + } else { + f := &field{ + Name: typeOf.Field(i).Name, + EnvVar: curPrefix + strings.ToUpper(typeOf.Field(i).Name), + Pointer: fieldToProcess, + Kind: fieldToProcess.Kind(), + } + + parsedTree = append(parsedTree, f) + + printDebug("Field data constructed: %+v", f) + } + } +} diff --git a/field.go b/field.go new file mode 100644 index 0000000..bd54912 --- /dev/null +++ b/field.go @@ -0,0 +1,22 @@ +package sec + +import ( + // stdlib + "reflect" +) + +// This structure represents every parsable field that was found while +// reading passed structure. +type field struct { + // Name is a field name. Mostly for debugging purpose. + Name string + // Pointer is a pointer to field wrapped in reflect.Value. + Pointer reflect.Value + // EnvVar is a name of environment variable we will try to read. + EnvVar string + // Kind is a reflect.Kind value. + Kind reflect.Kind + + // Next variables are tag-related. + optional bool +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5270973 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gitlab.com/pztrn/sec + +go 1.13 + +require github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f25b296 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/options.go b/options.go new file mode 100644 index 0000000..c792ffb --- /dev/null +++ b/options.go @@ -0,0 +1,18 @@ +package sec + +// Options represents configuration for SEC. Note that this is parser +// configuration, per-field configuration should be defined in tags. +type Options struct { + // ErrorsAreCritical indicates that on every parsing error we should + // stop processing things. By default we will proceed even if errors + // will occur except critical errors like passing invalid value to + // SEC_DEBUG environment variable and passing not a pointer to + // structure to Parse() function. + ErrorsAreCritical bool +} + +var ( + defaultOptions = &Options{ + ErrorsAreCritical: false, + } +) diff --git a/parse_env.go b/parse_env.go new file mode 100644 index 0000000..6c0aae2 --- /dev/null +++ b/parse_env.go @@ -0,0 +1,169 @@ +package sec + +import ( + // stdlib + "errors" + "os" + "reflect" + "strconv" +) + +var ( + errNotBool = errors.New("environment variable doesn't contain boolean") + + errNotFloat = errors.New("environment variable doesn't contain floating point number") + errNotFloat32 = errors.New("environment variable doesn't contain float32") + errNotFloat64 = errors.New("environment variable doesn't contain float64") + + errNotInt = errors.New("environment variable doesn't contain integer") + errNotInt8 = errors.New("environment variable doesn't contain int8") + errNotInt16 = errors.New("environment variable doesn't contain int16") + errNotInt32 = errors.New("environment variable doesn't contain int32") + errNotInt64 = errors.New("environment variable doesn't contain int64") + + errNotUint = errors.New("environment variable doesn't contain unsigned integer") + errNotUint8 = errors.New("environment variable doesn't contain uint8") + errNotUint16 = errors.New("environment variable doesn't contain uint16") + errNotUint32 = errors.New("environment variable doesn't contain uint32") + errNotUint64 = errors.New("environment variable doesn't contain uint64") +) + +// Parses environment for data. +func parseEnv() error { + printDebug("Starting parsing data into tree from environment variables...") + + for _, element := range parsedTree { + printDebug("Processing element '%s'", element.EnvVar) + data, found := os.LookupEnv(element.EnvVar) + if !found { + printDebug("Value for '%s' environment variable wasn't found", element.EnvVar) + continue + } else { + printDebug("Value for '%s' will be: %s", element.EnvVar, data) + } + + switch element.Kind { + case reflect.String: + element.Pointer.SetString(data) + case reflect.Bool: + val, err := strconv.ParseBool(data) + if err != nil { + printDebug("Error occurred while parsing boolean: %s", err.Error()) + if options.ErrorsAreCritical { + return errNotBool + } + } + element.Pointer.SetBool(val) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + // Bitsize 64 here specified for a reason - actual ints + // ranges checking goes below and we should expect it to + // be 0 in case of configuration. + val, err := strconv.ParseInt(data, 10, 64) + if err != nil { + printDebug("Error occurred while parsing int: %s", err.Error()) + if options.ErrorsAreCritical { + return errNotInt + } + } + switch element.Kind { + case reflect.Int8: + // int8 is an integer in [-128...127] range. + if val >= -128 && val <= 127 { + element.Pointer.SetInt(val) + } else { + printDebug("Data in environment variable '%s' isn't int8", element.EnvVar) + element.Pointer.SetInt(0) + if options.ErrorsAreCritical { + return errNotInt8 + } + } + case reflect.Int16: + // int16 is an integer in [-32768...32767] range. + if val >= -32768 && val <= 32767 { + element.Pointer.SetInt(val) + } else { + printDebug("Data in environment variable '%s' isn't int16", element.EnvVar) + element.Pointer.SetInt(0) + if options.ErrorsAreCritical { + return errNotInt16 + } + } + case reflect.Int32: + // int32 is an integer in [-2147483648...2147483647] range. + if val >= -2147483648 && val <= 2147483647 { + element.Pointer.SetInt(val) + } else { + printDebug("Data in environment variable '%s' isn't int32", element.EnvVar) + element.Pointer.SetInt(0) + if options.ErrorsAreCritical { + return errNotInt32 + } + } + case reflect.Int64, reflect.Int: + // int64 is an integer in [-9223372036854775808...9223372036854775807] range. + // This is currently maximum allowed int values, so we'll + // just set it. + element.Pointer.SetInt(val) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + val, err := strconv.ParseUint(data, 10, 64) + if err != nil { + printDebug("Error occurred while parsing unsigned integer: %s", err.Error()) + if options.ErrorsAreCritical { + return errNotUint + } + } + switch element.Kind { + case reflect.Uint8: + // uint8 is an integer in [0...255] range. + if val <= 255 { + element.Pointer.SetUint(val) + } else { + printDebug("Data in environment variable '%s' isn't uint8", element.EnvVar) + element.Pointer.SetUint(0) + if options.ErrorsAreCritical { + return errNotUint8 + } + } + case reflect.Uint16: + // uint16 is an integer in [0...65535] range. + if val <= 65535 { + element.Pointer.SetUint(val) + } else { + printDebug("Data in environment variable '%s' isn't uint16", element.EnvVar) + element.Pointer.SetUint(0) + if options.ErrorsAreCritical { + return errNotUint16 + } + } + case reflect.Uint32: + // uint32 is an integer in [0...4294967295] range. + if val <= 4294967295 { + element.Pointer.SetUint(val) + } else { + printDebug("Data in environment variable '%s' isn't uint32", element.EnvVar) + element.Pointer.SetUint(0) + if options.ErrorsAreCritical { + return errNotUint32 + } + } + case reflect.Uint64: + // uint64 is an integer in [0...18446744073709551615] range. + // This is currently maximum allowed int values, so we'll + // just set it. + element.Pointer.SetUint(val) + } + case reflect.Float32, reflect.Float64: + val, err := strconv.ParseFloat(data, 64) + if err != nil { + printDebug("Error occurred while parsing float: %s", err.Error()) + if options.ErrorsAreCritical { + return errNotFloat + } + } + element.Pointer.SetFloat(val) + } + } + + return nil +} diff --git a/parse_env_test.go b/parse_env_test.go new file mode 100644 index 0000000..84b8155 --- /dev/null +++ b/parse_env_test.go @@ -0,0 +1,740 @@ +package sec + +import ( + // stdlib + "os" + "strconv" + "testing" + + // other + "github.com/stretchr/testify/require" +) + +func TestParseString(t *testing.T) { + type testStruct struct { + StringData string + } + + os.Setenv("TESTSTRUCT_STRINGDATA", "test") + + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) +} + +func TestParseBoolean(t *testing.T) { + testCases := []struct { + TestData string + ValidData bool + }{ + {"1", true}, + {"t", true}, + {"T", true}, + {"TRUE", true}, + {"true", true}, + {"True", true}, + {"0", false}, + {"f", false}, + {"F", false}, + {"FALSE", false}, + {"false", false}, + {"False", false}, + {"not a boolean", false}, + } + + type testStruct struct { + BoolData bool + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_BOOLDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.BoolData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotBoolError bool + _, err2 := strconv.ParseBool(testCase.TestData) + if err2 != nil { + checkNotBoolError = true + } + + if checkNotBoolError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + require.Equal(t, errNotBool, err1) + } + + os.Unsetenv("TESTSTRUCT_BOOLDATA") + } +} + +func TestParseInt8(t *testing.T) { + testCases := []struct { + TestData string + ValidData int8 + }{ + {"0", 0}, + {"1", 1}, + {"2", 2}, + {"-128", -128}, + {"127", 127}, + {"-129", 0}, + {"128", 0}, + {"not an integer", 0}, + } + + type testStruct struct { + IntData int8 + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_INTDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.IntData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotIntError bool + var checkRangeError bool + passedData, err2 := strconv.ParseInt(testCase.TestData, 10, 64) + if err2 != nil { + checkNotIntError = true + } + if passedData != int64(testCase.ValidData) { + checkRangeError = true + } + + if checkNotIntError || checkRangeError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + + if checkNotIntError { + require.Equal(t, errNotInt, err1) + } + if checkRangeError { + require.Equal(t, errNotInt8, err1) + } + } + + os.Unsetenv("TESTSTRUCT_INTDATA") + } +} + +func TestParseInt16(t *testing.T) { + testCases := []struct { + TestData string + ValidData int16 + }{ + {"0", 0}, + {"1", 1}, + {"2", 2}, + {"-32768", -32768}, + {"32767", 32767}, + {"-32770", 0}, + {"32770", 0}, + {"not an integer", 0}, + } + + type testStruct struct { + IntData int16 + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_INTDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.IntData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotIntError bool + var checkRangeError bool + passedData, err2 := strconv.ParseInt(testCase.TestData, 10, 64) + if err2 != nil { + checkNotIntError = true + } + if passedData != int64(testCase.ValidData) { + checkRangeError = true + } + + if checkNotIntError || checkRangeError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + + if checkNotIntError { + require.Equal(t, errNotInt, err1) + } + if checkRangeError { + require.Equal(t, errNotInt16, err1) + } + } + + os.Unsetenv("TESTSTRUCT_INTDATA") + } +} + +func TestParseInt32(t *testing.T) { + testCases := []struct { + TestData string + ValidData int32 + }{ + {"0", 0}, + {"1", 1}, + {"2", 2}, + {"-2147483648", -2147483648}, + {"2147483647", 2147483647}, + {"-2147483650", 0}, + {"2147483650", 0}, + {"not an integer", 0}, + } + + type testStruct struct { + IntData int32 + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_INTDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.IntData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotIntError bool + var checkRangeError bool + passedData, err2 := strconv.ParseInt(testCase.TestData, 10, 64) + if err2 != nil { + checkNotIntError = true + } + if passedData != int64(testCase.ValidData) { + checkRangeError = true + } + + if checkNotIntError || checkRangeError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + + if checkNotIntError { + require.Equal(t, errNotInt, err1) + } + if checkRangeError { + require.Equal(t, errNotInt32, err1) + } + } + + os.Unsetenv("TESTSTRUCT_INTDATA") + } +} + +func TestParseInt64(t *testing.T) { + testCases := []struct { + TestData string + ValidData int64 + }{ + {"0", 0}, + {"1", 1}, + {"2", 2}, + {"-9223372036854775808", -9223372036854775808}, + {"9223372036854775807", 9223372036854775807}, + {"-9223372036854775810", -9223372036854775808}, + {"9223372036854775810", 9223372036854775807}, + {"not an integer", 0}, + } + + type testStruct struct { + IntData int64 + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_INTDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.IntData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotIntError bool + var checkRangeError bool + passedData, err2 := strconv.ParseInt(testCase.TestData, 10, 64) + if err2 != nil { + checkNotIntError = true + } + if passedData != testCase.ValidData { + checkRangeError = true + } + + if checkNotIntError || checkRangeError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + + if checkNotIntError { + require.Equal(t, errNotInt, err1) + } + if checkRangeError { + require.Equal(t, errNotInt64, err1) + } + } + + os.Unsetenv("TESTSTRUCT_INTDATA") + } +} + +func TestParseUint8(t *testing.T) { + testCases := []struct { + TestData string + ValidData uint8 + }{ + {"0", 0}, + {"1", 1}, + {"2", 2}, + {"255", 255}, + {"256", 0}, + {"-1", 0}, + {"not an integer", 0}, + } + + type testStruct struct { + UintData uint8 + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_UINTDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.UintData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotIntError bool + var checkRangeError bool + passedData, err2 := strconv.ParseUint(testCase.TestData, 10, 64) + if err2 != nil { + checkNotIntError = true + } + if passedData != uint64(testCase.ValidData) { + checkRangeError = true + } + + if checkNotIntError || checkRangeError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + + if checkNotIntError { + require.Equal(t, errNotUint, err1) + } + if checkRangeError { + require.Equal(t, errNotUint8, err1) + } + } + + os.Unsetenv("TESTSTRUCT_UINTDATA") + } +} + +func TestParseUint16(t *testing.T) { + testCases := []struct { + TestData string + ValidData uint16 + }{ + {"0", 0}, + {"1", 1}, + {"2", 2}, + {"65535", 65535}, + {"65536", 0}, + {"-1", 0}, + {"not an integer", 0}, + } + + type testStruct struct { + UintData uint16 + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_UINTDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.UintData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotIntError bool + var checkRangeError bool + passedData, err2 := strconv.ParseUint(testCase.TestData, 10, 64) + if err2 != nil { + checkNotIntError = true + } + if passedData != uint64(testCase.ValidData) { + checkRangeError = true + } + + if checkNotIntError || checkRangeError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + + if checkNotIntError { + require.Equal(t, errNotUint, err1) + } + if checkRangeError { + require.Equal(t, errNotUint16, err1) + } + } + + os.Unsetenv("TESTSTRUCT_UINTDATA") + } +} + +func TestParseUint32(t *testing.T) { + testCases := []struct { + TestData string + ValidData uint32 + }{ + {"0", 0}, + {"1", 1}, + {"2", 2}, + {"4294967295", 4294967295}, + {"4294967296", 0}, + {"-1", 0}, + {"not an integer", 0}, + } + + type testStruct struct { + UintData uint32 + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_UINTDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.UintData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotIntError bool + var checkRangeError bool + passedData, err2 := strconv.ParseUint(testCase.TestData, 10, 64) + if err2 != nil { + checkNotIntError = true + } + if passedData != uint64(testCase.ValidData) { + checkRangeError = true + } + + if checkNotIntError || checkRangeError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + + if checkNotIntError { + require.Equal(t, errNotUint, err1) + } + if checkRangeError { + require.Equal(t, errNotUint32, err1) + } + } + + os.Unsetenv("TESTSTRUCT_UINTDATA") + } +} + +func TestParseUint64(t *testing.T) { + testCases := []struct { + TestData string + ValidData uint64 + }{ + {"0", 0}, + {"1", 1}, + {"2", 2}, + {"18446744073709551615", 18446744073709551615}, + {"18446744073709551616", 18446744073709551615}, + {"-1", 0}, + {"not an integer", 0}, + } + + type testStruct struct { + UintData uint64 + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_UINTDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.UintData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotIntError bool + var checkRangeError bool + passedData, err2 := strconv.ParseUint(testCase.TestData, 10, 64) + if err2 != nil { + checkNotIntError = true + } + if passedData != testCase.ValidData { + checkRangeError = true + } + + if checkNotIntError || checkRangeError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + + if checkNotIntError { + require.Equal(t, errNotUint, err1) + } + if checkRangeError { + require.Equal(t, errNotUint64, err1) + } + } + + os.Unsetenv("TESTSTRUCT_UINTDATA") + } +} + +// Next tests should be improved. +func TestParseFloat32(t *testing.T) { + testCases := []struct { + TestData string + ValidData float32 + }{ + {"0.00", 0.00}, + {"1.00", 1.00}, + {"2.00", 2.00}, + {"-1", -1}, + {"not a float", 0.00}, + } + + type testStruct struct { + FloatData float32 + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_FLOATDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.FloatData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotIntError bool + var checkRangeError bool + passedData, err2 := strconv.ParseFloat(testCase.TestData, 64) + if err2 != nil { + checkNotIntError = true + } + if passedData != float64(testCase.ValidData) { + checkRangeError = true + } + + if checkNotIntError || checkRangeError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + + if checkNotIntError { + require.Equal(t, errNotFloat, err1) + } + if checkRangeError { + require.Equal(t, errNotFloat32, err1) + } + } + + os.Unsetenv("TESTSTRUCT_FLOATDATA") + } +} + +func TestParseFloat64(t *testing.T) { + testCases := []struct { + TestData string + ValidData float64 + }{ + {"0.00", 0.00}, + {"1.00", 1.00}, + {"2.00", 2.00}, + {"-1", -1}, + {"not a float", 0.00}, + } + + type testStruct struct { + FloatData float64 + } + + for _, testCase := range testCases { + t.Logf("Testing: %+v", testCase) + os.Setenv("TESTSTRUCT_FLOATDATA", testCase.TestData) + + // If ErrorsAreCritical == false, then we should check only + // equality of parsed data and valid data. + s := &testStruct{} + err := Parse(s, nil) + if err != nil { + t.Log(err.Error()) + } + require.Nil(t, err) + require.Equal(t, testCase.ValidData, s.FloatData) + + // If errors are critical - we should check if test data is within + // int8 range and check for error if it isn't. + s1 := &testStruct{} + err1 := Parse(s1, &Options{ErrorsAreCritical: true}) + + var checkNotIntError bool + var checkRangeError bool + passedData, err2 := strconv.ParseFloat(testCase.TestData, 64) + if err2 != nil { + checkNotIntError = true + } + if passedData != testCase.ValidData { + checkRangeError = true + } + + if checkNotIntError || checkRangeError { + if err1 == nil { + t.Log("No error returned!") + } + require.NotNil(t, err1) + + if checkNotIntError { + require.Equal(t, errNotFloat, err1) + } + if checkRangeError { + require.Equal(t, errNotFloat64, err1) + } + } + + os.Unsetenv("TESTSTRUCT_FLOATDATA") + } +} + +// Float diff --git a/sec.go b/sec.go new file mode 100644 index 0000000..f5a0a80 --- /dev/null +++ b/sec.go @@ -0,0 +1,93 @@ +package sec + +import ( + // stdlib + "errors" + "log" + "os" + "reflect" + "strconv" + "strings" +) + +var ( + // Errors. + errNotPTR = errors.New("passed data is not a pointer") + errNotStructure = errors.New("passed data is not a structure") + + // Debug flag. + debugFlagEnvName = "SEC_DEBUG" + debug bool + + // Parsed structure fields. + parsedTree []*field + + // Options for current run. + options *Options +) + +// Parse parses environment variables into passed structure. +func Parse(structure interface{}, config *Options) error { + parsedTree = []*field{} + + options = config + if config == nil { + options = defaultOptions + } + + // Set debug flag if defined in environment. + debugFlagRaw, found := os.LookupEnv(debugFlagEnvName) + if found { + var err error + debug, err = strconv.ParseBool(debugFlagRaw) + if err != nil { + log.Println("Invalid '" + debugFlagEnvName + "' environment variable data: '" + debugFlagRaw + "'. Error: " + err.Error()) + if options.ErrorsAreCritical { + return err + } + } else { + printDebug("Debug mode activated") + } + } + + printDebug("Parsing started with configuration: %+v", options) + + value := reflect.ValueOf(structure) + + // Figure out passed data type. We should accept ONLY pointers + // to structure. + printDebug("Passed structure kind: %s, want: %s", value.Type().Kind().String(), reflect.Ptr.String()) + + // If passed data isn't a pointer - return error in any case because + // we can't support anything except pointer. + if value.Type().Kind() != reflect.Ptr { + return errNotPTR + } + + printDebug("Passed data kind: %s, want: %s", value.Elem().Type().Kind().String(), reflect.Struct.String()) + + value = value.Elem() + + // Passed data should be a pointer to structure. Otherwise we should + // return error in any case. + if value.Type().Kind() != reflect.Struct { + return errNotStructure + } + + // Parse structure. + composeTree(value, strings.ToUpper(value.Type().Name())) + + return parseEnv() +} + +// Produces debug output into stdout using standard log module if debug +// mode was activated by setting SEC_DEBUG environment variable to true. +func printDebug(text string, params ...interface{}) { + if debug { + if len(params) == 0 { + log.Println(text) + } else { + log.Printf(text, params...) + } + } +} diff --git a/sec_test.go b/sec_test.go new file mode 100644 index 0000000..3ec653b --- /dev/null +++ b/sec_test.go @@ -0,0 +1,204 @@ +package sec + +import ( + // stdlib + "os" + "strconv" + "testing" + + // other + "github.com/stretchr/testify/require" +) + +var ( + testString = "Test string" + testInt8 = int8(8) + testInt16 = int16(16) + testInt32 = int32(32) + testInt64 = int64(64) + testUint8 = uint8(8) + testUint16 = uint16(16) + testUint32 = uint32(32) + testUint64 = uint64(64) + testFloat32 = float32(32.00) + testFloat64 = float64(64.00) + testBool = true +) + +type testDatas struct { + TestString string + TestInt8 int8 + TestInt16 int16 + TestInt32 int32 + TestInt64 int64 + TestUint8 uint8 + TestUint16 uint16 + TestUint32 uint32 + TestUint64 uint64 + TestFloat32 float32 + TestFloat64 float64 + TestBool bool +} + +type testStringType string + +type testStruct1 struct { + testDatas + testStringType + TestNestAnonymous struct { + TestString string + TestInt8 int8 + TestInt16 int16 + TestInt32 int32 + TestInt64 int64 + TestUint8 uint8 + TestUint16 uint16 + TestUint32 uint32 + TestUint64 uint64 + TestFloat32 float32 + TestFloat64 float64 + TestBool bool + } + TestNestAnonymousPointer *struct { + TestString string + TestInt8 int8 + TestInt16 int16 + TestInt32 int32 + TestInt64 int64 + TestUint8 uint8 + TestUint16 uint16 + TestUint32 uint32 + TestUint64 uint64 + TestFloat32 float32 + TestFloat64 float64 + TestBool bool + } + TestNestPointer *testDatas + TestNest testDatas + TestNestInterfacePointer interface{} + TestNestInterface interface{} + testUnexported string + testUnexportedNest *testDatas +} + +func setenv(prefix string) { + os.Setenv(prefix+"TESTSTRING", testString) + os.Setenv(prefix+"TESTINT8", strconv.FormatInt(int64(testInt8), 10)) + os.Setenv(prefix+"TESTINT16", strconv.FormatInt(int64(testInt16), 10)) + os.Setenv(prefix+"TESTINT32", strconv.FormatInt(int64(testInt32), 10)) + os.Setenv(prefix+"TESTINT64", strconv.FormatInt(int64(testInt64), 10)) + os.Setenv(prefix+"TESTUINT8", strconv.FormatInt(int64(testUint8), 10)) + os.Setenv(prefix+"TESTUINT16", strconv.FormatInt(int64(testUint16), 10)) + os.Setenv(prefix+"TESTUINT32", strconv.FormatInt(int64(testUint32), 10)) + os.Setenv(prefix+"TESTUINT64", strconv.FormatInt(int64(testUint64), 10)) + os.Setenv(prefix+"TESTFLOAT32", strconv.FormatFloat(float64(testFloat32), 'f', 2, 32)) + os.Setenv(prefix+"TESTFLOAT64", strconv.FormatFloat(testFloat64, 'f', 2, 64)) + os.Setenv(prefix+"TESTBOOL", "true") + + os.Setenv(debugFlagEnvName, "true") +} + +func unsetenv(prefix string) { + os.Unsetenv(prefix + "TESTSTRING") + os.Unsetenv(prefix + "TESTINT8") + os.Unsetenv(prefix + "TESTINT16") + os.Unsetenv(prefix + "TESTINT32") + os.Unsetenv(prefix + "TESTINT64") + os.Unsetenv(prefix + "TESTUINT8") + os.Unsetenv(prefix + "TESTUINT16") + os.Unsetenv(prefix + "TESTUINT32") + os.Unsetenv(prefix + "TESTUINT64") + os.Unsetenv(prefix + "TESTFLOAT32") + os.Unsetenv(prefix + "TESTFLOAT64") + os.Unsetenv(prefix + "TESTBOOL") + + os.Unsetenv(debugFlagEnvName) +} + +func TestParseValidData(t *testing.T) { + setenv("TESTSTRUCT1_") + setenv("TESTSTRUCT1_TESTNEST_") + setenv("TESTSTRUCT1_TESTNESTANONYMOUS_") + setenv("TESTSTRUCT1_TESTNESTANONYMOUSPOINTER_") + setenv("TESTSTRUCT1_TESTNESTINTERFACE_") + setenv("TESTSTRUCT1_TESTNESTINTERFACEPOINTER_") + setenv("TESTSTRUCT1_TESTNESTPOINTER_") + setenv("TESTSTRUCT1_TESTUNEXPORTEDNEST_") + + ts := &testStruct1{} + err := Parse(ts, nil) + t.Logf("Parsed data: %+v\n", ts) + t.Logf("Parsed nested data: %+v\n", ts.TestNest) + t.Logf("Parsed nested interface data: %+v\n", ts.TestNestInterface) + + require.Nil(t, err) + require.Equal(t, testBool, ts.TestBool) + + unsetenv("TESTSTRUCT1_") + unsetenv("TESTSTRUCT1_TESTNEST_") + unsetenv("TESTSTRUCT1_TESTNESTANONYMOUS_") + unsetenv("TESTSTRUCT1_TESTNESTANONYMOUSPOINTER_") + unsetenv("TESTSTRUCT1_TESTNESTINTERFACE_") + unsetenv("TESTSTRUCT1_TESTNESTINTERFACEPOINTER_") + unsetenv("TESTSTRUCT1_TESTNESTPOINTER_") + unsetenv("TESTSTRUCT1_TESTUNEXPORTEDNEST_") +} + +func TestParseNotPointerToStructurePassed(t *testing.T) { + setenv("") + + var data string + err := Parse(&data, nil) + + require.NotNil(t, err) + require.Equal(t, errNotStructure, err) + + unsetenv("") +} + +func TestParseNotPointerPassed(t *testing.T) { + setenv("") + + c := testStruct1{} + err := Parse(c, nil) + + require.NotNil(t, err) + require.Equal(t, errNotPTR, err) + + unsetenv("") +} + +func TestParseNotStructurePassed(t *testing.T) { + d := "invalid data" + err := Parse(d, nil) + t.Log(err.Error()) + + require.NotNil(t, err) + require.Equal(t, errNotPTR, err) +} + +func TestInvalidDebugFlagValue(t *testing.T) { + _ = os.Setenv(debugFlagEnvName, "INVALID") + c := &testStruct1{} + err := Parse(c, nil) + + require.Nil(t, err) + require.False(t, debug) + + os.Unsetenv(debugFlagEnvName) +} + +func TestInvalidDebugFlagValueWithErrorsAreCritical(t *testing.T) { + _ = os.Setenv(debugFlagEnvName, "INVALID") + c := &testStruct1{} + err := Parse(c, &Options{ErrorsAreCritical: true}) + + if err != nil { + t.Log(err.Error()) + } + + require.NotNil(t, err) + require.False(t, debug) + + os.Unsetenv(debugFlagEnvName) +}