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:
parent
c185e6b4b0
commit
a07af762e4
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue