Skip to content

feat: extend the shared libraries #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This repo contains the following packages:
* ssh
* retry
* awscommons
* env

Each of these packages is described below.

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

Expand Down
26 changes: 26 additions & 0 deletions collections/maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
65 changes: 65 additions & 0 deletions collections/maps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
70 changes: 70 additions & 0 deletions env/env.go
Original file line number Diff line number Diff line change
@@ -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
}
185 changes: 185 additions & 0 deletions env/env_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
6 changes: 6 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down