From 9218a3eb00ecbe2300abf4c14525eb10f8587daa Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 3 Nov 2021 20:17:15 +0000 Subject: [PATCH] fs: add a tristate true/false/unset configuration value --- fs/tristate.go | 74 ++++++++++++++++++++++++++++++++++++++ fs/tristate_test.go | 87 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 fs/tristate.go create mode 100644 fs/tristate_test.go diff --git a/fs/tristate.go b/fs/tristate.go new file mode 100644 index 000000000..d35980e03 --- /dev/null +++ b/fs/tristate.go @@ -0,0 +1,74 @@ +package fs + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// Tristate is a boolean that can has the states, true, false and +// unset/invalid/nil +type Tristate struct { + Value bool + Valid bool +} + +// String renders the tristate as true/false/unset +func (t Tristate) String() string { + if !t.Valid { + return "unset" + } + if t.Value { + return "true" + } + return "false" +} + +// Set the List entries +func (t *Tristate) Set(s string) error { + s = strings.ToLower(s) + if s == "" || s == "nil" || s == "null" || s == "unset" { + t.Valid = false + return nil + } + value, err := strconv.ParseBool(s) + if err != nil { + return errors.Wrapf(err, "failed to parse Tristate %q", s) + } + t.Value = value + t.Valid = true + return nil +} + +// Type of the value +func (Tristate) Type() string { + return "Tristate" +} + +// Scan implements the fmt.Scanner interface +func (t *Tristate) Scan(s fmt.ScanState, ch rune) error { + token, err := s.Token(true, nil) + if err != nil { + return err + } + return t.Set(string(token)) +} + +// UnmarshalJSON parses it as a bool or nil for unset +func (t *Tristate) UnmarshalJSON(in []byte) error { + var b *bool + err := json.Unmarshal(in, &b) + if err != nil { + return err + } + if b != nil { + t.Valid = true + t.Value = *b + } else { + t.Valid = false + } + return nil +} diff --git a/fs/tristate_test.go b/fs/tristate_test.go new file mode 100644 index 000000000..9189f2293 --- /dev/null +++ b/fs/tristate_test.go @@ -0,0 +1,87 @@ +package fs + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Check it satisfies the interface +var _ flagger = (*Tristate)(nil) + +func TestTristateString(t *testing.T) { + for _, test := range []struct { + in Tristate + want string + }{ + {Tristate{}, "unset"}, + {Tristate{Valid: false, Value: false}, "unset"}, + {Tristate{Valid: false, Value: true}, "unset"}, + {Tristate{Valid: true, Value: false}, "false"}, + {Tristate{Valid: true, Value: true}, "true"}, + } { + got := test.in.String() + assert.Equal(t, test.want, got) + } +} + +func TestTristateSet(t *testing.T) { + for _, test := range []struct { + in string + want Tristate + err bool + }{ + {"", Tristate{Valid: false, Value: false}, false}, + {"nil", Tristate{Valid: false, Value: false}, false}, + {"null", Tristate{Valid: false, Value: false}, false}, + {"UNSET", Tristate{Valid: false, Value: false}, false}, + {"true", Tristate{Valid: true, Value: true}, false}, + {"1", Tristate{Valid: true, Value: true}, false}, + {"false", Tristate{Valid: true, Value: false}, false}, + {"0", Tristate{Valid: true, Value: false}, false}, + {"potato", Tristate{Valid: false, Value: false}, true}, + } { + var got Tristate + err := got.Set(test.in) + if test.err { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.want, got) + } + } +} + +func TestTristateScan(t *testing.T) { + var v Tristate + n, err := fmt.Sscan(" true ", &v) + require.NoError(t, err) + assert.Equal(t, 1, n) + assert.Equal(t, Tristate{Valid: true, Value: true}, v) +} + +func TestTristateUnmarshalJSON(t *testing.T) { + for _, test := range []struct { + in string + want Tristate + err bool + }{ + {`null`, Tristate{}, false}, + {`true`, Tristate{Valid: true, Value: true}, false}, + {`false`, Tristate{Valid: true, Value: false}, false}, + {`potato`, Tristate{}, true}, + {``, Tristate{}, true}, + } { + var got Tristate + err := json.Unmarshal([]byte(test.in), &got) + if test.err { + require.Error(t, err, test.in) + } else { + require.NoError(t, err, test.in) + } + assert.Equal(t, test.want, got, test.in) + } +}