From fdb6b318cd8ecd01ad9c1b0e299e664e3bb95de7 Mon Sep 17 00:00:00 2001 From: Levko Burburas <62853952+levkohimins@users.noreply.github.com> Date: Mon, 28 Aug 2023 23:14:07 +0300 Subject: [PATCH] feat: extend the shared libraries (#94) * feat: add new packages * chore: update docs --- README.md | 4 + collections/maps.go | 26 ++++++ collections/maps_test.go | 65 ++++++++++++++ env/env.go | 70 +++++++++++++++ env/env_test.go | 185 +++++++++++++++++++++++++++++++++++++++ errors/errors.go | 6 ++ 6 files changed, 356 insertions(+) create mode 100644 env/env.go create mode 100644 env/env_test.go diff --git a/README.md b/README.md index 624dbbb..f05e1b3 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This repo contains the following packages: * ssh * retry * awscommons +* env Each of these packages is described below. @@ -156,6 +157,9 @@ This package contains routines for interacting with AWS. Meant to provide high l Note that the routines in this package are adapted for `aws-sdk-go-v2`, not v1 (`aws-sdk-go`). +### env + +This package contains helper methods for convenient work with environment variables. ## Running tests diff --git a/collections/maps.go b/collections/maps.go index 6c81967..89e0a14 100644 --- a/collections/maps.go +++ b/collections/maps.go @@ -79,3 +79,29 @@ func KeyValueStringSliceAsMap(kvPairs []string) map[string][]string { } return out } + +// MapJoin converts the map to a string type by concatenating the key with the value using the given `mapSep` string, and `sliceSep` string between the slice values. +// For example: `Slice(map[int]string{1: "one", 2: "two"}, "-", ", ")` returns `"1-one, 2-two"` +func MapJoin[M ~map[K]V, K comparable, V any](m M, sliceSep, mapSep string) string { + list := MapToSlice(m, mapSep) + + sort.Slice(list, func(i, j int) bool { + return list[i] < list[j] + }) + + return strings.Join(list, sliceSep) +} + +// MapToSlice converts the map to a string slice by concatenating the key with the value using the given `sep` string. +// For example: `Slice(map[int]string{1: "one", 2: "two"}, "-")` returns `[]string{"1-one", "2-two"}` +func MapToSlice[M ~map[K]V, K comparable, V any](m M, sep string) []string { + var list []string + + for key, val := range m { + s := fmt.Sprintf("%v%s%v", key, sep, val) + list = append(list, s) + + } + + return list +} diff --git a/collections/maps_test.go b/collections/maps_test.go index 3b147e1..bee6701 100644 --- a/collections/maps_test.go +++ b/collections/maps_test.go @@ -281,3 +281,68 @@ func TestKeyValueStringSliceAsMap(t *testing.T) { }) } } + +func TestMapJoin(t *testing.T) { + t.Parallel() + + var testCases = []struct { + vals any + sliceSep, mapSep string + expected string + }{ + {map[string]string{"color": "white", "number": "two"}, ",", "=", "color=white,number=two"}, + {map[int]int{10: 100, 20: 200}, " ", ":", "10:100 20:200"}, + } + + for i, testCase := range testCases { + // to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below + testCase := testCase + + t.Run(fmt.Sprintf("test-%d-vals-%v-expected-%s", i, testCase.vals, testCase.expected), func(t *testing.T) { + t.Parallel() + + var actual string + + switch vals := testCase.vals.(type) { + case map[string]string: + actual = MapJoin(vals, testCase.sliceSep, testCase.mapSep) + case map[int]int: + actual = MapJoin(vals, testCase.sliceSep, testCase.mapSep) + } + assert.Equal(t, testCase.expected, actual) + }) + } +} + +func TestMapToSlice(t *testing.T) { + t.Parallel() + + var testCases = []struct { + vals any + sep string + expected []string + }{ + {map[string]string{"color": "white", "number": "two"}, "=", []string{"color=white", "number=two"}}, + {map[int]int{10: 100, 20: 200}, ":", []string{"10:100", "20:200"}}, + } + + for i, testCase := range testCases { + // to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below + testCase := testCase + + t.Run(fmt.Sprintf("test-%d-vals-%v-expected-%s", i, testCase.vals, testCase.expected), func(t *testing.T) { + t.Parallel() + + var actual []string + + switch vals := testCase.vals.(type) { + case map[string]string: + actual = MapToSlice(vals, testCase.sep) + case map[int]int: + actual = MapToSlice(vals, testCase.sep) + } + + assert.Subset(t, testCase.expected, actual) + }) + } +} diff --git a/env/env.go b/env/env.go new file mode 100644 index 0000000..ef2dd67 --- /dev/null +++ b/env/env.go @@ -0,0 +1,70 @@ +package env + +import ( + "strconv" + "strings" +) + +// GetBool converts the given value to the bool type and returns that value, or returns the specified fallback value if the value is empty. +func GetBool(value string, fallback bool) bool { + if strVal, ok := nonEmptyValue(value); ok { + if val, err := strconv.ParseBool(strVal); err == nil { + return val + } + } + + return fallback +} + +// GetNegativeBool converts the given value to the bool type and returns the inverted value, or returns the specified fallback value if the value is empty. +func GetNegativeBool(value string, fallback bool) bool { + if strVal, ok := nonEmptyValue(value); ok { + if val, err := strconv.ParseBool(strVal); err == nil { + return !val + } + } + + return fallback +} + +// GetInt converts the given value to the integer type and returns that value, or returns the specified fallback value if the value is empty. +func GetInt(value string, fallback int) int { + if strVal, ok := nonEmptyValue(value); ok { + if val, err := strconv.Atoi(strVal); err == nil { + return val + } + } + + return fallback +} + +// GetString returns the same string value, or returns the given fallback value if the value is empty. +func GetString(value string, fallback string) string { + if val, ok := nonEmptyValue(value); ok { + return val + } + + return fallback +} + +// nonEmptyValue trims spaces in the value and returns this trimmed value and true if the value is not empty, otherwise false. +func nonEmptyValue(value string) (string, bool) { + value = strings.TrimSpace(value) + isPresent := value != "" + + return value, isPresent +} + +func Parse(envs []string) map[string]string { + envMap := make(map[string]string) + + for _, env := range envs { + parts := strings.SplitN(env, "=", 2) + + if len(parts) == 2 { + envMap[strings.TrimSpace(parts[0])] = parts[1] + } + } + + return envMap +} diff --git a/env/env_test.go b/env/env_test.go new file mode 100644 index 0000000..0195ec2 --- /dev/null +++ b/env/env_test.go @@ -0,0 +1,185 @@ +package env + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestGetBool(t *testing.T) { + t.Parallel() + + var testCases = []struct { + envVarValue string + fallback bool + expected bool + }{ + // false + {"", false, false}, + {"false", false, false}, + {" false ", false, false}, + {"False", false, false}, + {"FALSE", false, false}, + {"0", false, false}, + // true + {"true", false, true}, + {" true ", false, true}, + {"True", false, true}, + {"TRUE", false, true}, + {"", true, true}, + {"", true, true}, + {"1", true, true}, + {"foo", false, false}, + } + + for i, testCase := range testCases { + // to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below + testCase := testCase + + envVarName := fmt.Sprintf("TestGetBool-testCase-%d", i) + t.Run(envVarName, func(t *testing.T) { + t.Parallel() + + actual := GetBool(testCase.envVarValue, testCase.fallback) + assert.Equal(t, testCase.expected, actual) + }) + } +} + +func TestGetNegativeBool(t *testing.T) { + t.Parallel() + + var testCases = []struct { + envVarValue string + fallback bool + expected bool + }{ + // true + {"", true, true}, + {"false", false, true}, + {" false ", false, true}, + {"False", false, true}, + {"FALSE", false, true}, + {"0", false, true}, + // false + {"", false, false}, + {"true", false, false}, + {" true ", false, false}, + {"True", false, false}, + {"TRUE", false, false}, + + {"1", true, false}, + {"foo", false, false}, + } + + for i, testCase := range testCases { + // to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below + testCase := testCase + + envVarName := fmt.Sprintf("TestGetNegativeBool-testCase-%d", i) + t.Run(envVarName, func(t *testing.T) { + t.Parallel() + + actual := GetNegativeBool(testCase.envVarValue, testCase.fallback) + assert.Equal(t, testCase.expected, actual) + }) + } +} + +func TestGetInt(t *testing.T) { + t.Parallel() + + var testCases = []struct { + envVarValue string + fallback int + expected int + }{ + {"10", 20, 10}, + {"0", 30, 0}, + {"", 5, 5}, + {"foo", 15, 15}, + } + + for i, testCase := range testCases { + // to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below + testCase := testCase + + envVarName := fmt.Sprintf("TestGetInt-testCase-%d", i) + t.Run(envVarName, func(t *testing.T) { + t.Parallel() + + actual := GetInt(testCase.envVarValue, testCase.fallback) + assert.Equal(t, testCase.expected, actual) + }) + } +} + +func TestGetString(t *testing.T) { + t.Parallel() + + var testCases = []struct { + envVarValue string + fallback string + expected string + }{ + {"first", "second", "first"}, + {"", "second", "second"}, + } + + for i, testCase := range testCases { + // to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below + testCase := testCase + + envVarName := fmt.Sprintf("test-%d-val-%s-expected-%s", i, testCase.envVarValue, testCase.expected) + t.Run(envVarName, func(t *testing.T) { + t.Parallel() + + actual := GetString(testCase.envVarValue, testCase.fallback) + assert.Equal(t, testCase.expected, actual) + }) + } +} + +func TestParseironmentVariables(t *testing.T) { + t.Parallel() + + testCases := []struct { + environmentVariables []string + expectedVariables map[string]string + }{ + { + []string{}, + map[string]string{}, + }, + { + []string{"foobar"}, + map[string]string{}, + }, + { + []string{"foo=bar"}, + map[string]string{"foo": "bar"}, + }, + { + []string{"foo=bar", "goo=gar"}, + map[string]string{"foo": "bar", "goo": "gar"}, + }, + { + []string{"foo=bar "}, + map[string]string{"foo": "bar "}, + }, + { + []string{"foo =bar "}, + map[string]string{"foo": "bar "}, + }, + { + []string{"foo=composite=bar"}, + map[string]string{"foo": "composite=bar"}, + }, + } + + for _, testCase := range testCases { + actualVariables := Parse(testCase.environmentVariables) + assert.Equal(t, testCase.expectedVariables, actualVariables) + } +} diff --git a/errors/errors.go b/errors/errors.go index 47d206c..48a8c0d 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -7,6 +7,12 @@ import ( "github.com/urfave/cli/v2" ) +// Errorf creates a new error and wraps in an Error type that contains the stack trace. +func Errorf(message string, args ...interface{}) error { + err := fmt.Errorf(message, args...) + return goerrors.Wrap(err, 1) +} + // If this error is returned, the program should exit with the given exit code. type ErrorWithExitCode struct { Err error