Skip to content

Commit

Permalink
feat: extend the shared libraries (#94)
Browse files Browse the repository at this point in the history
* feat: add new packages

* chore: update docs
  • Loading branch information
levkohimins authored Aug 28, 2023
1 parent c1e986c commit fdb6b31
Show file tree
Hide file tree
Showing 6 changed files with 356 additions and 0 deletions.
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

0 comments on commit fdb6b31

Please sign in to comment.