Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>main
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