Updates #12736 Signed-off-by: Nick Khyl <nickk@tailscale.com>main
parent
5576972261
commit
e21d8768f9
@ -0,0 +1,122 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package opt |
||||
|
||||
import ( |
||||
"fmt" |
||||
"reflect" |
||||
|
||||
jsonv2 "github.com/go-json-experiment/json" |
||||
"github.com/go-json-experiment/json/jsontext" |
||||
) |
||||
|
||||
// Value is an optional value to be JSON-encoded.
|
||||
// With [encoding/json], a zero Value is marshaled as a JSON null.
|
||||
// With [github.com/go-json-experiment/json], a zero Value is omitted from the
|
||||
// JSON object if the Go struct field specified with omitzero.
|
||||
// The omitempty tag option should never be used with Value fields.
|
||||
type Value[T any] struct { |
||||
value T |
||||
set bool |
||||
} |
||||
|
||||
// Equal reports whether the receiver and the other value are equal.
|
||||
// If the template type T in Value[T] implements an Equal method, it will be used
|
||||
// instead of the == operator for comparing values.
|
||||
type equatable[T any] interface { |
||||
// Equal reports whether the receiver and the other values are equal.
|
||||
Equal(other T) bool |
||||
} |
||||
|
||||
// ValueOf returns an optional Value containing the specified value.
|
||||
// It treats nil slices and maps as empty slices and maps.
|
||||
func ValueOf[T any](v T) Value[T] { |
||||
return Value[T]{value: v, set: true} |
||||
} |
||||
|
||||
// String implements [fmt.Stringer].
|
||||
func (o *Value[T]) String() string { |
||||
if !o.set { |
||||
return fmt.Sprintf("(empty[%T])", o.value) |
||||
} |
||||
return fmt.Sprint(o.value) |
||||
} |
||||
|
||||
// Set assigns the specified value to the optional value o.
|
||||
func (o *Value[T]) Set(v T) { |
||||
*o = ValueOf(v) |
||||
} |
||||
|
||||
// Clear resets o to an empty state.
|
||||
func (o *Value[T]) Clear() { |
||||
*o = Value[T]{} |
||||
} |
||||
|
||||
// IsSet reports whether o has a value set.
|
||||
func (o *Value[T]) IsSet() bool { |
||||
return o.set |
||||
} |
||||
|
||||
// Get returns the value of o.
|
||||
// If a value hasn't been set, a zero value of T will be returned.
|
||||
func (o Value[T]) Get() T { |
||||
return o.value |
||||
} |
||||
|
||||
// Get returns the value and a flag indicating whether the value is set.
|
||||
func (o Value[T]) GetOk() (v T, ok bool) { |
||||
return o.value, o.set |
||||
} |
||||
|
||||
// Equal reports whether o is equal to v.
|
||||
// Two optional values are equal if both are empty,
|
||||
// or if both are set and the underlying values are equal.
|
||||
// If the template type T implements an Equal(T) bool method, it will be used
|
||||
// instead of the == operator for value comparison.
|
||||
// If T is not comparable, it returns false.
|
||||
func (o Value[T]) Equal(v Value[T]) bool { |
||||
if o.set != v.set { |
||||
return false |
||||
} |
||||
if !o.set { |
||||
return true |
||||
} |
||||
ov := any(o.value) |
||||
if eq, ok := ov.(equatable[T]); ok { |
||||
return eq.Equal(v.value) |
||||
} |
||||
if reflect.TypeFor[T]().Comparable() { |
||||
return ov == any(v.value) |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (o Value[T]) MarshalJSONV2(enc *jsontext.Encoder, opts jsonv2.Options) error { |
||||
if !o.set { |
||||
return enc.WriteToken(jsontext.Null) |
||||
} |
||||
return jsonv2.MarshalEncode(enc, &o.value, opts) |
||||
} |
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (o *Value[T]) UnmarshalJSONV2(dec *jsontext.Decoder, opts jsonv2.Options) error { |
||||
if dec.PeekKind() == 'n' { |
||||
*o = Value[T]{} |
||||
_, err := dec.ReadToken() // read null
|
||||
return err |
||||
} |
||||
o.set = true |
||||
return jsonv2.UnmarshalDecode(dec, &o.value, opts) |
||||
} |
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (o Value[T]) MarshalJSON() ([]byte, error) { |
||||
return jsonv2.Marshal(o) // uses MarshalJSONV2
|
||||
} |
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (o *Value[T]) UnmarshalJSON(b []byte) error { |
||||
return jsonv2.Unmarshal(b, o) // uses UnmarshalJSONV2
|
||||
} |
||||
@ -0,0 +1,296 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package opt |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"reflect" |
||||
"testing" |
||||
|
||||
jsonv2 "github.com/go-json-experiment/json" |
||||
) |
||||
|
||||
type testStruct struct { |
||||
Int int `json:",omitempty,omitzero"` |
||||
Str string `json:",omitempty"` |
||||
} |
||||
|
||||
func TestValue(t *testing.T) { |
||||
tests := []struct { |
||||
name string |
||||
in any |
||||
jsonv2 bool |
||||
want string // JSON
|
||||
wantBack any |
||||
}{ |
||||
{ |
||||
name: "null_for_unset", |
||||
in: struct { |
||||
True Value[bool] |
||||
False Value[bool] |
||||
Unset Value[bool] |
||||
ExplicitUnset Value[bool] |
||||
}{ |
||||
True: ValueOf(true), |
||||
False: ValueOf(false), |
||||
ExplicitUnset: Value[bool]{}, |
||||
}, |
||||
want: `{"True":true,"False":false,"Unset":null,"ExplicitUnset":null}`, |
||||
wantBack: struct { |
||||
True Value[bool] |
||||
False Value[bool] |
||||
Unset Value[bool] |
||||
ExplicitUnset Value[bool] |
||||
}{ |
||||
True: ValueOf(true), |
||||
False: ValueOf(false), |
||||
Unset: Value[bool]{}, |
||||
ExplicitUnset: Value[bool]{}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "null_for_unset_jsonv2", |
||||
in: struct { |
||||
True Value[bool] |
||||
False Value[bool] |
||||
Unset Value[bool] |
||||
ExplicitUnset Value[bool] |
||||
}{ |
||||
True: ValueOf(true), |
||||
False: ValueOf(false), |
||||
ExplicitUnset: Value[bool]{}, |
||||
}, |
||||
jsonv2: true, |
||||
want: `{"True":true,"False":false,"Unset":null,"ExplicitUnset":null}`, |
||||
wantBack: struct { |
||||
True Value[bool] |
||||
False Value[bool] |
||||
Unset Value[bool] |
||||
ExplicitUnset Value[bool] |
||||
}{ |
||||
True: ValueOf(true), |
||||
False: ValueOf(false), |
||||
Unset: Value[bool]{}, |
||||
ExplicitUnset: Value[bool]{}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "null_for_unset_omitzero", |
||||
in: struct { |
||||
True Value[bool] `json:",omitzero"` |
||||
False Value[bool] `json:",omitzero"` |
||||
Unset Value[bool] `json:",omitzero"` |
||||
ExplicitUnset Value[bool] `json:",omitzero"` |
||||
}{ |
||||
True: ValueOf(true), |
||||
False: ValueOf(false), |
||||
ExplicitUnset: Value[bool]{}, |
||||
}, |
||||
want: `{"True":true,"False":false,"Unset":null,"ExplicitUnset":null}`, |
||||
wantBack: struct { |
||||
True Value[bool] `json:",omitzero"` |
||||
False Value[bool] `json:",omitzero"` |
||||
Unset Value[bool] `json:",omitzero"` |
||||
ExplicitUnset Value[bool] `json:",omitzero"` |
||||
}{ |
||||
True: ValueOf(true), |
||||
False: ValueOf(false), |
||||
Unset: Value[bool]{}, |
||||
ExplicitUnset: Value[bool]{}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "null_for_unset_omitzero_jsonv2", |
||||
in: struct { |
||||
True Value[bool] `json:",omitzero"` |
||||
False Value[bool] `json:",omitzero"` |
||||
Unset Value[bool] `json:",omitzero"` |
||||
ExplicitUnset Value[bool] `json:",omitzero"` |
||||
}{ |
||||
True: ValueOf(true), |
||||
False: ValueOf(false), |
||||
ExplicitUnset: Value[bool]{}, |
||||
}, |
||||
jsonv2: true, |
||||
want: `{"True":true,"False":false}`, |
||||
wantBack: struct { |
||||
True Value[bool] `json:",omitzero"` |
||||
False Value[bool] `json:",omitzero"` |
||||
Unset Value[bool] `json:",omitzero"` |
||||
ExplicitUnset Value[bool] `json:",omitzero"` |
||||
}{ |
||||
True: ValueOf(true), |
||||
False: ValueOf(false), |
||||
Unset: Value[bool]{}, |
||||
ExplicitUnset: Value[bool]{}, |
||||
}, |
||||
}, |
||||
{ |
||||
name: "string", |
||||
in: struct { |
||||
EmptyString Value[string] |
||||
NonEmpty Value[string] |
||||
Unset Value[string] |
||||
}{ |
||||
EmptyString: ValueOf(""), |
||||
NonEmpty: ValueOf("value"), |
||||
Unset: Value[string]{}, |
||||
}, |
||||
want: `{"EmptyString":"","NonEmpty":"value","Unset":null}`, |
||||
wantBack: struct { |
||||
EmptyString Value[string] |
||||
NonEmpty Value[string] |
||||
Unset Value[string] |
||||
}{ValueOf(""), ValueOf("value"), Value[string]{}}, |
||||
}, |
||||
{ |
||||
name: "integer", |
||||
in: struct { |
||||
Zero Value[int] |
||||
NonZero Value[int] |
||||
Unset Value[int] |
||||
}{ |
||||
Zero: ValueOf(0), |
||||
NonZero: ValueOf(42), |
||||
Unset: Value[int]{}, |
||||
}, |
||||
want: `{"Zero":0,"NonZero":42,"Unset":null}`, |
||||
wantBack: struct { |
||||
Zero Value[int] |
||||
NonZero Value[int] |
||||
Unset Value[int] |
||||
}{ValueOf(0), ValueOf(42), Value[int]{}}, |
||||
}, |
||||
{ |
||||
name: "struct", |
||||
in: struct { |
||||
Zero Value[testStruct] |
||||
NonZero Value[testStruct] |
||||
Unset Value[testStruct] |
||||
}{ |
||||
Zero: ValueOf(testStruct{}), |
||||
NonZero: ValueOf(testStruct{Int: 42, Str: "String"}), |
||||
Unset: Value[testStruct]{}, |
||||
}, |
||||
want: `{"Zero":{},"NonZero":{"Int":42,"Str":"String"},"Unset":null}`, |
||||
wantBack: struct { |
||||
Zero Value[testStruct] |
||||
NonZero Value[testStruct] |
||||
Unset Value[testStruct] |
||||
}{ValueOf(testStruct{}), ValueOf(testStruct{Int: 42, Str: "String"}), Value[testStruct]{}}, |
||||
}, |
||||
{ |
||||
name: "struct_ptr", |
||||
in: struct { |
||||
Zero Value[*testStruct] |
||||
NonZero Value[*testStruct] |
||||
Unset Value[*testStruct] |
||||
}{ |
||||
Zero: ValueOf(&testStruct{}), |
||||
NonZero: ValueOf(&testStruct{Int: 42, Str: "String"}), |
||||
Unset: Value[*testStruct]{}, |
||||
}, |
||||
want: `{"Zero":{},"NonZero":{"Int":42,"Str":"String"},"Unset":null}`, |
||||
wantBack: struct { |
||||
Zero Value[*testStruct] |
||||
NonZero Value[*testStruct] |
||||
Unset Value[*testStruct] |
||||
}{ValueOf(&testStruct{}), ValueOf(&testStruct{Int: 42, Str: "String"}), Value[*testStruct]{}}, |
||||
}, |
||||
{ |
||||
name: "nil-slice-and-map", |
||||
in: struct { |
||||
Slice Value[[]int] |
||||
Map Value[map[string]int] |
||||
}{ |
||||
Slice: ValueOf[[]int](nil), // marshalled as []
|
||||
Map: ValueOf[map[string]int](nil), // marshalled as {}
|
||||
}, |
||||
want: `{"Slice":[],"Map":{}}`, |
||||
wantBack: struct { |
||||
Slice Value[[]int] |
||||
Map Value[map[string]int] |
||||
}{ValueOf([]int{}), ValueOf(map[string]int{})}, |
||||
}, |
||||
} |
||||
for _, tt := range tests { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
var j []byte |
||||
var err error |
||||
if tt.jsonv2 { |
||||
j, err = jsonv2.Marshal(tt.in) |
||||
} else { |
||||
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) |
||||
} |
||||
|
||||
wantBack := tt.in |
||||
if tt.wantBack != nil { |
||||
wantBack = tt.wantBack |
||||
} |
||||
// And back again:
|
||||
newVal := reflect.New(reflect.TypeOf(tt.in)) |
||||
out := newVal.Interface() |
||||
if tt.jsonv2 { |
||||
err = jsonv2.Unmarshal(j, out) |
||||
} else { |
||||
err = json.Unmarshal(j, out) |
||||
} |
||||
if err != nil { |
||||
t.Fatalf("Unmarshal %#q: %v", j, err) |
||||
} |
||||
got := newVal.Elem().Interface() |
||||
if !reflect.DeepEqual(got, wantBack) { |
||||
t.Errorf("value mismatch\n got: %+v\nwant: %+v\n", got, wantBack) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestValueEqual(t *testing.T) { |
||||
tests := []struct { |
||||
o Value[bool] |
||||
v Value[bool] |
||||
want bool |
||||
}{ |
||||
{ValueOf(true), ValueOf(true), true}, |
||||
{ValueOf(true), ValueOf(false), false}, |
||||
{ValueOf(true), Value[bool]{}, false}, |
||||
{ValueOf(false), ValueOf(false), true}, |
||||
{ValueOf(false), ValueOf(true), false}, |
||||
{ValueOf(false), Value[bool]{}, false}, |
||||
{Value[bool]{}, Value[bool]{}, true}, |
||||
{Value[bool]{}, ValueOf(true), false}, |
||||
{Value[bool]{}, ValueOf(false), false}, |
||||
} |
||||
for _, tt := range tests { |
||||
if got := tt.o.Equal(tt.v); got != tt.want { |
||||
t.Errorf("(%v).Equals(%v) = %v; want %v", tt.o, tt.v, got, tt.want) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func TestIncomparableValueEqual(t *testing.T) { |
||||
tests := []struct { |
||||
o Value[[]bool] |
||||
v Value[[]bool] |
||||
want bool |
||||
}{ |
||||
{ValueOf([]bool{}), ValueOf([]bool{}), false}, |
||||
{ValueOf([]bool{true}), ValueOf([]bool{true}), false}, |
||||
{Value[[]bool]{}, ValueOf([]bool{}), false}, |
||||
{ValueOf([]bool{}), Value[[]bool]{}, false}, |
||||
{Value[[]bool]{}, Value[[]bool]{}, true}, |
||||
} |
||||
for _, tt := range tests { |
||||
if got := tt.o.Equal(tt.v); got != tt.want { |
||||
t.Errorf("(%v).Equals(%v) = %v; want %v", tt.o, tt.v, got, tt.want) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue