From 9218a3eb00ecbe2300abf4c14525eb10f8587daa Mon Sep 17 00:00:00 2001
From: Nick Craig-Wood <nick@craig-wood.com>
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)
+	}
+}