Skip to content

Commit fdb6b31

Browse files
authored
feat: extend the shared libraries (#94)
* feat: add new packages * chore: update docs
1 parent c1e986c commit fdb6b31

File tree

6 files changed

+356
-0
lines changed

6 files changed

+356
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ This repo contains the following packages:
2222
* ssh
2323
* retry
2424
* awscommons
25+
* env
2526

2627
Each of these packages is described below.
2728

@@ -156,6 +157,9 @@ This package contains routines for interacting with AWS. Meant to provide high l
156157

157158
Note that the routines in this package are adapted for `aws-sdk-go-v2`, not v1 (`aws-sdk-go`).
158159

160+
### env
161+
162+
This package contains helper methods for convenient work with environment variables.
159163

160164
## Running tests
161165

collections/maps.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,29 @@ func KeyValueStringSliceAsMap(kvPairs []string) map[string][]string {
7979
}
8080
return out
8181
}
82+
83+
// 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.
84+
// For example: `Slice(map[int]string{1: "one", 2: "two"}, "-", ", ")` returns `"1-one, 2-two"`
85+
func MapJoin[M ~map[K]V, K comparable, V any](m M, sliceSep, mapSep string) string {
86+
list := MapToSlice(m, mapSep)
87+
88+
sort.Slice(list, func(i, j int) bool {
89+
return list[i] < list[j]
90+
})
91+
92+
return strings.Join(list, sliceSep)
93+
}
94+
95+
// MapToSlice converts the map to a string slice by concatenating the key with the value using the given `sep` string.
96+
// For example: `Slice(map[int]string{1: "one", 2: "two"}, "-")` returns `[]string{"1-one", "2-two"}`
97+
func MapToSlice[M ~map[K]V, K comparable, V any](m M, sep string) []string {
98+
var list []string
99+
100+
for key, val := range m {
101+
s := fmt.Sprintf("%v%s%v", key, sep, val)
102+
list = append(list, s)
103+
104+
}
105+
106+
return list
107+
}

collections/maps_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,68 @@ func TestKeyValueStringSliceAsMap(t *testing.T) {
281281
})
282282
}
283283
}
284+
285+
func TestMapJoin(t *testing.T) {
286+
t.Parallel()
287+
288+
var testCases = []struct {
289+
vals any
290+
sliceSep, mapSep string
291+
expected string
292+
}{
293+
{map[string]string{"color": "white", "number": "two"}, ",", "=", "color=white,number=two"},
294+
{map[int]int{10: 100, 20: 200}, " ", ":", "10:100 20:200"},
295+
}
296+
297+
for i, testCase := range testCases {
298+
// to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below
299+
testCase := testCase
300+
301+
t.Run(fmt.Sprintf("test-%d-vals-%v-expected-%s", i, testCase.vals, testCase.expected), func(t *testing.T) {
302+
t.Parallel()
303+
304+
var actual string
305+
306+
switch vals := testCase.vals.(type) {
307+
case map[string]string:
308+
actual = MapJoin(vals, testCase.sliceSep, testCase.mapSep)
309+
case map[int]int:
310+
actual = MapJoin(vals, testCase.sliceSep, testCase.mapSep)
311+
}
312+
assert.Equal(t, testCase.expected, actual)
313+
})
314+
}
315+
}
316+
317+
func TestMapToSlice(t *testing.T) {
318+
t.Parallel()
319+
320+
var testCases = []struct {
321+
vals any
322+
sep string
323+
expected []string
324+
}{
325+
{map[string]string{"color": "white", "number": "two"}, "=", []string{"color=white", "number=two"}},
326+
{map[int]int{10: 100, 20: 200}, ":", []string{"10:100", "20:200"}},
327+
}
328+
329+
for i, testCase := range testCases {
330+
// to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below
331+
testCase := testCase
332+
333+
t.Run(fmt.Sprintf("test-%d-vals-%v-expected-%s", i, testCase.vals, testCase.expected), func(t *testing.T) {
334+
t.Parallel()
335+
336+
var actual []string
337+
338+
switch vals := testCase.vals.(type) {
339+
case map[string]string:
340+
actual = MapToSlice(vals, testCase.sep)
341+
case map[int]int:
342+
actual = MapToSlice(vals, testCase.sep)
343+
}
344+
345+
assert.Subset(t, testCase.expected, actual)
346+
})
347+
}
348+
}

