types/opt: add opt package for a new opt.Bool JSON type

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2020-02-27 09:47:04 -08:00 committed by Brad Fitzpatrick
parent c185e6b4b0
commit a07af762e4
2 changed files with 134 additions and 0 deletions

68
types/opt/bool.go Normal file
View File

@ -0,0 +1,68 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package opt defines optional types.
package opt
import (
"fmt"
"strconv"
)
// Bool represents an optional boolean to be JSON-encoded.
// The string can be empty (for unknown or unspecified), or
// "true" or "false".
type Bool string
func (b *Bool) Set(v bool) {
*b = Bool(strconv.FormatBool(v))
}
func (b *Bool) Clear() { *b = "" }
func (b Bool) Get() (v bool, ok bool) {
if b == "" {
return
}
v, err := strconv.ParseBool(string(b))
return v, err == nil
}
var (
trueBytes = []byte("true")
falseBytes = []byte("false")
nullBytes = []byte("null")
)
func (b Bool) MarshalJSON() ([]byte, error) {
switch b {
case "true":
return trueBytes, nil
case "false":
return falseBytes, nil
case "":
return nullBytes, nil
}
return nil, fmt.Errorf("invalid opt.Bool value %q", string(b))
}
func (b *Bool) UnmarshalJSON(j []byte) error {
// Note: written with a bunch of ifs instead of a switch
// because I'm sure the Go compiler optimizes away these
// []byte->string allocations in an == comparison, but I'm too
// lazy to check whether that's true in a switch also.
if string(j) == "true" {
*b = "true"
return nil
}
if string(j) == "false" {
*b = "false"
return nil
}
if string(j) == "null" {
*b = ""
return nil
}
return fmt.Errorf("invalid opt.Bool value %q", j)
}

66
types/opt/bool_test.go Normal file
View File

@ -0,0 +1,66 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package opt
import (
"encoding/json"
"reflect"
"testing"
)
func TestBool(t *testing.T) {
tests := []struct {
name string
in interface{}
want string // JSON
}{
{
name: "null_for_unset",
in: struct {
True Bool
False Bool
Unset Bool
}{
True: "true",
False: "false",
},
want: `{"True":true,"False":false,"Unset":null}`,
},
{
name: "omitempty_unset",
in: struct {
True Bool
False Bool
Unset Bool `json:",omitempty"`
}{
True: "true",
False: "false",
},
want: `{"True":true,"False":false}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
j, err := json.Marshal(tt.in)
if err != nil {
t.Fatal(err)
}
if string(j) != tt.want {
t.Errorf("wrong JSON:\n got: %s\nwant: %s\n", j, tt.want)
}
// And back again:
newVal := reflect.New(reflect.TypeOf(tt.in))
out := newVal.Interface()
if err := json.Unmarshal(j, out); err != nil {
t.Fatalf("Unmarshal %#q: %v", j, err)
}
got := newVal.Elem().Interface()
if !reflect.DeepEqual(tt.in, got) {
t.Errorf("value mismatch\n got: %+v\nwant: %+v\n", got, tt.in)
}
})
}
}