Skip to content

Commit 6b201bf

Browse files
authored
Add StringMap type (#97)
1 parent 04a1b46 commit 6b201bf

File tree

3 files changed

+107
-0
lines changed

3 files changed

+107
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ The fields have to be one of the types that the sync package supports in order t
4545
- sync.Bool, allows for concurrent bool manipulation
4646
- sync.Secret, allows for concurrent secret manipulation. Secrets can only be strings
4747
- sync.TimeDuration, allows for concurrent time.duration manipulation.
48+
- sync.StringMap, allows for concurrent map[string]string manipulation.
4849

4950
For sensitive configuration (passwords, tokens, etc.) that shouldn't be printed in log, you can use the `Secret` flavor of `sync` types. If one of these is selected, then at harvester log instead of the real value the text `***` will be displayed.
5051

sync/sync.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
package sync
33

44
import (
5+
"bytes"
56
"fmt"
67
"strconv"
8+
"strings"
79
"sync"
810
"time"
911
)
@@ -222,3 +224,55 @@ func (s *Secret) SetString(val string) error {
222224
s.Set(val)
223225
return nil
224226
}
227+
228+
// StringMap is a map[string]string type with concurrent access support.
229+
type StringMap struct {
230+
rw sync.RWMutex
231+
value map[string]string
232+
}
233+
234+
// Get returns the internal value.
235+
func (s *StringMap) Get() map[string]string {
236+
s.rw.RLock()
237+
defer s.rw.RUnlock()
238+
return s.value
239+
}
240+
241+
// Set a value.
242+
func (s *StringMap) Set(value map[string]string) {
243+
s.rw.Lock()
244+
defer s.rw.Unlock()
245+
s.value = value
246+
}
247+
248+
// String returns a string representation of the value.
249+
func (s *StringMap) String() string {
250+
s.rw.RLock()
251+
defer s.rw.RUnlock()
252+
b := new(bytes.Buffer)
253+
firstChar := ""
254+
for key, value := range s.value {
255+
_, _ = fmt.Fprintf(b, "%s%s=%q", firstChar, key, value)
256+
firstChar = ","
257+
}
258+
return b.String()
259+
}
260+
261+
// SetString parses and sets a value from string type.
262+
func (s *StringMap) SetString(val string) error {
263+
dict := make(map[string]string)
264+
if val == "" || strings.TrimSpace(val) == "" {
265+
s.Set(dict)
266+
return nil
267+
}
268+
for _, pair := range strings.Split(val, ",") {
269+
items := strings.SplitN(pair, ":", 2)
270+
if len(items) != 2 {
271+
return fmt.Errorf("map must be formatted as `key:value`, got %q", pair)
272+
}
273+
key, value := strings.TrimSpace(items[0]), strings.TrimSpace(items[1])
274+
dict[key] = value
275+
}
276+
s.Set(dict)
277+
return nil
278+
}

sync/sync_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,55 @@ func TestTimeDuration_SetString(t *testing.T) {
119119
assert.NoError(t, f.SetString("3s"))
120120
assert.Equal(t, 3*time.Second, f.Get())
121121
}
122+
123+
func TestStringMap(t *testing.T) {
124+
var sm StringMap
125+
ch := make(chan struct{})
126+
go func() {
127+
sm.Set(map[string]string{"key": "value"})
128+
ch <- struct{}{}
129+
}()
130+
<-ch
131+
assert.Equal(t, map[string]string{"key": "value"}, sm.Get())
132+
assert.Equal(t, "key=\"value\"", sm.String())
133+
}
134+
135+
func TestStringMap_SetString(t *testing.T) {
136+
tests := []struct {
137+
name string
138+
input string
139+
result map[string]string
140+
throwsError bool
141+
}{
142+
{"empty", "", map[string]string{}, false},
143+
{"empty with spaces", " ", map[string]string{}, false},
144+
{"single item", "key:value", map[string]string{"key": "value"}, false},
145+
{"single item with route as val", "key:http://thing", map[string]string{"key": "http://thing"}, false},
146+
{"key without value", "key", nil, true},
147+
{"multiple items", "key1:value,key2:value", map[string]string{"key1": "value", "key2": "value"}, false},
148+
{"multiple items with spaces", " key1 : value , key2 :value ", map[string]string{"key1": "value", "key2": "value"}, false},
149+
{"multiple urls", "key1:http://one,key2:https://two", map[string]string{"key1": "http://one", "key2": "https://two"}, false},
150+
}
151+
for _, test := range tests {
152+
t.Run(test.name, func(t *testing.T) {
153+
sm := StringMap{}
154+
155+
err := sm.SetString(test.input)
156+
if test.throwsError {
157+
assert.Error(t, err)
158+
}
159+
160+
assert.Equal(t, test.result, sm.Get())
161+
})
162+
}
163+
}
164+
165+
func TestStringMap_SetString_DoesntOverrideValueIfError(t *testing.T) {
166+
sm := StringMap{}
167+
168+
assert.NoError(t, sm.SetString("k1:v1"))
169+
assert.Equal(t, map[string]string{"k1": "v1"}, sm.Get())
170+
171+
assert.Error(t, sm.SetString("k1:v1,k2:v2,k3"))
172+
assert.Equal(t, map[string]string{"k1": "v1"}, sm.Get())
173+
}

0 commit comments

Comments
 (0)