env/env.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package env
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
)
7+
8+
// GetBool converts the given value to the bool type and returns that value, or returns the specified fallback value if the value is empty.
9+
func GetBool(value string, fallback bool) bool {
10+
if strVal, ok := nonEmptyValue(value); ok {
11+
if val, err := strconv.ParseBool(strVal); err == nil {
12+
return val
13+
}
14+
}
15+
16+
return fallback
17+
}
18+
19+
// 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.
20+
func GetNegativeBool(value string, fallback bool) bool {
21+
if strVal, ok := nonEmptyValue(value); ok {
22+
if val, err := strconv.ParseBool(strVal); err == nil {
23+
return !val
24+
}
25+
}
26+
27+
return fallback
28+
}
29+
30+
// GetInt converts the given value to the integer type and returns that value, or returns the specified fallback value if the value is empty.
31+
func GetInt(value string, fallback int) int {
32+
if strVal, ok := nonEmptyValue(value); ok {
33+
if val, err := strconv.Atoi(strVal); err == nil {
34+
return val
35+
}
36+
}
37+
38+
return fallback
39+
}
40+
41+
// GetString returns the same string value, or returns the given fallback value if the value is empty.
42+
func GetString(value string, fallback string) string {
43+
if val, ok := nonEmptyValue(value); ok {
44+
return val
45+
}
46+
47+
return fallback
48+
}
49+
50+
// nonEmptyValue trims spaces in the value and returns this trimmed value and true if the value is not empty, otherwise false.
51+
func nonEmptyValue(value string) (string, bool) {
52+
value = strings.TrimSpace(value)
53+
isPresent := value != ""
54+
55+
return value, isPresent
56+
}
57+
58+
func Parse(envs []string) map[string]string {
59+
envMap := make(map[string]string)
60+
61+
for _, env := range envs {
62+
parts := strings.SplitN(env, "=", 2)
63+
64+
if len(parts) == 2 {
65+
envMap[strings.TrimSpace(parts[0])] = parts[1]
66+
}
67+
}
68+
69+
return envMap
70+
}

