Initial commit.

This commit is contained in:
Stanislav Nikitin 2019-09-17 18:39:37 +05:00
commit 3476411569
No known key found for this signature in database
GPG Key ID: 106900B32F8192EE
11 changed files with 1401 additions and 0 deletions

6
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"go.testFlags": [
"-test.v",
"-cover"
]
}

42
README.md Normal file
View File

@ -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.

91
compose_tree.go Normal file
View File

@ -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)
}
}
}

22
field.go Normal file
View File

@ -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
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module gitlab.com/pztrn/sec
go 1.13
require github.com/stretchr/testify v1.4.0

11
go.sum Normal file
View File

@ -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=

18
options.go Normal file
View File

@ -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,
}
)

169
parse_env.go Normal file
View File

@ -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
}

740
parse_env_test.go Normal file
View File

@ -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

93
sec.go Normal file
View File

@ -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...)
}
}
}

204
sec_test.go Normal file
View File

@ -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)
}