From a07af762e4313ade39943315a01720a7d3046f01 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 27 Feb 2020 09:47:04 -0800 Subject: [PATCH] types/opt: add opt package for a new opt.Bool JSON type Signed-off-by: Brad Fitzpatrick --- types/opt/bool.go | 68 ++++++++++++++++++++++++++++++++++++++++++ types/opt/bool_test.go | 66 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 types/opt/bool.go create mode 100644 types/opt/bool_test.go diff --git a/types/opt/bool.go b/types/opt/bool.go new file mode 100644 index 000000000..adfd275c3 --- /dev/null +++ b/types/opt/bool.go @@ -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) +} diff --git a/types/opt/bool_test.go b/types/opt/bool_test.go new file mode 100644 index 000000000..ce08556c0 --- /dev/null +++ b/types/opt/bool_test.go @@ -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) + } + }) + } +}