env/env_test.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package env
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestGetBool(t *testing.T) {
11+
t.Parallel()
12+
13+
var testCases = []struct {
14+
envVarValue string
15+
fallback bool
16+
expected bool
17+
}{
18+
// false
19+
{"", false, false},
20+
{"false", false, false},
21+
{" false ", false, false},
22+
{"False", false, false},
23+
{"FALSE", false, false},
24+
{"0", false, false},
25+
// true
26+
{"true", false, true},
27+
{" true ", false, true},
28+
{"True", false, true},
29+
{"TRUE", false, true},
30+
{"", true, true},
31+
{"", true, true},
32+
{"1", true, true},
33+
{"foo", false, false},
34+
}
35+
36+
for i, testCase := range testCases {
37+
// to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below
38+
testCase := testCase
39+
40+
envVarName := fmt.Sprintf("TestGetBool-testCase-%d", i)
41+
t.Run(envVarName, func(t *testing.T) {
42+
t.Parallel()
43+
44+
actual := GetBool(testCase.envVarValue, testCase.fallback)
45+
assert.Equal(t, testCase.expected, actual)
46+
})
47+
}
48+
}
49+
50+
func TestGetNegativeBool(t *testing.T) {
51+
t.Parallel()
52+
53+
var testCases = []struct {
54+
envVarValue string
55+
fallback bool
56+
expected bool
57+
}{
58+
// true
59+
{"", true, true},
60+
{"false", false, true},
61+
{" false ", false, true},
62+
{"False", false, true},
63+
{"FALSE", false, true},
64+
{"0", false, true},
65+
// false
66+
{"", false, false},
67+
{"true", false, false},
68+
{" true ", false, false},
69+
{"True", false, false},
70+
{"TRUE", false, false},
71+
72+
{"1", true, false},
73+
{"foo", false, false},
74+
}
75+
76+
for i, testCase := range testCases {
77+
// to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below
78+
testCase := testCase
79+
80+
envVarName := fmt.Sprintf("TestGetNegativeBool-testCase-%d", i)
81+
t.Run(envVarName, func(t *testing.T) {
82+
t.Parallel()
83+
84+
actual := GetNegativeBool(testCase.envVarValue, testCase.fallback)
85+
assert.Equal(t, testCase.expected, actual)
86+
})
87+
}
88+
}
89+
90+
func TestGetInt(t *testing.T) {
91+
t.Parallel()
92+
93+
var testCases = []struct {
94+
envVarValue string
95+
fallback int
96+
expected int
97+
}{
98+
{"10", 20, 10},
99+
{"0", 30, 0},
100+
{"", 5, 5},
101+
{"foo", 15, 15},
102+
}
103+
104+
for i, testCase := range testCases {
105+
// to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below
106+
testCase := testCase
107+
108+
envVarName := fmt.Sprintf("TestGetInt-testCase-%d", i)
109+
t.Run(envVarName, func(t *testing.T) {
110+
t.Parallel()
111+
112+
actual := GetInt(testCase.envVarValue, testCase.fallback)
113+
assert.Equal(t, testCase.expected, actual)
114+
})
115+
}
116+
}
117+
118+
func TestGetString(t *testing.T) {
119+
t.Parallel()
120+
121+
var testCases = []struct {
122+
envVarValue string
123+
fallback string
124+
expected string
125+
}{
126+
{"first", "second", "first"},
127+
{"", "second", "second"},
128+
}
129+
130+
for i, testCase := range testCases {
131+
// to make sure testCase's values don't get updated due to concurrency within the scope of t.Run(..) below
132+
testCase := testCase
133+
134+
envVarName := fmt.Sprintf("test-%d-val-%s-expected-%s", i, testCase.envVarValue, testCase.expected)
135+
t.Run(envVarName, func(t *testing.T) {
136+
t.Parallel()
137+
138+
actual := GetString(testCase.envVarValue, testCase.fallback)
139+
assert.Equal(t, testCase.expected, actual)
140+
})
141+
}
142+
}
143+
144+
func TestParseironmentVariables(t *testing.T) {
145+
t.Parallel()
146+
147+
testCases := []struct {
148+
environmentVariables []string
149+
expectedVariables map[string]string
150+
}{
151+
{
152+
[]string{},
153+
map[string]string{},
154+
},
155+
{
156+
[]string{"foobar"},
157+
map[string]string{},
158+
},
159+
{
160+
[]string{"foo=bar"},
161+
map[string]string{"foo": "bar"},
162+
},
163+
{
164+
[]string{"foo=bar", "goo=gar"},
165+
map[string]string{"foo": "bar", "goo": "gar"},
166+
},
167+
{
168+
[]string{"foo=bar "},
169+
map[string]string{"foo": "bar "},
170+
},
171+
{
172+
[]string{"foo =bar "},
173+
map[string]string{"foo": "bar "},
174+
},
175+
{
176+
[]string{"foo=composite=bar"},
177+
map[string]string{"foo": "composite=bar"},
178+
},
179+
}
180+
181+
for _, testCase := range testCases {
182+
actualVariables := Parse(testCase.environmentVariables)
183+
assert.Equal(t, testCase.expectedVariables, actualVariables)
184+
}
185+
}

errors/errors.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import (
77
"github.com/urfave/cli/v2"
88
)
99

10+
// Errorf creates a new error and wraps in an Error type that contains the stack trace.
11+
func Errorf(message string, args ...interface{}) error {
12+
err := fmt.Errorf(message, args...)
13+
return goerrors.Wrap(err, 1)
14+
}
15+
1016
// If this error is returned, the program should exit with the given exit code.
1117
type ErrorWithExitCode struct {
1218
Err error

0 commit comments

Comments
 (0)