Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
158 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
// Copyright 2015 Michal Witkowski. All Rights Reserved. | ||
// See LICENSE for licensing terms. | ||
|
||
package flagz | ||
|
||
import ( | ||
"encoding/csv" | ||
"fmt" | ||
"strings" | ||
"sync/atomic" | ||
"unsafe" | ||
|
||
flag "github.com/spf13/pflag" | ||
) | ||
|
||
// DynStringSet creates a `Flag` that represents `map[string]bool` which is safe to change dynamically at runtime. | ||
// Unlike `pflag.StringSlice`, consecutive sets don't append to the slice, but override it. | ||
func DynStringSet(flagSet *flag.FlagSet, name string, value []string, usage string) *DynStringSetValue { | ||
set := buildStringSet(value) | ||
dynValue := &DynStringSetValue{ptr: unsafe.Pointer(&set)} | ||
flag := flagSet.VarPF(dynValue, name, "", usage) | ||
MarkFlagDynamic(flag) | ||
return dynValue | ||
} | ||
|
||
// DynStringSetValue is a flag-related `map[string]bool` value wrapper. | ||
type DynStringSetValue struct { | ||
ptr unsafe.Pointer | ||
validator func(map[string]bool) error | ||
notifier func(oldValue map[string]bool, newValue map[string]bool) | ||
} | ||
|
||
// Get retrieves the value in a thread-safe manner. | ||
func (d *DynStringSetValue) Get() map[string]bool { | ||
p := (*map[string]bool)(atomic.LoadPointer(&d.ptr)) | ||
return *p | ||
} | ||
|
||
// Set updates the value from a string representation in a thread-safe manner. | ||
// This operation may return an error if the provided `input` doesn't parse, or the resulting value doesn't pass an | ||
// optional validator. | ||
// If a notifier is set on the value, it will be invoked in a separate go-routine. | ||
func (d *DynStringSetValue) Set(val string) error { | ||
v, err := csv.NewReader(strings.NewReader(val)).Read() | ||
if err != nil { | ||
return err | ||
} | ||
s := buildStringSet(v) | ||
if d.validator != nil { | ||
if err := d.validator(s); err != nil { | ||
return err | ||
} | ||
} | ||
oldPtr := atomic.SwapPointer(&d.ptr, unsafe.Pointer(&s)) | ||
if d.notifier != nil { | ||
go d.notifier(*(*map[string]bool)(oldPtr), s) | ||
} | ||
return nil | ||
} | ||
|
||
// WithValidator adds a function that checks values before they're set. | ||
// Any error returned by the validator will lead to the value being rejected. | ||
// Validators are executed on the same go-routine as the call to `Set`. | ||
func (d *DynStringSetValue) WithValidator(validator func(map[string]bool) error) { | ||
d.validator = validator | ||
} | ||
|
||
// WithNotifier adds a function that is called every time a new value is successfully set. | ||
// Each notifier is executed asynchronously in a new go-routine. | ||
func (d *DynStringSetValue) WithNotifier(notifier func(oldValue map[string]bool, newValue map[string]bool)) { | ||
d.notifier = notifier | ||
} | ||
|
||
// Type is an indicator of what this flag represents. | ||
func (d *DynStringSetValue) Type() string { | ||
return "dyn_stringslice" | ||
} | ||
|
||
// String represents the canonical representation of the type. | ||
func (d *DynStringSetValue) String() string { | ||
v := d.Get() | ||
arr := make([]string, len(v)) | ||
for k := range v { | ||
arr = append(arr, k) | ||
} | ||
return fmt.Sprintf("%v", arr) | ||
} | ||
|
||
// ValidateDynStringSetMinElements validates that the given string slice has at least x elements. | ||
func ValidateDynStringSetMinElements(count int) func(map[string]bool) error { | ||
return func(value map[string]bool) error { | ||
if len(value) < count { | ||
return fmt.Errorf("value slice %v must have at least %v elements", value, count) | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
func buildStringSet(items []string) map[string]bool { | ||
res := map[string]bool{} | ||
for _, item := range items { | ||
res[item] = true | ||
} | ||
return res | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// Copyright 2015 Michal Witkowski. All Rights Reserved. | ||
// See LICENSE for licensing terms. | ||
|
||
package flagz | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
flag "github.com/spf13/pflag" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestDynStringSet_SetAndGet(t *testing.T) { | ||
set := flag.NewFlagSet("foobar", flag.ContinueOnError) | ||
dynFlag := DynStringSet(set, "some_stringslice_1", []string{"foo", "bar"}, "Use it or lose it") | ||
assert.Equal(t, map[string]bool{"foo": true, "bar": true}, dynFlag.Get(), "value must be default after create") | ||
err := set.Set("some_stringslice_1", "car,bar") | ||
assert.NoError(t, err, "setting value must succeed") | ||
assert.Equal(t, map[string]bool{"car": true, "bar": true}, dynFlag.Get(), "value must be set after update") | ||
} | ||
|
||
func TestDynStringSet_IsMarkedDynamic(t *testing.T) { | ||
set := flag.NewFlagSet("foobar", flag.ContinueOnError) | ||
DynStringSet(set, "some_stringslice_1", []string{"foo", "bar"}, "Use it or lose it") | ||
assert.True(t, IsFlagDynamic(set.Lookup("some_stringslice_1"))) | ||
} | ||
|
||
func TestDynStringSet_FiresValidators(t *testing.T) { | ||
set := flag.NewFlagSet("foobar", flag.ContinueOnError) | ||
DynStringSet(set, "some_stringslice_1", []string{"foo", "bar"}, "Use it or lose it").WithValidator(ValidateDynStringSetMinElements(2)) | ||
|
||
assert.NoError(t, set.Set("some_stringslice_1", "car,far"), "no error from validator when in range") | ||
assert.Error(t, set.Set("some_stringslice_1", "car"), "error from validator when value out of range") | ||
} | ||
|
||
func TestDynStringSet_FiresNotifier(t *testing.T) { | ||
waitCh := make(chan bool, 1) | ||
notifier := func(oldVal map[string]bool, newVal map[string]bool) { | ||
assert.EqualValues(t, map[string]bool{"foo": true, "bar": true}, oldVal, "old value in notify must match previous value") | ||
assert.EqualValues(t, map[string]bool{"car": true, "far": true}, newVal, "new value in notify must match set value") | ||
waitCh <- true | ||
} | ||
|
||
set := flag.NewFlagSet("foobar", flag.ContinueOnError) | ||
DynStringSet(set, "some_stringslice_1", []string{"foo", "bar"}, "Use it or lose it").WithNotifier(notifier) | ||
set.Set("some_stringslice_1", "car,far") | ||
select { | ||
case <-time.After(5 * time.Millisecond): | ||
assert.Fail(t, "failed to trigger notifier") | ||
case <-waitCh: | ||
} | ||
} |