mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
types/prefs: add a package containing generic preference types
This adds a new package containing generic types to be used for defining preference hierarchies. These include prefs.Item, prefs.List, prefs.StructList, and prefs.StructMap. Each of these types represents a configurable preference, holding the preference's state, value, and metadata. The metadata includes the default value (if it differs from the zero value of the Go type) and flags indicating whether a preference is managed via syspolicy or is hidden/read-only for another reason. This information can be marshaled and sent to the GUI, CLI and web clients as a source of truth regarding preference configuration, management, and visibility/mutability states. We plan to use these types to define device preferences, such as the updater preferences, the permission mode to be used on Windows with #tailscale/corp#18342, and certain global options that are currently exposed as tailscaled flags. We also aim to eventually use these types for profile-local preferences in ipn.Prefs and and as a replacement for ipn.MaskedPrefs. The generic preference types are compatible with the tailscale.com/cmd/viewer and tailscale.com/cmd/cloner utilities. Updates #12736 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
151b77f9d6
commit
af3d3c433b
178
types/prefs/item.go
Normal file
178
types/prefs/item.go
Normal file
@ -0,0 +1,178 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package prefs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
// Item is a single preference item that can be configured.
|
||||
// T must either be an immutable type or implement the [views.ViewCloner] interface.
|
||||
type Item[T any] struct {
|
||||
preference[T]
|
||||
}
|
||||
|
||||
// ItemOf returns an [Item] configured with the specified value and [Options].
|
||||
func ItemOf[T any](v T, opts ...Options) Item[T] {
|
||||
return Item[T]{preferenceOf(opt.ValueOf(must.Get(deepClone(v))), opts...)}
|
||||
}
|
||||
|
||||
// ItemWithOpts returns an unconfigured [Item] with the specified [Options].
|
||||
func ItemWithOpts[T any](opts ...Options) Item[T] {
|
||||
return Item[T]{preferenceOf(opt.Value[T]{}, opts...)}
|
||||
}
|
||||
|
||||
// SetValue configures the preference with the specified value.
|
||||
// It fails and returns [ErrManaged] if p is a managed preference,
|
||||
// and [ErrReadOnly] if p is a read-only preference.
|
||||
func (i *Item[T]) SetValue(val T) error {
|
||||
return i.preference.SetValue(must.Get(deepClone(val)))
|
||||
}
|
||||
|
||||
// SetManagedValue configures the preference with the specified value
|
||||
// and marks the preference as managed.
|
||||
func (i *Item[T]) SetManagedValue(val T) {
|
||||
i.preference.SetManagedValue(must.Get(deepClone(val)))
|
||||
}
|
||||
|
||||
// Clone returns a copy of i that aliases no memory with i.
|
||||
// It is a runtime error to call [Item.Clone] if T contains pointers
|
||||
// but does not implement [views.Cloner].
|
||||
func (i Item[T]) Clone() *Item[T] {
|
||||
res := ptr.To(i)
|
||||
if v, ok := i.ValueOk(); ok {
|
||||
res.s.Value.Set(must.Get(deepClone(v)))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Equal reports whether i and i2 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 reports false.
|
||||
func (i Item[T]) Equal(i2 Item[T]) bool {
|
||||
if i.s.Metadata != i2.s.Metadata {
|
||||
return false
|
||||
}
|
||||
return i.s.Value.Equal(i2.s.Value)
|
||||
}
|
||||
|
||||
func deepClone[T any](v T) (T, error) {
|
||||
if c, ok := any(v).(views.Cloner[T]); ok {
|
||||
return c.Clone(), nil
|
||||
}
|
||||
if !views.ContainsPointers[T]() {
|
||||
return v, nil
|
||||
}
|
||||
var zero T
|
||||
return zero, fmt.Errorf("%T contains pointers, but does not implement Clone", v)
|
||||
}
|
||||
|
||||
// ItemView is a read-only view of an [Item][T], where T is a mutable type
|
||||
// implementing [views.ViewCloner].
|
||||
type ItemView[T views.ViewCloner[T, V], V views.StructView[T]] struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Item[T]
|
||||
}
|
||||
|
||||
// ItemViewOf returns a read-only view of i.
|
||||
// It is used by [tailscale.com/cmd/viewer].
|
||||
func ItemViewOf[T views.ViewCloner[T, V], V views.StructView[T]](i *Item[T]) ItemView[T, V] {
|
||||
return ItemView[T, V]{i}
|
||||
}
|
||||
|
||||
// Valid reports whether the underlying [Item] is non-nil.
|
||||
func (iv ItemView[T, V]) Valid() bool {
|
||||
return iv.ж != nil
|
||||
}
|
||||
|
||||
// AsStruct implements [views.StructView] by returning a clone of the preference
|
||||
// which aliases no memory with the original.
|
||||
func (iv ItemView[T, V]) AsStruct() *Item[T] {
|
||||
if iv.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return iv.ж.Clone()
|
||||
}
|
||||
|
||||
// IsSet reports whether the preference has a value set.
|
||||
func (iv ItemView[T, V]) IsSet() bool {
|
||||
return iv.ж.IsSet()
|
||||
}
|
||||
|
||||
// Value returns a read-only view of the value if the preference has a value set.
|
||||
// Otherwise, it returns a read-only view of its default value.
|
||||
func (iv ItemView[T, V]) Value() V {
|
||||
return iv.ж.Value().View()
|
||||
}
|
||||
|
||||
// ValueOk returns a read-only view of the value and true if the preference has a value set.
|
||||
// Otherwise, it returns an invalid view and false.
|
||||
func (iv ItemView[T, V]) ValueOk() (val V, ok bool) {
|
||||
if val, ok := iv.ж.ValueOk(); ok {
|
||||
return val.View(), true
|
||||
}
|
||||
return val, false
|
||||
}
|
||||
|
||||
// DefaultValue returns a read-only view of the default value of the preference.
|
||||
func (iv ItemView[T, V]) DefaultValue() V {
|
||||
return iv.ж.DefaultValue().View()
|
||||
}
|
||||
|
||||
// IsManaged reports whether the preference is managed via MDM, Group Policy, or similar means.
|
||||
func (iv ItemView[T, V]) IsManaged() bool {
|
||||
return iv.ж.IsManaged()
|
||||
}
|
||||
|
||||
// IsReadOnly reports whether the preference is read-only and cannot be changed by user.
|
||||
func (iv ItemView[T, V]) IsReadOnly() bool {
|
||||
return iv.ж.IsReadOnly()
|
||||
}
|
||||
|
||||
// Equal reports whether iv and iv2 are equal.
|
||||
func (iv ItemView[T, V]) Equal(iv2 ItemView[T, V]) bool {
|
||||
if !iv.Valid() && !iv2.Valid() {
|
||||
return true
|
||||
}
|
||||
if iv.Valid() != iv2.Valid() {
|
||||
return false
|
||||
}
|
||||
return iv.ж.Equal(*iv2.ж)
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (iv ItemView[T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
return iv.ж.MarshalJSONV2(out, opts)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (iv *ItemView[T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
var x Item[T]
|
||||
if err := x.UnmarshalJSONV2(in, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
iv.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (iv ItemView[T, V]) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(iv) // uses MarshalJSONV2
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (iv *ItemView[T, V]) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, iv) // uses UnmarshalJSONV2
|
||||
}
|
183
types/prefs/list.go
Normal file
183
types/prefs/list.go
Normal file
@ -0,0 +1,183 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package prefs
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"golang.org/x/exp/constraints"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
// BasicType is a constraint that allows types whose underlying type is a predeclared
|
||||
// boolean, numeric, or string type.
|
||||
type BasicType interface {
|
||||
~bool | constraints.Integer | constraints.Float | constraints.Complex | ~string
|
||||
}
|
||||
|
||||
// ImmutableType is a constraint that allows [BasicType]s and certain well-known immutable types.
|
||||
type ImmutableType interface {
|
||||
BasicType | time.Time | netip.Addr | netip.Prefix | netip.AddrPort
|
||||
}
|
||||
|
||||
// List is a preference type that holds zero or more values of an [ImmutableType] T.
|
||||
type List[T ImmutableType] struct {
|
||||
preference[[]T]
|
||||
}
|
||||
|
||||
// ListOf returns a [List] configured with the specified value and [Options].
|
||||
func ListOf[T ImmutableType](v []T, opts ...Options) List[T] {
|
||||
return List[T]{preferenceOf(opt.ValueOf(cloneSlice(v)), opts...)}
|
||||
}
|
||||
|
||||
// ListWithOpts returns an unconfigured [List] with the specified [Options].
|
||||
func ListWithOpts[T ImmutableType](opts ...Options) List[T] {
|
||||
return List[T]{preferenceOf(opt.Value[[]T]{}, opts...)}
|
||||
}
|
||||
|
||||
// SetValue configures the preference with the specified value.
|
||||
// It fails and returns [ErrManaged] if p is a managed preference,
|
||||
// and [ErrReadOnly] if p is a read-only preference.
|
||||
func (l *List[T]) SetValue(val []T) error {
|
||||
return l.preference.SetValue(cloneSlice(val))
|
||||
}
|
||||
|
||||
// SetManagedValue configures the preference with the specified value
|
||||
// and marks the preference as managed.
|
||||
func (l *List[T]) SetManagedValue(val []T) {
|
||||
l.preference.SetManagedValue(cloneSlice(val))
|
||||
}
|
||||
|
||||
// View returns a read-only view of l.
|
||||
func (l *List[T]) View() ListView[T] {
|
||||
return ListView[T]{l}
|
||||
}
|
||||
|
||||
// Clone returns a copy of l that aliases no memory with l.
|
||||
func (l List[T]) Clone() *List[T] {
|
||||
res := ptr.To(l)
|
||||
if v, ok := l.s.Value.GetOk(); ok {
|
||||
res.s.Value.Set(append(v[:0:0], v...))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Equal reports whether l and l2 are equal.
|
||||
func (l List[T]) Equal(l2 List[T]) bool {
|
||||
if l.s.Metadata != l2.s.Metadata {
|
||||
return false
|
||||
}
|
||||
v1, ok1 := l.s.Value.GetOk()
|
||||
v2, ok2 := l2.s.Value.GetOk()
|
||||
if ok1 != ok2 {
|
||||
return false
|
||||
}
|
||||
return !ok1 || slices.Equal(v1, v2)
|
||||
}
|
||||
|
||||
func cloneSlice[T ImmutableType](s []T) []T {
|
||||
c := make([]T, len(s))
|
||||
copy(c, s)
|
||||
return c
|
||||
}
|
||||
|
||||
// ListView is a read-only view of a [List].
|
||||
type ListView[T ImmutableType] struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *List[T]
|
||||
}
|
||||
|
||||
// Valid reports whether the underlying [List] is non-nil.
|
||||
func (lv ListView[T]) Valid() bool {
|
||||
return lv.ж != nil
|
||||
}
|
||||
|
||||
// AsStruct implements [views.StructView] by returning a clone of the [List]
|
||||
// which aliases no memory with the original.
|
||||
func (lv ListView[T]) AsStruct() *List[T] {
|
||||
if lv.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return lv.ж.Clone()
|
||||
}
|
||||
|
||||
// IsSet reports whether the preference has a value set.
|
||||
func (lv ListView[T]) IsSet() bool {
|
||||
return lv.ж.IsSet()
|
||||
}
|
||||
|
||||
// Value returns a read-only view of the value if the preference has a value set.
|
||||
// Otherwise, it returns a read-only view of its default value.
|
||||
func (lv ListView[T]) Value() views.Slice[T] {
|
||||
return views.SliceOf(lv.ж.Value())
|
||||
}
|
||||
|
||||
// ValueOk returns a read-only view of the value and true if the preference has a value set.
|
||||
// Otherwise, it returns an invalid view and false.
|
||||
func (lv ListView[T]) ValueOk() (val views.Slice[T], ok bool) {
|
||||
if v, ok := lv.ж.ValueOk(); ok {
|
||||
return views.SliceOf(v), true
|
||||
}
|
||||
return views.Slice[T]{}, false
|
||||
}
|
||||
|
||||
// DefaultValue returns a read-only view of the default value of the preference.
|
||||
func (lv ListView[T]) DefaultValue() views.Slice[T] {
|
||||
return views.SliceOf(lv.ж.DefaultValue())
|
||||
}
|
||||
|
||||
// IsManaged reports whether the preference is managed via MDM, Group Policy, or similar means.
|
||||
func (lv ListView[T]) IsManaged() bool {
|
||||
return lv.ж.IsManaged()
|
||||
}
|
||||
|
||||
// IsReadOnly reports whether the preference is read-only and cannot be changed by user.
|
||||
func (lv ListView[T]) IsReadOnly() bool {
|
||||
return lv.ж.IsReadOnly()
|
||||
}
|
||||
|
||||
// Equal reports whether lv and lv2 are equal.
|
||||
func (lv ListView[T]) Equal(lv2 ListView[T]) bool {
|
||||
if !lv.Valid() && !lv2.Valid() {
|
||||
return true
|
||||
}
|
||||
if lv.Valid() != lv2.Valid() {
|
||||
return false
|
||||
}
|
||||
return lv.ж.Equal(*lv2.ж)
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (lv ListView[T]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
return lv.ж.MarshalJSONV2(out, opts)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (lv *ListView[T]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
var x List[T]
|
||||
if err := x.UnmarshalJSONV2(in, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
lv.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (lv ListView[T]) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(lv) // uses MarshalJSONV2
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (lv *ListView[T]) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, lv) // uses UnmarshalJSONV2
|
||||
}
|
159
types/prefs/map.go
Normal file
159
types/prefs/map.go
Normal file
@ -0,0 +1,159 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package prefs
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"net/netip"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"golang.org/x/exp/constraints"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
// MapKeyType is a constraint allowing types that can be used as [Map] and [StructMap] keys.
|
||||
// To satisfy this requirement, a type must be comparable and must encode as a JSON string.
|
||||
// See [jsonv2.Marshal] for more details.
|
||||
type MapKeyType interface {
|
||||
~string | constraints.Integer | netip.Addr | netip.Prefix | netip.AddrPort
|
||||
}
|
||||
|
||||
// Map is a preference type that holds immutable key-value pairs.
|
||||
type Map[K MapKeyType, V ImmutableType] struct {
|
||||
preference[map[K]V]
|
||||
}
|
||||
|
||||
// MapOf returns a map configured with the specified value and [Options].
|
||||
func MapOf[K MapKeyType, V ImmutableType](v map[K]V, opts ...Options) Map[K, V] {
|
||||
return Map[K, V]{preferenceOf(opt.ValueOf(v), opts...)}
|
||||
}
|
||||
|
||||
// MapWithOpts returns an unconfigured [Map] with the specified [Options].
|
||||
func MapWithOpts[K MapKeyType, V ImmutableType](opts ...Options) Map[K, V] {
|
||||
return Map[K, V]{preferenceOf(opt.Value[map[K]V]{}, opts...)}
|
||||
}
|
||||
|
||||
// View returns a read-only view of m.
|
||||
func (m *Map[K, V]) View() MapView[K, V] {
|
||||
return MapView[K, V]{m}
|
||||
}
|
||||
|
||||
// Clone returns a copy of m that aliases no memory with m.
|
||||
func (m Map[K, V]) Clone() *Map[K, V] {
|
||||
res := ptr.To(m)
|
||||
if v, ok := m.s.Value.GetOk(); ok {
|
||||
res.s.Value.Set(maps.Clone(v))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Equal reports whether m and m2 are equal.
|
||||
func (m Map[K, V]) Equal(m2 Map[K, V]) bool {
|
||||
if m.s.Metadata != m2.s.Metadata {
|
||||
return false
|
||||
}
|
||||
v1, ok1 := m.s.Value.GetOk()
|
||||
v2, ok2 := m2.s.Value.GetOk()
|
||||
if ok1 != ok2 {
|
||||
return false
|
||||
}
|
||||
return !ok1 || maps.Equal(v1, v2)
|
||||
}
|
||||
|
||||
// MapView is a read-only view of a [Map].
|
||||
type MapView[K MapKeyType, V ImmutableType] struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Map[K, V]
|
||||
}
|
||||
|
||||
// Valid reports whether the underlying [Map] is non-nil.
|
||||
func (mv MapView[K, V]) Valid() bool {
|
||||
return mv.ж != nil
|
||||
}
|
||||
|
||||
// AsStruct implements [views.StructView] by returning a clone of the [Map]
|
||||
// which aliases no memory with the original.
|
||||
func (mv MapView[K, V]) AsStruct() *Map[K, V] {
|
||||
if mv.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return mv.ж.Clone()
|
||||
}
|
||||
|
||||
// IsSet reports whether the preference has a value set.
|
||||
func (mv MapView[K, V]) IsSet() bool {
|
||||
return mv.ж.IsSet()
|
||||
}
|
||||
|
||||
// Value returns a read-only view of the value if the preference has a value set.
|
||||
// Otherwise, it returns a read-only view of its default value.
|
||||
func (mv MapView[K, V]) Value() views.Map[K, V] {
|
||||
return views.MapOf(mv.ж.Value())
|
||||
}
|
||||
|
||||
// ValueOk returns a read-only view of the value and true if the preference has a value set.
|
||||
// Otherwise, it returns an invalid view and false.
|
||||
func (mv MapView[K, V]) ValueOk() (val views.Map[K, V], ok bool) {
|
||||
if v, ok := mv.ж.ValueOk(); ok {
|
||||
return views.MapOf(v), true
|
||||
}
|
||||
return views.Map[K, V]{}, false
|
||||
}
|
||||
|
||||
// DefaultValue returns a read-only view of the default value of the preference.
|
||||
func (mv MapView[K, V]) DefaultValue() views.Map[K, V] {
|
||||
return views.MapOf(mv.ж.DefaultValue())
|
||||
}
|
||||
|
||||
// Managed reports whether the preference is managed via MDM, Group Policy, or similar means.
|
||||
func (mv MapView[K, V]) Managed() bool {
|
||||
return mv.ж.IsManaged()
|
||||
}
|
||||
|
||||
// ReadOnly reports whether the preference is read-only and cannot be changed by user.
|
||||
func (mv MapView[K, V]) ReadOnly() bool {
|
||||
return mv.ж.IsReadOnly()
|
||||
}
|
||||
|
||||
// Equal reports whether mv and mv2 are equal.
|
||||
func (mv MapView[K, V]) Equal(mv2 MapView[K, V]) bool {
|
||||
if !mv.Valid() && !mv2.Valid() {
|
||||
return true
|
||||
}
|
||||
if mv.Valid() != mv2.Valid() {
|
||||
return false
|
||||
}
|
||||
return mv.ж.Equal(*mv2.ж)
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (mv MapView[K, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
return mv.ж.MarshalJSONV2(out, opts)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (mv *MapView[K, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
var x Map[K, V]
|
||||
if err := x.UnmarshalJSONV2(in, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
mv.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (mv MapView[K, V]) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(mv) // uses MarshalJSONV2
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (mv *MapView[K, V]) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, mv) // uses UnmarshalJSONV2
|
||||
}
|
22
types/prefs/options.go
Normal file
22
types/prefs/options.go
Normal file
@ -0,0 +1,22 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package prefs
|
||||
|
||||
// Options are used to configure additional parameters of a preference.
|
||||
type Options func(s *metadata)
|
||||
|
||||
var (
|
||||
// ReadOnly is an option that marks preference as read-only.
|
||||
ReadOnly Options = markReadOnly
|
||||
// Managed is an option that marks preference as managed.
|
||||
Managed Options = markManaged
|
||||
)
|
||||
|
||||
func markReadOnly(s *metadata) {
|
||||
s.ReadOnly = true
|
||||
}
|
||||
|
||||
func markManaged(s *metadata) {
|
||||
s.Managed = true
|
||||
}
|
179
types/prefs/prefs.go
Normal file
179
types/prefs/prefs.go
Normal file
@ -0,0 +1,179 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package prefs contains types and functions to work with arbitrary
|
||||
// preference hierarchies.
|
||||
//
|
||||
// Specifically, the package provides [Item], [List], [Map], [StructList] and [StructMap]
|
||||
// types which represent individual preferences in a user-defined prefs struct.
|
||||
// A valid prefs struct must contain one or more exported fields of the preference types,
|
||||
// either directly or within nested structs, but not pointers to these types.
|
||||
// Additionally to preferences, a prefs struct may contain any number of
|
||||
// non-preference fields that will be marshalled and unmarshalled but are
|
||||
// otherwise ignored by the prefs package.
|
||||
//
|
||||
// The preference types are compatible with the [tailscale.com/cmd/viewer] and
|
||||
// [tailscale.com/cmd/cloner] utilities. It is recommended to generate a read-only view
|
||||
// of the user-defined prefs structure and use it in place of prefs whenever the prefs
|
||||
// should not be modified.
|
||||
package prefs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrManaged is the error returned when attempting to modify a managed preference.
|
||||
ErrManaged = errors.New("cannot modify a managed preference")
|
||||
// ErrReadOnly is the error returned when attempting to modify a readonly preference.
|
||||
ErrReadOnly = errors.New("cannot modify a readonly preference")
|
||||
)
|
||||
|
||||
// metadata holds type-agnostic preference metadata.
|
||||
type metadata struct {
|
||||
// Managed indicates whether the preference is managed via MDM, Group Policy, or other means.
|
||||
Managed bool `json:",omitzero"`
|
||||
|
||||
// ReadOnly indicates whether the preference is read-only due to any other reasons,
|
||||
// such as user's access rights.
|
||||
ReadOnly bool `json:",omitzero"`
|
||||
}
|
||||
|
||||
// serializable is a JSON-serializable preference data.
|
||||
type serializable[T any] struct {
|
||||
// Value is an optional preference value that is set when the preference is
|
||||
// configured by the user or managed by an admin.
|
||||
Value opt.Value[T] `json:",omitzero"`
|
||||
// Default is the default preference value to be used
|
||||
// when the preference has not been configured.
|
||||
Default T `json:",omitzero"`
|
||||
// Metadata is any additional type-agnostic preference metadata to be serialized.
|
||||
Metadata metadata `json:",inline"`
|
||||
}
|
||||
|
||||
// preference is an embeddable type that provides a common implementation for
|
||||
// concrete preference types, such as [Item], [List], [Map], [StructList] and [StructMap].
|
||||
type preference[T any] struct {
|
||||
s serializable[T]
|
||||
}
|
||||
|
||||
// preferenceOf returns a preference with the specified value and/or [Options].
|
||||
func preferenceOf[T any](v opt.Value[T], opts ...Options) preference[T] {
|
||||
var m metadata
|
||||
for _, o := range opts {
|
||||
o(&m)
|
||||
}
|
||||
return preference[T]{serializable[T]{Value: v, Metadata: m}}
|
||||
}
|
||||
|
||||
// IsSet reports whether p has a value set.
|
||||
func (p preference[T]) IsSet() bool {
|
||||
return p.s.Value.IsSet()
|
||||
}
|
||||
|
||||
// Value returns the value of p if the preference has a value set.
|
||||
// Otherwise, it returns its default value.
|
||||
func (p preference[T]) Value() T {
|
||||
val, _ := p.ValueOk()
|
||||
return val
|
||||
}
|
||||
|
||||
// ValueOk returns the value of p and true if the preference has a value set.
|
||||
// Otherwise, it returns its default value and false.
|
||||
func (p preference[T]) ValueOk() (val T, ok bool) {
|
||||
if val, ok = p.s.Value.GetOk(); ok {
|
||||
return val, true
|
||||
}
|
||||
return p.DefaultValue(), false
|
||||
}
|
||||
|
||||
// SetValue configures the preference with the specified value.
|
||||
// It fails and returns [ErrManaged] if p is a managed preference,
|
||||
// and [ErrReadOnly] if p is a read-only preference.
|
||||
func (p *preference[T]) SetValue(val T) error {
|
||||
switch {
|
||||
case p.s.Metadata.Managed:
|
||||
return ErrManaged
|
||||
case p.s.Metadata.ReadOnly:
|
||||
return ErrReadOnly
|
||||
default:
|
||||
p.s.Value.Set(val)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ClearValue resets the preference to an unconfigured state.
|
||||
// It fails and returns [ErrManaged] if p is a managed preference,
|
||||
// and [ErrReadOnly] if p is a read-only preference.
|
||||
func (p *preference[T]) ClearValue() error {
|
||||
switch {
|
||||
case p.s.Metadata.Managed:
|
||||
return ErrManaged
|
||||
case p.s.Metadata.ReadOnly:
|
||||
return ErrReadOnly
|
||||
default:
|
||||
p.s.Value.Clear()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultValue returns the default value of p.
|
||||
func (p preference[T]) DefaultValue() T {
|
||||
return p.s.Default
|
||||
}
|
||||
|
||||
// SetDefaultValue sets the default value of p.
|
||||
func (p *preference[T]) SetDefaultValue(def T) {
|
||||
p.s.Default = def
|
||||
}
|
||||
|
||||
// IsManaged reports whether p is managed via MDM, Group Policy, or similar means.
|
||||
func (p preference[T]) IsManaged() bool {
|
||||
return p.s.Metadata.Managed
|
||||
}
|
||||
|
||||
// SetManagedValue configures the preference with the specified value
|
||||
// and marks the preference as managed.
|
||||
func (p *preference[T]) SetManagedValue(val T) {
|
||||
p.s.Value.Set(val)
|
||||
p.s.Metadata.Managed = true
|
||||
}
|
||||
|
||||
// ClearManaged clears the managed flag of the preference without altering its value.
|
||||
func (p *preference[T]) ClearManaged() {
|
||||
p.s.Metadata.Managed = false
|
||||
}
|
||||
|
||||
// IsReadOnly reports whether p is read-only and cannot be changed by user.
|
||||
func (p preference[T]) IsReadOnly() bool {
|
||||
return p.s.Metadata.ReadOnly || p.s.Metadata.Managed
|
||||
}
|
||||
|
||||
// SetReadOnly sets the read-only status of p, preventing changes by a user if set to true.
|
||||
func (p *preference[T]) SetReadOnly(readonly bool) {
|
||||
p.s.Metadata.ReadOnly = readonly
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (p preference[T]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
return jsonv2.MarshalEncode(out, &p.s, opts)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (p *preference[T]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
return jsonv2.UnmarshalDecode(in, &p.s, opts)
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (p preference[T]) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(p) // uses MarshalJSONV2
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (p *preference[T]) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
|
||||
}
|
130
types/prefs/prefs_clone_test.go
Normal file
130
types/prefs/prefs_clone_test.go
Normal file
@ -0,0 +1,130 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
|
||||
package prefs
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of TestPrefs.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *TestPrefs) Clone() *TestPrefs {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(TestPrefs)
|
||||
*dst = *src
|
||||
dst.StringSlice = *src.StringSlice.Clone()
|
||||
dst.IntSlice = *src.IntSlice.Clone()
|
||||
dst.StringStringMap = *src.StringStringMap.Clone()
|
||||
dst.IntStringMap = *src.IntStringMap.Clone()
|
||||
dst.AddrIntMap = *src.AddrIntMap.Clone()
|
||||
dst.Bundle1 = *src.Bundle1.Clone()
|
||||
dst.Bundle2 = *src.Bundle2.Clone()
|
||||
dst.Generic = *src.Generic.Clone()
|
||||
dst.BundleList = *src.BundleList.Clone()
|
||||
dst.StringBundleMap = *src.StringBundleMap.Clone()
|
||||
dst.IntBundleMap = *src.IntBundleMap.Clone()
|
||||
dst.AddrBundleMap = *src.AddrBundleMap.Clone()
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TestPrefsCloneNeedsRegeneration = TestPrefs(struct {
|
||||
Int32Item Item[int32]
|
||||
UInt64Item Item[uint64]
|
||||
StringItem1 Item[string]
|
||||
StringItem2 Item[string]
|
||||
BoolItem1 Item[bool]
|
||||
BoolItem2 Item[bool]
|
||||
StringSlice List[string]
|
||||
IntSlice List[int]
|
||||
AddrItem Item[netip.Addr]
|
||||
StringStringMap Map[string, string]
|
||||
IntStringMap Map[int, string]
|
||||
AddrIntMap Map[netip.Addr, int]
|
||||
Bundle1 Item[*TestBundle]
|
||||
Bundle2 Item[*TestBundle]
|
||||
Generic Item[*TestGenericStruct[int]]
|
||||
BundleList StructList[*TestBundle]
|
||||
StringBundleMap StructMap[string, *TestBundle]
|
||||
IntBundleMap StructMap[int, *TestBundle]
|
||||
AddrBundleMap StructMap[netip.Addr, *TestBundle]
|
||||
Group TestPrefsGroup
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of TestBundle.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *TestBundle) Clone() *TestBundle {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(TestBundle)
|
||||
*dst = *src
|
||||
if dst.Nested != nil {
|
||||
dst.Nested = ptr.To(*src.Nested)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TestBundleCloneNeedsRegeneration = TestBundle(struct {
|
||||
Name string
|
||||
Nested *TestValueStruct
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of TestValueStruct.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *TestValueStruct) Clone() *TestValueStruct {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(TestValueStruct)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TestValueStructCloneNeedsRegeneration = TestValueStruct(struct {
|
||||
Value int
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of TestGenericStruct.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *TestGenericStruct[T]) Clone() *TestGenericStruct[T] {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(TestGenericStruct[T])
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
func _TestGenericStructCloneNeedsRegeneration[T ImmutableType](TestGenericStruct[T]) {
|
||||
_TestGenericStructCloneNeedsRegeneration(struct {
|
||||
Value T
|
||||
}{})
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of TestPrefsGroup.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *TestPrefsGroup) Clone() *TestPrefsGroup {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(TestPrefsGroup)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TestPrefsGroupCloneNeedsRegeneration = TestPrefsGroup(struct {
|
||||
FloatItem Item[float64]
|
||||
TestStringItem Item[TestStringType]
|
||||
}{})
|
99
types/prefs/prefs_example/prefs_example_clone.go
Normal file
99
types/prefs/prefs_example/prefs_example_clone.go
Normal file
@ -0,0 +1,99 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
|
||||
|
||||
package prefs_example
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/prefs"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Prefs.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Prefs) Clone() *Prefs {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Prefs)
|
||||
*dst = *src
|
||||
dst.AdvertiseTags = *src.AdvertiseTags.Clone()
|
||||
dst.AdvertiseRoutes = *src.AdvertiseRoutes.Clone()
|
||||
dst.DriveShares = *src.DriveShares.Clone()
|
||||
dst.Persist = src.Persist.Clone()
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
ControlURL prefs.Item[string]
|
||||
RouteAll prefs.Item[bool]
|
||||
ExitNodeID prefs.Item[tailcfg.StableNodeID]
|
||||
ExitNodeIP prefs.Item[netip.Addr]
|
||||
ExitNodePrior tailcfg.StableNodeID
|
||||
ExitNodeAllowLANAccess prefs.Item[bool]
|
||||
CorpDNS prefs.Item[bool]
|
||||
RunSSH prefs.Item[bool]
|
||||
RunWebClient prefs.Item[bool]
|
||||
WantRunning prefs.Item[bool]
|
||||
LoggedOut prefs.Item[bool]
|
||||
ShieldsUp prefs.Item[bool]
|
||||
AdvertiseTags prefs.List[string]
|
||||
Hostname prefs.Item[string]
|
||||
NotepadURLs prefs.Item[bool]
|
||||
ForceDaemon prefs.Item[bool]
|
||||
Egg prefs.Item[bool]
|
||||
AdvertiseRoutes prefs.List[netip.Prefix]
|
||||
NoSNAT prefs.Item[bool]
|
||||
NoStatefulFiltering prefs.Item[opt.Bool]
|
||||
NetfilterMode prefs.Item[preftype.NetfilterMode]
|
||||
OperatorUser prefs.Item[string]
|
||||
ProfileName prefs.Item[string]
|
||||
AutoUpdate AutoUpdatePrefs
|
||||
AppConnector AppConnectorPrefs
|
||||
PostureChecking prefs.Item[bool]
|
||||
NetfilterKind prefs.Item[string]
|
||||
DriveShares prefs.StructList[*drive.Share]
|
||||
AllowSingleHosts prefs.Item[marshalAsTrueInJSON]
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of AutoUpdatePrefs.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *AutoUpdatePrefs) Clone() *AutoUpdatePrefs {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(AutoUpdatePrefs)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _AutoUpdatePrefsCloneNeedsRegeneration = AutoUpdatePrefs(struct {
|
||||
Check prefs.Item[bool]
|
||||
Apply prefs.Item[opt.Bool]
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of AppConnectorPrefs.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *AppConnectorPrefs) Clone() *AppConnectorPrefs {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(AppConnectorPrefs)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _AppConnectorPrefsCloneNeedsRegeneration = AppConnectorPrefs(struct {
|
||||
Advertise prefs.Item[bool]
|
||||
}{})
|
239
types/prefs/prefs_example/prefs_example_view.go
Normal file
239
types/prefs/prefs_example/prefs_example_view.go
Normal file
@ -0,0 +1,239 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
|
||||
|
||||
package prefs_example
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/prefs"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Prefs,AutoUpdatePrefs,AppConnectorPrefs
|
||||
|
||||
// View returns a readonly view of Prefs.
|
||||
func (p *Prefs) View() PrefsView {
|
||||
return PrefsView{ж: p}
|
||||
}
|
||||
|
||||
// PrefsView provides a read-only view over Prefs.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type PrefsView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Prefs
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v PrefsView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v PrefsView) AsStruct() *Prefs {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v PrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *PrefsView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x Prefs
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v PrefsView) ControlURL() prefs.Item[string] { return v.ж.ControlURL }
|
||||
func (v PrefsView) RouteAll() prefs.Item[bool] { return v.ж.RouteAll }
|
||||
func (v PrefsView) ExitNodeID() prefs.Item[tailcfg.StableNodeID] { return v.ж.ExitNodeID }
|
||||
func (v PrefsView) ExitNodeIP() prefs.Item[netip.Addr] { return v.ж.ExitNodeIP }
|
||||
func (v PrefsView) ExitNodePrior() tailcfg.StableNodeID { return v.ж.ExitNodePrior }
|
||||
func (v PrefsView) ExitNodeAllowLANAccess() prefs.Item[bool] { return v.ж.ExitNodeAllowLANAccess }
|
||||
func (v PrefsView) CorpDNS() prefs.Item[bool] { return v.ж.CorpDNS }
|
||||
func (v PrefsView) RunSSH() prefs.Item[bool] { return v.ж.RunSSH }
|
||||
func (v PrefsView) RunWebClient() prefs.Item[bool] { return v.ж.RunWebClient }
|
||||
func (v PrefsView) WantRunning() prefs.Item[bool] { return v.ж.WantRunning }
|
||||
func (v PrefsView) LoggedOut() prefs.Item[bool] { return v.ж.LoggedOut }
|
||||
func (v PrefsView) ShieldsUp() prefs.Item[bool] { return v.ж.ShieldsUp }
|
||||
func (v PrefsView) AdvertiseTags() prefs.ListView[string] { return v.ж.AdvertiseTags.View() }
|
||||
func (v PrefsView) Hostname() prefs.Item[string] { return v.ж.Hostname }
|
||||
func (v PrefsView) NotepadURLs() prefs.Item[bool] { return v.ж.NotepadURLs }
|
||||
func (v PrefsView) ForceDaemon() prefs.Item[bool] { return v.ж.ForceDaemon }
|
||||
func (v PrefsView) Egg() prefs.Item[bool] { return v.ж.Egg }
|
||||
func (v PrefsView) AdvertiseRoutes() prefs.ListView[netip.Prefix] { return v.ж.AdvertiseRoutes.View() }
|
||||
func (v PrefsView) NoSNAT() prefs.Item[bool] { return v.ж.NoSNAT }
|
||||
func (v PrefsView) NoStatefulFiltering() prefs.Item[opt.Bool] { return v.ж.NoStatefulFiltering }
|
||||
func (v PrefsView) NetfilterMode() prefs.Item[preftype.NetfilterMode] { return v.ж.NetfilterMode }
|
||||
func (v PrefsView) OperatorUser() prefs.Item[string] { return v.ж.OperatorUser }
|
||||
func (v PrefsView) ProfileName() prefs.Item[string] { return v.ж.ProfileName }
|
||||
func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate }
|
||||
func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector }
|
||||
func (v PrefsView) PostureChecking() prefs.Item[bool] { return v.ж.PostureChecking }
|
||||
func (v PrefsView) NetfilterKind() prefs.Item[string] { return v.ж.NetfilterKind }
|
||||
func (v PrefsView) DriveShares() prefs.StructListView[*drive.Share, drive.ShareView] {
|
||||
return prefs.StructListViewOf(&v.ж.DriveShares)
|
||||
}
|
||||
func (v PrefsView) AllowSingleHosts() prefs.Item[marshalAsTrueInJSON] { return v.ж.AllowSingleHosts }
|
||||
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
ControlURL prefs.Item[string]
|
||||
RouteAll prefs.Item[bool]
|
||||
ExitNodeID prefs.Item[tailcfg.StableNodeID]
|
||||
ExitNodeIP prefs.Item[netip.Addr]
|
||||
ExitNodePrior tailcfg.StableNodeID
|
||||
ExitNodeAllowLANAccess prefs.Item[bool]
|
||||
CorpDNS prefs.Item[bool]
|
||||
RunSSH prefs.Item[bool]
|
||||
RunWebClient prefs.Item[bool]
|
||||
WantRunning prefs.Item[bool]
|
||||
LoggedOut prefs.Item[bool]
|
||||
ShieldsUp prefs.Item[bool]
|
||||
AdvertiseTags prefs.List[string]
|
||||
Hostname prefs.Item[string]
|
||||
NotepadURLs prefs.Item[bool]
|
||||
ForceDaemon prefs.Item[bool]
|
||||
Egg prefs.Item[bool]
|
||||
AdvertiseRoutes prefs.List[netip.Prefix]
|
||||
NoSNAT prefs.Item[bool]
|
||||
NoStatefulFiltering prefs.Item[opt.Bool]
|
||||
NetfilterMode prefs.Item[preftype.NetfilterMode]
|
||||
OperatorUser prefs.Item[string]
|
||||
ProfileName prefs.Item[string]
|
||||
AutoUpdate AutoUpdatePrefs
|
||||
AppConnector AppConnectorPrefs
|
||||
PostureChecking prefs.Item[bool]
|
||||
NetfilterKind prefs.Item[string]
|
||||
DriveShares prefs.StructList[*drive.Share]
|
||||
AllowSingleHosts prefs.Item[marshalAsTrueInJSON]
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of AutoUpdatePrefs.
|
||||
func (p *AutoUpdatePrefs) View() AutoUpdatePrefsView {
|
||||
return AutoUpdatePrefsView{ж: p}
|
||||
}
|
||||
|
||||
// AutoUpdatePrefsView provides a read-only view over AutoUpdatePrefs.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type AutoUpdatePrefsView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *AutoUpdatePrefs
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v AutoUpdatePrefsView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v AutoUpdatePrefsView) AsStruct() *AutoUpdatePrefs {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v AutoUpdatePrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *AutoUpdatePrefsView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x AutoUpdatePrefs
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v AutoUpdatePrefsView) Check() prefs.Item[bool] { return v.ж.Check }
|
||||
func (v AutoUpdatePrefsView) Apply() prefs.Item[opt.Bool] { return v.ж.Apply }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _AutoUpdatePrefsViewNeedsRegeneration = AutoUpdatePrefs(struct {
|
||||
Check prefs.Item[bool]
|
||||
Apply prefs.Item[opt.Bool]
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of AppConnectorPrefs.
|
||||
func (p *AppConnectorPrefs) View() AppConnectorPrefsView {
|
||||
return AppConnectorPrefsView{ж: p}
|
||||
}
|
||||
|
||||
// AppConnectorPrefsView provides a read-only view over AppConnectorPrefs.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type AppConnectorPrefsView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *AppConnectorPrefs
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v AppConnectorPrefsView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v AppConnectorPrefsView) AsStruct() *AppConnectorPrefs {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v AppConnectorPrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *AppConnectorPrefsView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x AppConnectorPrefs
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v AppConnectorPrefsView) Advertise() prefs.Item[bool] { return v.ж.Advertise }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _AppConnectorPrefsViewNeedsRegeneration = AppConnectorPrefs(struct {
|
||||
Advertise prefs.Item[bool]
|
||||
}{})
|
140
types/prefs/prefs_example/prefs_test.go
Normal file
140
types/prefs/prefs_example/prefs_test.go
Normal file
@ -0,0 +1,140 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package prefs_example
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/prefs"
|
||||
)
|
||||
|
||||
func ExamplePrefs_AdvertiseRoutes_setValue() {
|
||||
p := &Prefs{}
|
||||
|
||||
// Initially, preferences are not configured.
|
||||
fmt.Println("IsSet:", p.AdvertiseRoutes.IsSet()) // prints false
|
||||
// And the Value method returns the default (or zero) value.
|
||||
fmt.Println("Initial:", p.AdvertiseRoutes.Value()) // prints []
|
||||
|
||||
// Preferences can be configured with user-provided values using the
|
||||
// SetValue method. It may fail if the preference is managed via syspolicy
|
||||
// or is otherwise read-only.
|
||||
routes := []netip.Prefix{netip.MustParsePrefix("192.168.1.1/24")}
|
||||
if err := p.AdvertiseRoutes.SetValue(routes); err != nil {
|
||||
// This block is never executed in the example because the
|
||||
// AdvertiseRoutes preference is neither managed nor read-only.
|
||||
fmt.Println("SetValue:", err)
|
||||
}
|
||||
fmt.Println("IsSet:", p.AdvertiseRoutes.IsSet()) // prints true
|
||||
fmt.Println("Value:", p.AdvertiseRoutes.Value()) // prints 192.168.1.1/24
|
||||
|
||||
// Preference values are copied on use; you cannot not modify them after they are set.
|
||||
routes[0] = netip.MustParsePrefix("10.10.10.0/24") // this has no effect
|
||||
fmt.Println("Unchanged:", p.AdvertiseRoutes.Value()) // still prints 192.168.1.1/24
|
||||
// If necessary, the value can be changed by calling the SetValue method again.
|
||||
p.AdvertiseRoutes.SetValue(routes)
|
||||
fmt.Println("Changed:", p.AdvertiseRoutes.Value()) // prints 10.10.10.0/24
|
||||
|
||||
// The following code is fine when defining default or baseline prefs, or
|
||||
// in tests. However, assigning to a preference field directly overwrites
|
||||
// syspolicy-managed values and metadata, so it should generally be avoided
|
||||
// when working with the actual profile or device preferences.
|
||||
// It is caller's responsibility to use the mutable Prefs struct correctly.
|
||||
defaults := &Prefs{WantRunning: prefs.ItemOf(true)}
|
||||
defaults.CorpDNS = prefs.Item[bool]{}
|
||||
defaults.ExitNodeAllowLANAccess = prefs.ItemOf(true)
|
||||
_, _, _ = defaults.WantRunning, defaults.CorpDNS, defaults.ExitNodeAllowLANAccess
|
||||
|
||||
// In most contexts, preferences should only be read and never mutated.
|
||||
// To make it easier to enforce this guarantee, a view type generated with
|
||||
// [tailscale.com/cmd/viewer] can be used instead of the mutable Prefs struct.
|
||||
// Preferences accessed via a view have the same set of non-mutating
|
||||
// methods as the underlying preferences but do not expose [prefs.Item.SetValue] or
|
||||
// other methods that modify the preference's value or state.
|
||||
v := p.View()
|
||||
// Additionally, non-mutating methods like [prefs.ItemView.Value] and [prefs.ItemView.ValueOk]
|
||||
// return read-only views of the underlying values instead of the actual potentially mutable values.
|
||||
// For example, on the next line Value() returns a views.Slice[netip.Prefix], not a []netip.Prefix.
|
||||
_ = v.AdvertiseRoutes().Value()
|
||||
fmt.Println("Via View:", v.AdvertiseRoutes().Value().At(0)) // prints 10.10.10.0/24
|
||||
fmt.Println("IsSet:", v.AdvertiseRoutes().IsSet()) // prints true
|
||||
fmt.Println("IsManaged:", v.AdvertiseRoutes().IsManaged()) // prints false
|
||||
fmt.Println("IsReadOnly:", v.AdvertiseRoutes().IsReadOnly()) // prints false
|
||||
|
||||
// Output:
|
||||
// IsSet: false
|
||||
// Initial: []
|
||||
// IsSet: true
|
||||
// Value: [192.168.1.1/24]
|
||||
// Unchanged: [192.168.1.1/24]
|
||||
// Changed: [10.10.10.0/24]
|
||||
// Via View: 10.10.10.0/24
|
||||
// IsSet: true
|
||||
// IsManaged: false
|
||||
// IsReadOnly: false
|
||||
}
|
||||
|
||||
func ExamplePrefs_ControlURL_setDefaultValue() {
|
||||
p := &Prefs{}
|
||||
v := p.View()
|
||||
|
||||
// We can set default values for preferences when their default values
|
||||
// should differ from the zero values of the corresponding Go types.
|
||||
//
|
||||
// Note that in this example, we configure preferences via a mutable
|
||||
// [Prefs] struct but fetch values via a read-only [PrefsView].
|
||||
// Typically, we set and get preference values in different parts
|
||||
// of the codebase.
|
||||
p.ControlURL.SetDefaultValue(ipn.DefaultControlURL)
|
||||
// The default value is used if the preference is not configured...
|
||||
fmt.Println("Default:", v.ControlURL().Value())
|
||||
p.ControlURL.SetValue("https://control.example.com")
|
||||
fmt.Println("User Set:", v.ControlURL().Value())
|
||||
// ...including when it has been reset.
|
||||
p.ControlURL.ClearValue()
|
||||
fmt.Println("Reset to Default:", v.ControlURL().Value())
|
||||
|
||||
// Output:
|
||||
// Default: https://controlplane.tailscale.com
|
||||
// User Set: https://control.example.com
|
||||
// Reset to Default: https://controlplane.tailscale.com
|
||||
}
|
||||
|
||||
func ExamplePrefs_ExitNodeID_setManagedValue() {
|
||||
p := &Prefs{}
|
||||
v := p.View()
|
||||
|
||||
// We can mark preferences as being managed via syspolicy (e.g., via GP/MDM)
|
||||
// by setting its managed value.
|
||||
//
|
||||
// Note that in this example, we enforce syspolicy-managed values
|
||||
// via a mutable [Prefs] struct but fetch values via a read-only [PrefsView].
|
||||
// This is typically spread throughout the codebase.
|
||||
p.ExitNodeID.SetManagedValue("ManagedExitNode")
|
||||
// Marking a preference as managed prevents it from being changed by the user.
|
||||
if err := p.ExitNodeID.SetValue("CustomExitNode"); err != nil {
|
||||
fmt.Println("SetValue:", err) // reports an error
|
||||
}
|
||||
fmt.Println("Exit Node:", v.ExitNodeID().Value()) // prints ManagedExitNode
|
||||
|
||||
// Clients can hide or disable preferences that are managed or read-only.
|
||||
fmt.Println("IsManaged:", v.ExitNodeID().IsManaged()) // prints true
|
||||
fmt.Println("IsReadOnly:", v.ExitNodeID().IsReadOnly()) // prints true; managed preferences are always read-only.
|
||||
|
||||
// ClearManaged is called when the preference is no longer managed,
|
||||
// allowing the user to change it.
|
||||
p.ExitNodeID.ClearManaged()
|
||||
fmt.Println("IsManaged:", v.ExitNodeID().IsManaged()) // prints false
|
||||
fmt.Println("IsReadOnly:", v.ExitNodeID().IsReadOnly()) // prints false
|
||||
|
||||
// Output:
|
||||
// SetValue: cannot modify a managed preference
|
||||
// Exit Node: ManagedExitNode
|
||||
// IsManaged: true
|
||||
// IsReadOnly: true
|
||||
// IsManaged: false
|
||||
// IsReadOnly: false
|
||||
}
|
166
types/prefs/prefs_example/prefs_types.go
Normal file
166
types/prefs/prefs_example/prefs_types.go
Normal file
@ -0,0 +1,166 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package prefs_example contains a [Prefs] type, which is like [tailscale.com/ipn.Prefs],
|
||||
// but uses the [prefs] package to enhance individual preferences with state and metadata.
|
||||
//
|
||||
// It also includes testable examples utilizing the [Prefs] type.
|
||||
// We made it a separate package to avoid circular dependencies
|
||||
// and due to limitations in [tailscale.com/cmd/viewer] when
|
||||
// generating code for test packages.
|
||||
package prefs_example
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/prefs"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=Prefs,AutoUpdatePrefs,AppConnectorPrefs
|
||||
|
||||
// Prefs is like [tailscale.com/ipn.Prefs], but with individual preferences wrapped in
|
||||
// [prefs.Item], [prefs.List], and [prefs.StructList] to include preference
|
||||
// state and metadata. Related preferences can be grouped together in a nested
|
||||
// struct (e.g., [AutoUpdatePrefs] or [AppConnectorPrefs]), whereas each
|
||||
// individual preference that can be configured by a user or managed via
|
||||
// syspolicy is wrapped.
|
||||
//
|
||||
// Non-preference fields, such as ExitNodePrior and Persist, can be included as-is.
|
||||
//
|
||||
// Just like [tailscale.com/ipn.Prefs], [Prefs] is a mutable struct. It should
|
||||
// only be used in well-defined contexts where mutability is expected and desired,
|
||||
// such as when the LocalBackend receives a request from the GUI/CLI to change a
|
||||
// preference, when a preference is managed via syspolicy and needs to be
|
||||
// configured with an admin-provided value, or when the internal state (e.g.,
|
||||
// [persist.Persist]) has changed and needs to be preserved.
|
||||
// In other contexts, a [PrefsView] should be used to provide a read-only view
|
||||
// of the preferences.
|
||||
//
|
||||
// It is recommended to use [jsonv2] for [Prefs] marshaling and unmarshalling to
|
||||
// improve performance and enable the omission of unconfigured preferences with
|
||||
// the `omitzero` JSON tag option. This option is not supported by the
|
||||
// [encoding/json] package as of 2024-08-21; see golang/go#45669.
|
||||
// It is recommended that a prefs type implements both
|
||||
// [jsonv2.MarshalerV2]/[jsonv2.UnmarshalerV2] and [json.Marshaler]/[json.Unmarshaler]
|
||||
// to ensure consistent and more performant marshaling, regardless of the JSON package
|
||||
// used at the call sites; the standard marshalers can be implemented via [jsonv2].
|
||||
// See [Prefs.MarshalJSONV2], [Prefs.UnmarshalJSONV2], [Prefs.MarshalJSON],
|
||||
// and [Prefs.UnmarshalJSON] for an example implementation.
|
||||
type Prefs struct {
|
||||
ControlURL prefs.Item[string] `json:",omitzero"`
|
||||
RouteAll prefs.Item[bool] `json:",omitzero"`
|
||||
ExitNodeID prefs.Item[tailcfg.StableNodeID] `json:",omitzero"`
|
||||
ExitNodeIP prefs.Item[netip.Addr] `json:",omitzero"`
|
||||
|
||||
// ExitNodePrior is an internal state rather than a preference.
|
||||
// It can be kept in the Prefs structure but should not be wrapped
|
||||
// and is ignored by the [prefs] package.
|
||||
ExitNodePrior tailcfg.StableNodeID
|
||||
|
||||
ExitNodeAllowLANAccess prefs.Item[bool] `json:",omitzero"`
|
||||
CorpDNS prefs.Item[bool] `json:",omitzero"`
|
||||
RunSSH prefs.Item[bool] `json:",omitzero"`
|
||||
RunWebClient prefs.Item[bool] `json:",omitzero"`
|
||||
WantRunning prefs.Item[bool] `json:",omitzero"`
|
||||
LoggedOut prefs.Item[bool] `json:",omitzero"`
|
||||
ShieldsUp prefs.Item[bool] `json:",omitzero"`
|
||||
// AdvertiseTags is a preference whose value is a slice of strings.
|
||||
// The value is atomic, and individual items in the slice should
|
||||
// not be modified after the preference is set.
|
||||
// Since the item type (string) is immutable, we can use [prefs.List].
|
||||
AdvertiseTags prefs.List[string] `json:",omitzero"`
|
||||
Hostname prefs.Item[string] `json:",omitzero"`
|
||||
NotepadURLs prefs.Item[bool] `json:",omitzero"`
|
||||
ForceDaemon prefs.Item[bool] `json:",omitzero"`
|
||||
Egg prefs.Item[bool] `json:",omitzero"`
|
||||
// AdvertiseRoutes is a preference whose value is a slice of netip.Prefix.
|
||||
// The value is atomic, and individual items in the slice should
|
||||
// not be modified after the preference is set.
|
||||
// Since the item type (netip.Prefix) is immutable, we can use [prefs.List].
|
||||
AdvertiseRoutes prefs.List[netip.Prefix] `json:",omitzero"`
|
||||
NoSNAT prefs.Item[bool] `json:",omitzero"`
|
||||
NoStatefulFiltering prefs.Item[opt.Bool] `json:",omitzero"`
|
||||
NetfilterMode prefs.Item[preftype.NetfilterMode] `json:",omitzero"`
|
||||
OperatorUser prefs.Item[string] `json:",omitzero"`
|
||||
ProfileName prefs.Item[string] `json:",omitzero"`
|
||||
|
||||
// AutoUpdate contains auto-update preferences.
|
||||
// Each preference in the group can be configured and managed individually.
|
||||
AutoUpdate AutoUpdatePrefs `json:",omitzero"`
|
||||
|
||||
// AppConnector contains app connector-related preferences.
|
||||
// Each preference in the group can be configured and managed individually.
|
||||
AppConnector AppConnectorPrefs `json:",omitzero"`
|
||||
|
||||
PostureChecking prefs.Item[bool] `json:",omitzero"`
|
||||
NetfilterKind prefs.Item[string] `json:",omitzero"`
|
||||
// DriveShares is a preference whose value is a slice of *[drive.Share].
|
||||
// The value is atomic, and individual items in the slice should
|
||||
// not be modified after the preference is set.
|
||||
// Since the item type (*drive.Share) is mutable and implements [views.ViewCloner],
|
||||
// we need to use [prefs.StructList] instead of [prefs.List].
|
||||
DriveShares prefs.StructList[*drive.Share] `json:",omitzero"`
|
||||
AllowSingleHosts prefs.Item[marshalAsTrueInJSON] `json:",omitzero"`
|
||||
|
||||
// Persist is an internal state rather than a preference.
|
||||
// It can be kept in the Prefs structure but should not be wrapped
|
||||
// and is ignored by the [prefs] package.
|
||||
Persist *persist.Persist `json:"Config"`
|
||||
}
|
||||
|
||||
// AutoUpdatePrefs is like [ipn.AutoUpdatePrefs], but it wraps individual preferences with [prefs.Item].
|
||||
// It groups related preferences together while allowing each to be configured individually.
|
||||
type AutoUpdatePrefs struct {
|
||||
Check prefs.Item[bool] `json:",omitzero"`
|
||||
Apply prefs.Item[opt.Bool] `json:",omitzero"`
|
||||
}
|
||||
|
||||
// AppConnectorPrefs is like [ipn.AppConnectorPrefs], but it wraps individual preferences with [prefs.Item].
|
||||
// It groups related preferences together while allowing each to be configured individually.
|
||||
type AppConnectorPrefs struct {
|
||||
Advertise prefs.Item[bool] `json:",omitzero"`
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
// It is implemented as a performance improvement and to enable omission of
|
||||
// unconfigured preferences from the JSON output. See the [Prefs] doc for details.
|
||||
func (p Prefs) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
// The prefs type shadows the Prefs's method set,
|
||||
// causing [jsonv2] to use the default marshaler and avoiding
|
||||
// infinite recursion.
|
||||
type prefs Prefs
|
||||
return jsonv2.MarshalEncode(out, (*prefs)(&p), opts)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (p *Prefs) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
// The prefs type shadows the Prefs's method set,
|
||||
// causing [jsonv2] to use the default unmarshaler and avoiding
|
||||
// infinite recursion.
|
||||
type prefs Prefs
|
||||
return jsonv2.UnmarshalDecode(in, (*prefs)(p), opts)
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (p Prefs) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(p) // uses MarshalJSONV2
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (p *Prefs) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
|
||||
}
|
||||
|
||||
type marshalAsTrueInJSON struct{}
|
||||
|
||||
var trueJSON = []byte("true")
|
||||
|
||||
func (marshalAsTrueInJSON) MarshalJSON() ([]byte, error) { return trueJSON, nil }
|
||||
func (*marshalAsTrueInJSON) UnmarshalJSON([]byte) error { return nil }
|
670
types/prefs/prefs_test.go
Normal file
670
types/prefs/prefs_test.go
Normal file
@ -0,0 +1,670 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package prefs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --tags=test --type=TestPrefs,TestBundle,TestValueStruct,TestGenericStruct,TestPrefsGroup
|
||||
|
||||
type TestPrefs struct {
|
||||
Int32Item Item[int32] `json:",omitzero"`
|
||||
UInt64Item Item[uint64] `json:",omitzero"`
|
||||
StringItem1 Item[string] `json:",omitzero"`
|
||||
StringItem2 Item[string] `json:",omitzero"`
|
||||
BoolItem1 Item[bool] `json:",omitzero"`
|
||||
BoolItem2 Item[bool] `json:",omitzero"`
|
||||
StringSlice List[string] `json:",omitzero"`
|
||||
IntSlice List[int] `json:",omitzero"`
|
||||
|
||||
AddrItem Item[netip.Addr] `json:",omitzero"`
|
||||
|
||||
StringStringMap Map[string, string] `json:",omitzero"`
|
||||
IntStringMap Map[int, string] `json:",omitzero"`
|
||||
AddrIntMap Map[netip.Addr, int] `json:",omitzero"`
|
||||
|
||||
// Bundles are complex preferences that usually consist of
|
||||
// multiple parameters that must be configured atomically.
|
||||
Bundle1 Item[*TestBundle] `json:",omitzero"`
|
||||
Bundle2 Item[*TestBundle] `json:",omitzero"`
|
||||
Generic Item[*TestGenericStruct[int]] `json:",omitzero"`
|
||||
|
||||
BundleList StructList[*TestBundle] `json:",omitzero"`
|
||||
|
||||
StringBundleMap StructMap[string, *TestBundle] `json:",omitzero"`
|
||||
IntBundleMap StructMap[int, *TestBundle] `json:",omitzero"`
|
||||
AddrBundleMap StructMap[netip.Addr, *TestBundle] `json:",omitzero"`
|
||||
|
||||
// Group is a nested struct that contains one or more preferences.
|
||||
// Each preference in a group can be configured individually.
|
||||
// Preference groups should be included directly rather than by pointers.
|
||||
Group TestPrefsGroup `json:",omitzero"`
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (p TestPrefs) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
// The testPrefs type shadows the TestPrefs's method set,
|
||||
// causing jsonv2 to use the default marshaler and avoiding
|
||||
// infinite recursion.
|
||||
type testPrefs TestPrefs
|
||||
return jsonv2.MarshalEncode(out, (*testPrefs)(&p), opts)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (p *TestPrefs) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
// The testPrefs type shadows the TestPrefs's method set,
|
||||
// causing jsonv2 to use the default unmarshaler and avoiding
|
||||
// infinite recursion.
|
||||
type testPrefs TestPrefs
|
||||
return jsonv2.UnmarshalDecode(in, (*testPrefs)(p), opts)
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (p TestPrefs) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(p) // uses MarshalJSONV2
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (p *TestPrefs) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
|
||||
}
|
||||
|
||||
// TestBundle is an example structure type that,
|
||||
// despite containing multiple values, represents
|
||||
// a single configurable preference item.
|
||||
type TestBundle struct {
|
||||
Name string `json:",omitzero"`
|
||||
Nested *TestValueStruct `json:",omitzero"`
|
||||
}
|
||||
|
||||
func (b *TestBundle) Equal(b2 *TestBundle) bool {
|
||||
if b == b2 {
|
||||
return true
|
||||
}
|
||||
if b == nil || b2 == nil {
|
||||
return false
|
||||
}
|
||||
return b.Name == b2.Name && b.Nested.Equal(b2.Nested)
|
||||
}
|
||||
|
||||
// TestPrefsGroup contains logically grouped preference items.
|
||||
// Each preference item in a group can be configured individually.
|
||||
type TestPrefsGroup struct {
|
||||
FloatItem Item[float64] `json:",omitzero"`
|
||||
|
||||
TestStringItem Item[TestStringType] `json:",omitzero"`
|
||||
}
|
||||
|
||||
type TestValueStruct struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
func (s *TestValueStruct) Equal(s2 *TestValueStruct) bool {
|
||||
if s == s2 {
|
||||
return true
|
||||
}
|
||||
if s == nil || s2 == nil {
|
||||
return false
|
||||
}
|
||||
return *s == *s2
|
||||
}
|
||||
|
||||
type TestGenericStruct[T ImmutableType] struct {
|
||||
Value T
|
||||
}
|
||||
|
||||
func (s *TestGenericStruct[T]) Equal(s2 *TestGenericStruct[T]) bool {
|
||||
if s == s2 {
|
||||
return true
|
||||
}
|
||||
if s == nil || s2 == nil {
|
||||
return false
|
||||
}
|
||||
return *s == *s2
|
||||
}
|
||||
|
||||
type TestStringType string
|
||||
|
||||
func TestMarshalUnmarshal(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefs *TestPrefs
|
||||
indent bool
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "string",
|
||||
prefs: &TestPrefs{StringItem1: ItemOf("Value1")},
|
||||
want: `{"StringItem1": {"Value": "Value1"}}`,
|
||||
},
|
||||
{
|
||||
name: "empty-string",
|
||||
prefs: &TestPrefs{StringItem1: ItemOf("")},
|
||||
want: `{"StringItem1": {"Value": ""}}`,
|
||||
},
|
||||
{
|
||||
name: "managed-string",
|
||||
prefs: &TestPrefs{StringItem1: ItemOf("Value1", Managed)},
|
||||
want: `{"StringItem1": {"Value": "Value1", "Managed": true}}`,
|
||||
},
|
||||
{
|
||||
name: "readonly-item",
|
||||
prefs: &TestPrefs{StringItem1: ItemWithOpts[string](ReadOnly)},
|
||||
want: `{"StringItem1": {"ReadOnly": true}}`,
|
||||
},
|
||||
{
|
||||
name: "readonly-item-with-value",
|
||||
prefs: &TestPrefs{StringItem1: ItemOf("RO", ReadOnly)},
|
||||
want: `{"StringItem1": {"Value": "RO", "ReadOnly": true}}`,
|
||||
},
|
||||
{
|
||||
name: "int32",
|
||||
prefs: &TestPrefs{Int32Item: ItemOf[int32](101)},
|
||||
want: `{"Int32Item": {"Value": 101}}`,
|
||||
},
|
||||
{
|
||||
name: "uint64",
|
||||
prefs: &TestPrefs{UInt64Item: ItemOf[uint64](42)},
|
||||
want: `{"UInt64Item": {"Value": 42}}`,
|
||||
},
|
||||
{
|
||||
name: "bool-true",
|
||||
prefs: &TestPrefs{BoolItem1: ItemOf(true)},
|
||||
want: `{"BoolItem1": {"Value": true}}`,
|
||||
},
|
||||
{
|
||||
name: "bool-false",
|
||||
prefs: &TestPrefs{BoolItem1: ItemOf(false)},
|
||||
want: `{"BoolItem1": {"Value": false}}`,
|
||||
},
|
||||
{
|
||||
name: "empty-slice",
|
||||
prefs: &TestPrefs{StringSlice: ListOf([]string{})},
|
||||
want: `{"StringSlice": {"Value": []}}`,
|
||||
},
|
||||
{
|
||||
name: "string-slice",
|
||||
prefs: &TestPrefs{StringSlice: ListOf([]string{"1", "2", "3"})},
|
||||
want: `{"StringSlice": {"Value": ["1", "2", "3"]}}`,
|
||||
},
|
||||
{
|
||||
name: "int-slice",
|
||||
prefs: &TestPrefs{IntSlice: ListOf([]int{4, 8, 15, 16, 23})},
|
||||
want: `{"IntSlice": {"Value": [4, 8, 15, 16, 23]}}`,
|
||||
},
|
||||
{
|
||||
name: "managed-int-slice",
|
||||
prefs: &TestPrefs{IntSlice: ListOf([]int{4, 8, 15, 16, 23}, Managed)},
|
||||
want: `{"IntSlice": {"Value": [4, 8, 15, 16, 23], "Managed": true}}`,
|
||||
},
|
||||
{
|
||||
name: "netip-addr",
|
||||
prefs: &TestPrefs{AddrItem: ItemOf(netip.MustParseAddr("127.0.0.1"))},
|
||||
want: `{"AddrItem": {"Value": "127.0.0.1"}}`,
|
||||
},
|
||||
{
|
||||
name: "string-string-map",
|
||||
prefs: &TestPrefs{StringStringMap: MapOf(map[string]string{"K1": "V1"})},
|
||||
want: `{"StringStringMap": {"Value": {"K1": "V1"}}}`,
|
||||
},
|
||||
{
|
||||
name: "int-string-map",
|
||||
prefs: &TestPrefs{IntStringMap: MapOf(map[int]string{42: "V1"})},
|
||||
want: `{"IntStringMap": {"Value": {"42": "V1"}}}`,
|
||||
},
|
||||
{
|
||||
name: "addr-int-map",
|
||||
prefs: &TestPrefs{AddrIntMap: MapOf(map[netip.Addr]int{netip.MustParseAddr("127.0.0.1"): 42})},
|
||||
want: `{"AddrIntMap": {"Value": {"127.0.0.1": 42}}}`,
|
||||
},
|
||||
{
|
||||
name: "bundle-list",
|
||||
prefs: &TestPrefs{BundleList: StructListOf([]*TestBundle{{Name: "Bundle1"}, {Name: "Bundle2"}})},
|
||||
want: `{"BundleList": {"Value": [{"Name": "Bundle1"},{"Name": "Bundle2"}]}}`,
|
||||
},
|
||||
{
|
||||
name: "string-bundle-map",
|
||||
prefs: &TestPrefs{StringBundleMap: StructMapOf(map[string]*TestBundle{
|
||||
"K1": {Name: "Bundle1"},
|
||||
"K2": {Name: "Bundle2"},
|
||||
})},
|
||||
want: `{"StringBundleMap": {"Value": {"K1": {"Name": "Bundle1"}, "K2": {"Name": "Bundle2"}}}}`,
|
||||
},
|
||||
{
|
||||
name: "int-bundle-map",
|
||||
prefs: &TestPrefs{IntBundleMap: StructMapOf(map[int]*TestBundle{42: {Name: "Bundle1"}})},
|
||||
want: `{"IntBundleMap": {"Value": {"42": {"Name": "Bundle1"}}}}`,
|
||||
},
|
||||
{
|
||||
name: "addr-bundle-map",
|
||||
prefs: &TestPrefs{AddrBundleMap: StructMapOf(map[netip.Addr]*TestBundle{netip.MustParseAddr("127.0.0.1"): {Name: "Bundle1"}})},
|
||||
want: `{"AddrBundleMap": {"Value": {"127.0.0.1": {"Name": "Bundle1"}}}}`,
|
||||
},
|
||||
{
|
||||
name: "bundle",
|
||||
prefs: &TestPrefs{Bundle1: ItemOf(&TestBundle{Name: "Bundle1"})},
|
||||
want: `{"Bundle1": {"Value": {"Name": "Bundle1"}}}`,
|
||||
},
|
||||
{
|
||||
name: "managed-bundle",
|
||||
prefs: &TestPrefs{Bundle2: ItemOf(&TestBundle{Name: "Bundle2", Nested: &TestValueStruct{Value: 17}}, Managed)},
|
||||
want: `{"Bundle2": {"Value": {"Name": "Bundle2", "Nested": {"Value": 17}}, "Managed": true}}`,
|
||||
},
|
||||
{
|
||||
name: "subgroup",
|
||||
prefs: &TestPrefs{Group: TestPrefsGroup{FloatItem: ItemOf(1.618), TestStringItem: ItemOf(TestStringType("Value"))}},
|
||||
want: `{"Group": {"FloatItem": {"Value": 1.618}, "TestStringItem": {"Value": "Value"}}}`,
|
||||
},
|
||||
{
|
||||
name: "various",
|
||||
prefs: &TestPrefs{
|
||||
Int32Item: ItemOf[int32](101),
|
||||
UInt64Item: ItemOf[uint64](42),
|
||||
StringItem1: ItemOf("Value1"),
|
||||
StringItem2: ItemWithOpts[string](ReadOnly),
|
||||
BoolItem1: ItemOf(true),
|
||||
BoolItem2: ItemOf(false, Managed),
|
||||
StringSlice: ListOf([]string{"1", "2", "3"}),
|
||||
IntSlice: ListOf([]int{4, 8, 15, 16, 23}, Managed),
|
||||
AddrItem: ItemOf(netip.MustParseAddr("127.0.0.1")),
|
||||
StringStringMap: MapOf(map[string]string{"K1": "V1"}),
|
||||
IntStringMap: MapOf(map[int]string{42: "V1"}),
|
||||
AddrIntMap: MapOf(map[netip.Addr]int{netip.MustParseAddr("127.0.0.1"): 42}),
|
||||
BundleList: StructListOf([]*TestBundle{{Name: "Bundle1"}}),
|
||||
StringBundleMap: StructMapOf(map[string]*TestBundle{"K1": {Name: "Bundle1"}}),
|
||||
IntBundleMap: StructMapOf(map[int]*TestBundle{42: {Name: "Bundle1"}}),
|
||||
AddrBundleMap: StructMapOf(map[netip.Addr]*TestBundle{netip.MustParseAddr("127.0.0.1"): {Name: "Bundle1"}}),
|
||||
Bundle1: ItemOf(&TestBundle{Name: "Bundle1"}),
|
||||
Bundle2: ItemOf(&TestBundle{Name: "Bundle2", Nested: &TestValueStruct{Value: 17}}, Managed),
|
||||
Group: TestPrefsGroup{
|
||||
FloatItem: ItemOf(1.618),
|
||||
TestStringItem: ItemOf(TestStringType("Value")),
|
||||
},
|
||||
},
|
||||
want: `{
|
||||
"Int32Item": {"Value": 101},
|
||||
"UInt64Item": {"Value": 42},
|
||||
"StringItem1": {"Value": "Value1"},
|
||||
"StringItem2": {"ReadOnly": true},
|
||||
"BoolItem1": {"Value": true},
|
||||
"BoolItem2": {"Value": false, "Managed": true},
|
||||
"StringSlice": {"Value": ["1", "2", "3"]},
|
||||
"IntSlice": {"Value": [4, 8, 15, 16, 23], "Managed": true},
|
||||
"AddrItem": {"Value": "127.0.0.1"},
|
||||
"StringStringMap": {"Value": {"K1": "V1"}},
|
||||
"IntStringMap": {"Value": {"42": "V1"}},
|
||||
"AddrIntMap": {"Value": {"127.0.0.1": 42}},
|
||||
"BundleList": {"Value": [{"Name": "Bundle1"}]},
|
||||
"StringBundleMap": {"Value": {"K1": {"Name": "Bundle1"}}},
|
||||
"IntBundleMap": {"Value": {"42": {"Name": "Bundle1"}}},
|
||||
"AddrBundleMap": {"Value": {"127.0.0.1": {"Name": "Bundle1"}}},
|
||||
"Bundle1": {"Value": {"Name": "Bundle1"}},
|
||||
"Bundle2": {"Value": {"Name": "Bundle2", "Nested": {"Value": 17}}, "Managed": true},
|
||||
"Group": {
|
||||
"FloatItem": {"Value": 1.618},
|
||||
"TestStringItem": {"Value": "Value"}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}
|
||||
|
||||
arshalers := []struct {
|
||||
name string
|
||||
marshal func(in any) (out []byte, err error)
|
||||
unmarshal func(in []byte, out any) (err error)
|
||||
}{
|
||||
{
|
||||
name: "json",
|
||||
marshal: json.Marshal,
|
||||
unmarshal: json.Unmarshal,
|
||||
},
|
||||
{
|
||||
name: "jsonv2",
|
||||
marshal: func(in any) (out []byte, err error) { return jsonv2.Marshal(in) },
|
||||
unmarshal: func(in []byte, out any) (err error) { return jsonv2.Unmarshal(in, out) },
|
||||
},
|
||||
}
|
||||
|
||||
for _, a := range arshalers {
|
||||
t.Run(a.name, func(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Run("marshal-directly", func(t *testing.T) {
|
||||
gotJSON, err := a.marshal(tt.prefs)
|
||||
if err != nil {
|
||||
t.Fatalf("marshalling failed: %v", err)
|
||||
}
|
||||
|
||||
checkJSON(t, gotJSON, jsontext.Value(tt.want))
|
||||
|
||||
var gotPrefs TestPrefs
|
||||
if err = a.unmarshal(gotJSON, &gotPrefs); err != nil {
|
||||
t.Fatalf("unmarshalling failed: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.prefs, &gotPrefs); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("marshal-via-view", func(t *testing.T) {
|
||||
gotJSON, err := a.marshal(tt.prefs.View())
|
||||
if err != nil {
|
||||
t.Fatalf("marshalling failed: %v", err)
|
||||
}
|
||||
|
||||
checkJSON(t, gotJSON, jsontext.Value(tt.want))
|
||||
|
||||
var gotPrefs TestPrefsView
|
||||
if err = a.unmarshal(gotJSON, &gotPrefs); err != nil {
|
||||
t.Fatalf("unmarshalling failed: %v", err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.prefs, gotPrefs.AsStruct()); diff != "" {
|
||||
t.Errorf("mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreferenceStates(t *testing.T) {
|
||||
const (
|
||||
zeroValue = 0
|
||||
defValue = 5
|
||||
userValue = 42
|
||||
mdmValue = 1001
|
||||
)
|
||||
i := ItemWithOpts[int]()
|
||||
checkIsSet(t, &i, false)
|
||||
checkIsManaged(t, &i, false)
|
||||
checkIsReadOnly(t, &i, false)
|
||||
checkValueOk(t, &i, zeroValue, false)
|
||||
|
||||
i.SetDefaultValue(defValue)
|
||||
checkValue(t, &i, defValue)
|
||||
checkValueOk(t, &i, defValue, false)
|
||||
|
||||
checkSetValue(t, &i, userValue)
|
||||
checkValue(t, &i, userValue)
|
||||
checkValueOk(t, &i, userValue, true)
|
||||
|
||||
i2 := ItemOf(userValue)
|
||||
checkIsSet(t, &i2, true)
|
||||
checkValue(t, &i2, userValue)
|
||||
checkValueOk(t, &i2, userValue, true)
|
||||
checkEqual(t, i2, i, true)
|
||||
|
||||
i2.SetManagedValue(mdmValue)
|
||||
// Setting a managed value should set the value, mark the preference
|
||||
// as managed and read-only, and prevent it from being modified with SetValue.
|
||||
checkIsSet(t, &i2, true)
|
||||
checkIsManaged(t, &i2, true)
|
||||
checkIsReadOnly(t, &i2, true)
|
||||
checkValue(t, &i2, mdmValue)
|
||||
checkValueOk(t, &i2, mdmValue, true)
|
||||
checkCanNotSetValue(t, &i2, userValue, ErrManaged)
|
||||
checkValue(t, &i2, mdmValue) // the value must not be changed
|
||||
checkCanNotClearValue(t, &i2, ErrManaged)
|
||||
|
||||
i2.ClearManaged()
|
||||
// Clearing the managed flag should change the IsManaged and IsReadOnly flags...
|
||||
checkIsManaged(t, &i2, false)
|
||||
checkIsReadOnly(t, &i2, false)
|
||||
// ...but not the value.
|
||||
checkValue(t, &i2, mdmValue)
|
||||
|
||||
// We should be able to change the value after clearing the managed flag.
|
||||
checkSetValue(t, &i2, userValue)
|
||||
checkIsSet(t, &i2, true)
|
||||
checkValue(t, &i2, userValue)
|
||||
checkValueOk(t, &i2, userValue, true)
|
||||
checkEqual(t, i2, i, true)
|
||||
|
||||
i2.SetReadOnly(true)
|
||||
checkIsReadOnly(t, &i2, true)
|
||||
checkIsManaged(t, &i2, false)
|
||||
checkCanNotSetValue(t, &i2, userValue, ErrReadOnly)
|
||||
checkCanNotClearValue(t, &i2, ErrReadOnly)
|
||||
|
||||
i2.SetReadOnly(false)
|
||||
i2.SetDefaultValue(defValue)
|
||||
checkClearValue(t, &i2)
|
||||
checkIsSet(t, &i2, false)
|
||||
checkValue(t, &i2, defValue)
|
||||
checkValueOk(t, &i2, defValue, false)
|
||||
}
|
||||
|
||||
func TestItemView(t *testing.T) {
|
||||
i := ItemOf(&TestBundle{Name: "B1"})
|
||||
|
||||
iv := ItemViewOf(&i)
|
||||
checkIsSet(t, iv, true)
|
||||
checkIsManaged(t, iv, false)
|
||||
checkIsReadOnly(t, iv, false)
|
||||
checkValue(t, iv, TestBundleView{i.Value()})
|
||||
checkValueOk(t, iv, TestBundleView{i.Value()}, true)
|
||||
|
||||
i2 := *iv.AsStruct()
|
||||
checkEqual(t, i, i2, true)
|
||||
i2.SetValue(&TestBundle{Name: "B2"})
|
||||
|
||||
iv2 := ItemViewOf(&i2)
|
||||
checkEqual(t, iv, iv2, false)
|
||||
}
|
||||
|
||||
func TestListView(t *testing.T) {
|
||||
l := ListOf([]int{4, 8, 15, 16, 23, 42}, ReadOnly)
|
||||
|
||||
lv := l.View()
|
||||
checkIsSet(t, lv, true)
|
||||
checkIsManaged(t, lv, false)
|
||||
checkIsReadOnly(t, lv, true)
|
||||
checkValue(t, lv, views.SliceOf(l.Value()))
|
||||
checkValueOk(t, lv, views.SliceOf(l.Value()), true)
|
||||
|
||||
l2 := *lv.AsStruct()
|
||||
checkEqual(t, l, l2, true)
|
||||
}
|
||||
|
||||
func TestStructListView(t *testing.T) {
|
||||
l := StructListOf([]*TestBundle{{Name: "E1"}, {Name: "E2"}}, ReadOnly)
|
||||
|
||||
lv := StructListViewOf(&l)
|
||||
checkIsSet(t, lv, true)
|
||||
checkIsManaged(t, lv, false)
|
||||
checkIsReadOnly(t, lv, true)
|
||||
checkValue(t, lv, views.SliceOfViews(l.Value()))
|
||||
checkValueOk(t, lv, views.SliceOfViews(l.Value()), true)
|
||||
|
||||
l2 := *lv.AsStruct()
|
||||
checkEqual(t, l, l2, true)
|
||||
}
|
||||
|
||||
func TestStructMapView(t *testing.T) {
|
||||
m := StructMapOf(map[string]*TestBundle{
|
||||
"K1": {Name: "E1"},
|
||||
"K2": {Name: "E2"},
|
||||
}, ReadOnly)
|
||||
|
||||
mv := StructMapViewOf(&m)
|
||||
checkIsSet(t, mv, true)
|
||||
checkIsManaged(t, mv, false)
|
||||
checkIsReadOnly(t, mv, true)
|
||||
checkValue(t, *mv.AsStruct(), m.Value())
|
||||
checkValueOk(t, *mv.AsStruct(), m.Value(), true)
|
||||
|
||||
m2 := *mv.AsStruct()
|
||||
checkEqual(t, m, m2, true)
|
||||
}
|
||||
|
||||
// check that the preference types implement the test [pref] interface.
|
||||
var (
|
||||
_ pref[int] = (*Item[int])(nil)
|
||||
_ pref[*TestBundle] = (*Item[*TestBundle])(nil)
|
||||
_ pref[[]int] = (*List[int])(nil)
|
||||
_ pref[[]*TestBundle] = (*StructList[*TestBundle])(nil)
|
||||
_ pref[map[string]*TestBundle] = (*StructMap[string, *TestBundle])(nil)
|
||||
)
|
||||
|
||||
// pref is an interface used by [checkSetValue], [checkClearValue], and similar test
|
||||
// functions that mutate preferences. It is implemented by all preference types, such
|
||||
// as [Item], [List], [StructList], and [StructMap], and provides both read and write
|
||||
// access to the preference's value and state.
|
||||
type pref[T any] interface {
|
||||
prefView[T]
|
||||
SetValue(v T) error
|
||||
ClearValue() error
|
||||
SetDefaultValue(v T)
|
||||
SetManagedValue(v T)
|
||||
ClearManaged()
|
||||
SetReadOnly(readonly bool)
|
||||
}
|
||||
|
||||
// check that the preference view types implement the test [prefView] interface.
|
||||
var (
|
||||
_ prefView[int] = (*Item[int])(nil)
|
||||
_ prefView[TestBundleView] = (*ItemView[*TestBundle, TestBundleView])(nil)
|
||||
_ prefView[views.Slice[int]] = (*ListView[int])(nil)
|
||||
_ prefView[views.SliceView[*TestBundle, TestBundleView]] = (*StructListView[*TestBundle, TestBundleView])(nil)
|
||||
_ prefView[views.MapFn[string, *TestBundle, TestBundleView]] = (*StructMapView[string, *TestBundle, TestBundleView])(nil)
|
||||
)
|
||||
|
||||
// prefView is an interface used by [checkIsSet], [checkIsManaged], and similar non-mutating
|
||||
// test functions. It is implemented by all preference types, such as [Item], [List], [StructList],
|
||||
// and [StructMap], as well as their corresponding views, such as [ItemView], [ListView], [StructListView],
|
||||
// and [StructMapView], and provides read-only access to the preference's value and state.
|
||||
type prefView[T any] interface {
|
||||
IsSet() bool
|
||||
Value() T
|
||||
ValueOk() (T, bool)
|
||||
DefaultValue() T
|
||||
IsManaged() bool
|
||||
IsReadOnly() bool
|
||||
}
|
||||
|
||||
func checkIsSet[T any](tb testing.TB, p prefView[T], wantSet bool) {
|
||||
tb.Helper()
|
||||
if gotSet := p.IsSet(); gotSet != wantSet {
|
||||
tb.Errorf("IsSet: got %v; want %v", gotSet, wantSet)
|
||||
}
|
||||
}
|
||||
|
||||
func checkIsManaged[T any](tb testing.TB, p prefView[T], wantManaged bool) {
|
||||
tb.Helper()
|
||||
if gotManaged := p.IsManaged(); gotManaged != wantManaged {
|
||||
tb.Errorf("IsManaged: got %v; want %v", gotManaged, wantManaged)
|
||||
}
|
||||
}
|
||||
|
||||
func checkIsReadOnly[T any](tb testing.TB, p prefView[T], wantReadOnly bool) {
|
||||
tb.Helper()
|
||||
if gotReadOnly := p.IsReadOnly(); gotReadOnly != wantReadOnly {
|
||||
tb.Errorf("IsReadOnly: got %v; want %v", gotReadOnly, wantReadOnly)
|
||||
}
|
||||
}
|
||||
|
||||
func checkValue[T any](tb testing.TB, p prefView[T], wantValue T) {
|
||||
tb.Helper()
|
||||
if gotValue := p.Value(); !testComparerFor[T]()(gotValue, wantValue) {
|
||||
tb.Errorf("Value: got %v; want %v", gotValue, wantValue)
|
||||
}
|
||||
}
|
||||
|
||||
func checkValueOk[T any](tb testing.TB, p prefView[T], wantValue T, wantOk bool) {
|
||||
tb.Helper()
|
||||
gotValue, gotOk := p.ValueOk()
|
||||
|
||||
if gotOk != wantOk || !testComparerFor[T]()(gotValue, wantValue) {
|
||||
tb.Errorf("ValueOk: got (%v, %v); want (%v, %v)", gotValue, gotOk, wantValue, wantOk)
|
||||
}
|
||||
}
|
||||
|
||||
func checkEqual[T equatable[T]](tb testing.TB, a, b T, wantEqual bool) {
|
||||
tb.Helper()
|
||||
if gotEqual := a.Equal(b); gotEqual != wantEqual {
|
||||
tb.Errorf("Equal: got %v; want %v", gotEqual, wantEqual)
|
||||
}
|
||||
}
|
||||
|
||||
func checkSetValue[T any](tb testing.TB, p pref[T], v T) {
|
||||
tb.Helper()
|
||||
if err := p.SetValue(v); err != nil {
|
||||
tb.Fatalf("SetValue: gotErr %v, wantErr: nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkCanNotSetValue[T any](tb testing.TB, p pref[T], v T, wantErr error) {
|
||||
tb.Helper()
|
||||
if err := p.SetValue(v); err == nil || !errors.Is(err, wantErr) {
|
||||
tb.Fatalf("SetValue: gotErr %v, wantErr: %v", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
func checkClearValue[T any](tb testing.TB, p pref[T]) {
|
||||
tb.Helper()
|
||||
if err := p.ClearValue(); err != nil {
|
||||
tb.Fatalf("ClearValue: gotErr %v, wantErr: nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkCanNotClearValue[T any](tb testing.TB, p pref[T], wantErr error) {
|
||||
tb.Helper()
|
||||
err := p.ClearValue()
|
||||
if err == nil || !errors.Is(err, wantErr) {
|
||||
tb.Fatalf("ClearValue: gotErr %v, wantErr: %v", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
// testComparerFor is like [comparerFor], but uses [reflect.DeepEqual]
|
||||
// unless T is [equatable].
|
||||
func testComparerFor[T any]() func(a, b T) bool {
|
||||
return func(a, b T) bool {
|
||||
switch a := any(a).(type) {
|
||||
case equatable[T]:
|
||||
return a.Equal(b)
|
||||
default:
|
||||
return reflect.DeepEqual(a, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkJSON(tb testing.TB, got, want jsontext.Value) {
|
||||
tb.Helper()
|
||||
got = got.Clone()
|
||||
want = want.Clone()
|
||||
// Compare canonical forms.
|
||||
if err := got.Canonicalize(); err != nil {
|
||||
tb.Error(err)
|
||||
}
|
||||
if err := want.Canonicalize(); err != nil {
|
||||
tb.Error(err)
|
||||
}
|
||||
if bytes.Equal(got, want) {
|
||||
return
|
||||
}
|
||||
|
||||
gotMap := make(map[string]any)
|
||||
if err := jsonv2.Unmarshal(got, &gotMap); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
wantMap := make(map[string]any)
|
||||
if err := jsonv2.Unmarshal(want, &wantMap); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
tb.Errorf("mismatch (-want +got):\n%s", cmp.Diff(wantMap, gotMap))
|
||||
}
|
342
types/prefs/prefs_view_test.go
Normal file
342
types/prefs/prefs_view_test.go
Normal file
@ -0,0 +1,342 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
|
||||
|
||||
package prefs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=TestPrefs,TestBundle,TestValueStruct,TestGenericStruct,TestPrefsGroup -tags=test
|
||||
|
||||
// View returns a readonly view of TestPrefs.
|
||||
func (p *TestPrefs) View() TestPrefsView {
|
||||
return TestPrefsView{ж: p}
|
||||
}
|
||||
|
||||
// TestPrefsView provides a read-only view over TestPrefs.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type TestPrefsView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *TestPrefs
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v TestPrefsView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v TestPrefsView) AsStruct() *TestPrefs {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v TestPrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *TestPrefsView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x TestPrefs
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v TestPrefsView) Int32Item() Item[int32] { return v.ж.Int32Item }
|
||||
func (v TestPrefsView) UInt64Item() Item[uint64] { return v.ж.UInt64Item }
|
||||
func (v TestPrefsView) StringItem1() Item[string] { return v.ж.StringItem1 }
|
||||
func (v TestPrefsView) StringItem2() Item[string] { return v.ж.StringItem2 }
|
||||
func (v TestPrefsView) BoolItem1() Item[bool] { return v.ж.BoolItem1 }
|
||||
func (v TestPrefsView) BoolItem2() Item[bool] { return v.ж.BoolItem2 }
|
||||
func (v TestPrefsView) StringSlice() ListView[string] { return v.ж.StringSlice.View() }
|
||||
func (v TestPrefsView) IntSlice() ListView[int] { return v.ж.IntSlice.View() }
|
||||
func (v TestPrefsView) AddrItem() Item[netip.Addr] { return v.ж.AddrItem }
|
||||
func (v TestPrefsView) StringStringMap() MapView[string, string] { return v.ж.StringStringMap.View() }
|
||||
func (v TestPrefsView) IntStringMap() MapView[int, string] { return v.ж.IntStringMap.View() }
|
||||
func (v TestPrefsView) AddrIntMap() MapView[netip.Addr, int] { return v.ж.AddrIntMap.View() }
|
||||
func (v TestPrefsView) Bundle1() ItemView[*TestBundle, TestBundleView] {
|
||||
return ItemViewOf(&v.ж.Bundle1)
|
||||
}
|
||||
func (v TestPrefsView) Bundle2() ItemView[*TestBundle, TestBundleView] {
|
||||
return ItemViewOf(&v.ж.Bundle2)
|
||||
}
|
||||
func (v TestPrefsView) Generic() ItemView[*TestGenericStruct[int], TestGenericStructView[int]] {
|
||||
return ItemViewOf(&v.ж.Generic)
|
||||
}
|
||||
func (v TestPrefsView) BundleList() StructListView[*TestBundle, TestBundleView] {
|
||||
return StructListViewOf(&v.ж.BundleList)
|
||||
}
|
||||
func (v TestPrefsView) StringBundleMap() StructMapView[string, *TestBundle, TestBundleView] {
|
||||
return StructMapViewOf(&v.ж.StringBundleMap)
|
||||
}
|
||||
func (v TestPrefsView) IntBundleMap() StructMapView[int, *TestBundle, TestBundleView] {
|
||||
return StructMapViewOf(&v.ж.IntBundleMap)
|
||||
}
|
||||
func (v TestPrefsView) AddrBundleMap() StructMapView[netip.Addr, *TestBundle, TestBundleView] {
|
||||
return StructMapViewOf(&v.ж.AddrBundleMap)
|
||||
}
|
||||
func (v TestPrefsView) Group() TestPrefsGroup { return v.ж.Group }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TestPrefsViewNeedsRegeneration = TestPrefs(struct {
|
||||
Int32Item Item[int32]
|
||||
UInt64Item Item[uint64]
|
||||
StringItem1 Item[string]
|
||||
StringItem2 Item[string]
|
||||
BoolItem1 Item[bool]
|
||||
BoolItem2 Item[bool]
|
||||
StringSlice List[string]
|
||||
IntSlice List[int]
|
||||
AddrItem Item[netip.Addr]
|
||||
StringStringMap Map[string, string]
|
||||
IntStringMap Map[int, string]
|
||||
AddrIntMap Map[netip.Addr, int]
|
||||
Bundle1 Item[*TestBundle]
|
||||
Bundle2 Item[*TestBundle]
|
||||
Generic Item[*TestGenericStruct[int]]
|
||||
BundleList StructList[*TestBundle]
|
||||
StringBundleMap StructMap[string, *TestBundle]
|
||||
IntBundleMap StructMap[int, *TestBundle]
|
||||
AddrBundleMap StructMap[netip.Addr, *TestBundle]
|
||||
Group TestPrefsGroup
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of TestBundle.
|
||||
func (p *TestBundle) View() TestBundleView {
|
||||
return TestBundleView{ж: p}
|
||||
}
|
||||
|
||||
// TestBundleView provides a read-only view over TestBundle.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type TestBundleView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *TestBundle
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v TestBundleView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v TestBundleView) AsStruct() *TestBundle {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v TestBundleView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *TestBundleView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x TestBundle
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v TestBundleView) Name() string { return v.ж.Name }
|
||||
func (v TestBundleView) Nested() *TestValueStruct {
|
||||
if v.ж.Nested == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Nested
|
||||
return &x
|
||||
}
|
||||
|
||||
func (v TestBundleView) Equal(v2 TestBundleView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TestBundleViewNeedsRegeneration = TestBundle(struct {
|
||||
Name string
|
||||
Nested *TestValueStruct
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of TestValueStruct.
|
||||
func (p *TestValueStruct) View() TestValueStructView {
|
||||
return TestValueStructView{ж: p}
|
||||
}
|
||||
|
||||
// TestValueStructView provides a read-only view over TestValueStruct.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type TestValueStructView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *TestValueStruct
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v TestValueStructView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v TestValueStructView) AsStruct() *TestValueStruct {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v TestValueStructView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *TestValueStructView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x TestValueStruct
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v TestValueStructView) Value() int { return v.ж.Value }
|
||||
func (v TestValueStructView) Equal(v2 TestValueStructView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TestValueStructViewNeedsRegeneration = TestValueStruct(struct {
|
||||
Value int
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of TestGenericStruct.
|
||||
func (p *TestGenericStruct[T]) View() TestGenericStructView[T] {
|
||||
return TestGenericStructView[T]{ж: p}
|
||||
}
|
||||
|
||||
// TestGenericStructView[T] provides a read-only view over TestGenericStruct[T].
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type TestGenericStructView[T ImmutableType] struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *TestGenericStruct[T]
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v TestGenericStructView[T]) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v TestGenericStructView[T]) AsStruct() *TestGenericStruct[T] {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v TestGenericStructView[T]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *TestGenericStructView[T]) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x TestGenericStruct[T]
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v TestGenericStructView[T]) Value() T { return v.ж.Value }
|
||||
func (v TestGenericStructView[T]) Equal(v2 TestGenericStructView[T]) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
func _TestGenericStructViewNeedsRegeneration[T ImmutableType](TestGenericStruct[T]) {
|
||||
_TestGenericStructViewNeedsRegeneration(struct {
|
||||
Value T
|
||||
}{})
|
||||
}
|
||||
|
||||
// View returns a readonly view of TestPrefsGroup.
|
||||
func (p *TestPrefsGroup) View() TestPrefsGroupView {
|
||||
return TestPrefsGroupView{ж: p}
|
||||
}
|
||||
|
||||
// TestPrefsGroupView provides a read-only view over TestPrefsGroup.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type TestPrefsGroupView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *TestPrefsGroup
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v TestPrefsGroupView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v TestPrefsGroupView) AsStruct() *TestPrefsGroup {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v TestPrefsGroupView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *TestPrefsGroupView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x TestPrefsGroup
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v TestPrefsGroupView) FloatItem() Item[float64] { return v.ж.FloatItem }
|
||||
func (v TestPrefsGroupView) TestStringItem() Item[TestStringType] { return v.ж.TestStringItem }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TestPrefsGroupViewNeedsRegeneration = TestPrefsGroup(struct {
|
||||
FloatItem Item[float64]
|
||||
TestStringItem Item[TestStringType]
|
||||
}{})
|
195
types/prefs/struct_list.go
Normal file
195
types/prefs/struct_list.go
Normal file
@ -0,0 +1,195 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package prefs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
// StructList is a preference type that holds zero or more potentially mutable struct values.
|
||||
type StructList[T views.Cloner[T]] struct {
|
||||
preference[[]T]
|
||||
}
|
||||
|
||||
// StructListOf returns a [StructList] configured with the specified value and [Options].
|
||||
func StructListOf[T views.Cloner[T]](v []T, opts ...Options) StructList[T] {
|
||||
return StructList[T]{preferenceOf(opt.ValueOf(deepCloneSlice(v)), opts...)}
|
||||
}
|
||||
|
||||
// StructListWithOpts returns an unconfigured [StructList] with the specified [Options].
|
||||
func StructListWithOpts[T views.Cloner[T]](opts ...Options) StructList[T] {
|
||||
return StructList[T]{preferenceOf(opt.Value[[]T]{}, opts...)}
|
||||
}
|
||||
|
||||
// SetValue configures the preference with the specified value.
|
||||
// It fails and returns [ErrManaged] if p is a managed preference,
|
||||
// and [ErrReadOnly] if p is a read-only preference.
|
||||
func (l *StructList[T]) SetValue(val []T) error {
|
||||
return l.preference.SetValue(deepCloneSlice(val))
|
||||
}
|
||||
|
||||
// SetManagedValue configures the preference with the specified value
|
||||
// and marks the preference as managed.
|
||||
func (l *StructList[T]) SetManagedValue(val []T) {
|
||||
l.preference.SetManagedValue(deepCloneSlice(val))
|
||||
}
|
||||
|
||||
// Clone returns a copy of l that aliases no memory with l.
|
||||
func (l StructList[T]) Clone() *StructList[T] {
|
||||
res := ptr.To(l)
|
||||
if v, ok := l.s.Value.GetOk(); ok {
|
||||
res.s.Value.Set(deepCloneSlice(v))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Equal reports whether l and l2 are equal.
|
||||
// If the template type T implements an Equal(T) bool method, it will be used
|
||||
// instead of the == operator for value comparison.
|
||||
// It panics if T is not comparable.
|
||||
func (l StructList[T]) Equal(l2 StructList[T]) bool {
|
||||
if l.s.Metadata != l2.s.Metadata {
|
||||
return false
|
||||
}
|
||||
v1, ok1 := l.s.Value.GetOk()
|
||||
v2, ok2 := l2.s.Value.GetOk()
|
||||
if ok1 != ok2 {
|
||||
return false
|
||||
}
|
||||
if ok1 != ok2 {
|
||||
return false
|
||||
}
|
||||
return !ok1 || slices.EqualFunc(v1, v2, comparerFor[T]())
|
||||
}
|
||||
|
||||
func deepCloneSlice[T views.Cloner[T]](s []T) []T {
|
||||
c := make([]T, len(s))
|
||||
for i := range s {
|
||||
c[i] = s[i].Clone()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type equatable[T any] interface {
|
||||
Equal(other T) bool
|
||||
}
|
||||
|
||||
func comparerFor[T any]() func(a, b T) bool {
|
||||
switch t := reflect.TypeFor[T](); {
|
||||
case t.Implements(reflect.TypeFor[equatable[T]]()):
|
||||
return func(a, b T) bool { return any(a).(equatable[T]).Equal(b) }
|
||||
case t.Comparable():
|
||||
return func(a, b T) bool { return any(a) == any(b) }
|
||||
default:
|
||||
panic(fmt.Errorf("%v is not comparable", t))
|
||||
}
|
||||
}
|
||||
|
||||
// StructListView is a read-only view of a [StructList].
|
||||
type StructListView[T views.ViewCloner[T, V], V views.StructView[T]] struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *StructList[T]
|
||||
}
|
||||
|
||||
// StructListViewOf returns a read-only view of l.
|
||||
// It is used by [tailscale.com/cmd/viewer].
|
||||
func StructListViewOf[T views.ViewCloner[T, V], V views.StructView[T]](l *StructList[T]) StructListView[T, V] {
|
||||
return StructListView[T, V]{l}
|
||||
}
|
||||
|
||||
// Valid reports whether the underlying [StructList] is non-nil.
|
||||
func (lv StructListView[T, V]) Valid() bool {
|
||||
return lv.ж != nil
|
||||
}
|
||||
|
||||
// AsStruct implements [views.StructView] by returning a clone of the preference
|
||||
// which aliases no memory with the original.
|
||||
func (lv StructListView[T, V]) AsStruct() *StructList[T] {
|
||||
if lv.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return lv.ж.Clone()
|
||||
}
|
||||
|
||||
// IsSet reports whether the preference has a value set.
|
||||
func (lv StructListView[T, V]) IsSet() bool {
|
||||
return lv.ж.IsSet()
|
||||
}
|
||||
|
||||
// Value returns a read-only view of the value if the preference has a value set.
|
||||
// Otherwise, it returns a read-only view of its default value.
|
||||
func (lv StructListView[T, V]) Value() views.SliceView[T, V] {
|
||||
return views.SliceOfViews(lv.ж.Value())
|
||||
}
|
||||
|
||||
// ValueOk returns a read-only view of the value and true if the preference has a value set.
|
||||
// Otherwise, it returns an invalid view and false.
|
||||
func (lv StructListView[T, V]) ValueOk() (val views.SliceView[T, V], ok bool) {
|
||||
if v, ok := lv.ж.ValueOk(); ok {
|
||||
return views.SliceOfViews(v), true
|
||||
}
|
||||
return views.SliceView[T, V]{}, false
|
||||
}
|
||||
|
||||
// DefaultValue returns a read-only view of the default value of the preference.
|
||||
func (lv StructListView[T, V]) DefaultValue() views.SliceView[T, V] {
|
||||
return views.SliceOfViews(lv.ж.DefaultValue())
|
||||
}
|
||||
|
||||
// IsManaged reports whether the preference is managed via MDM, Group Policy, or similar means.
|
||||
func (lv StructListView[T, V]) IsManaged() bool {
|
||||
return lv.ж.IsManaged()
|
||||
}
|
||||
|
||||
// IsReadOnly reports whether the preference is read-only and cannot be changed by user.
|
||||
func (lv StructListView[T, V]) IsReadOnly() bool {
|
||||
return lv.ж.IsReadOnly()
|
||||
}
|
||||
|
||||
// Equal reports whether iv and iv2 are equal.
|
||||
func (lv StructListView[T, V]) Equal(lv2 StructListView[T, V]) bool {
|
||||
if !lv.Valid() && !lv2.Valid() {
|
||||
return true
|
||||
}
|
||||
if lv.Valid() != lv2.Valid() {
|
||||
return false
|
||||
}
|
||||
return lv.ж.Equal(*lv2.ж)
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (lv StructListView[T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
return lv.ж.MarshalJSONV2(out, opts)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (lv *StructListView[T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
var x StructList[T]
|
||||
if err := x.UnmarshalJSONV2(in, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
lv.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (lv StructListView[T, V]) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(lv) // uses MarshalJSONV2
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (lv *StructListView[T, V]) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, lv) // uses UnmarshalJSONV2
|
||||
}
|
175
types/prefs/struct_map.go
Normal file
175
types/prefs/struct_map.go
Normal file
@ -0,0 +1,175 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package prefs
|
||||
|
||||
import (
|
||||
"maps"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
// StructMap is a preference type that holds potentially mutable key-value pairs.
|
||||
type StructMap[K MapKeyType, V views.Cloner[V]] struct {
|
||||
preference[map[K]V]
|
||||
}
|
||||
|
||||
// StructMapOf returns a [StructMap] configured with the specified value and [Options].
|
||||
func StructMapOf[K MapKeyType, V views.Cloner[V]](v map[K]V, opts ...Options) StructMap[K, V] {
|
||||
return StructMap[K, V]{preferenceOf(opt.ValueOf(deepCloneMap(v)), opts...)}
|
||||
}
|
||||
|
||||
// StructMapWithOpts returns an unconfigured [StructMap] with the specified [Options].
|
||||
func StructMapWithOpts[K MapKeyType, V views.Cloner[V]](opts ...Options) StructMap[K, V] {
|
||||
return StructMap[K, V]{preferenceOf(opt.Value[map[K]V]{}, opts...)}
|
||||
}
|
||||
|
||||
// SetValue configures the preference with the specified value.
|
||||
// It fails and returns [ErrManaged] if p is a managed preference,
|
||||
// and [ErrReadOnly] if p is a read-only preference.
|
||||
func (l *StructMap[K, V]) SetValue(val map[K]V) error {
|
||||
return l.preference.SetValue(deepCloneMap(val))
|
||||
}
|
||||
|
||||
// SetManagedValue configures the preference with the specified value
|
||||
// and marks the preference as managed.
|
||||
func (l *StructMap[K, V]) SetManagedValue(val map[K]V) {
|
||||
l.preference.SetManagedValue(deepCloneMap(val))
|
||||
}
|
||||
|
||||
// Clone returns a copy of m that aliases no memory with m.
|
||||
func (m StructMap[K, V]) Clone() *StructMap[K, V] {
|
||||
res := ptr.To(m)
|
||||
if v, ok := m.s.Value.GetOk(); ok {
|
||||
res.s.Value.Set(deepCloneMap(v))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Equal reports whether m and m2 are equal.
|
||||
// If the template type V implements an Equal(V) bool method, it will be used
|
||||
// instead of the == operator for value comparison.
|
||||
// It panics if T is not comparable.
|
||||
func (m StructMap[K, V]) Equal(m2 StructMap[K, V]) bool {
|
||||
if m.s.Metadata != m2.s.Metadata {
|
||||
return false
|
||||
}
|
||||
v1, ok1 := m.s.Value.GetOk()
|
||||
v2, ok2 := m2.s.Value.GetOk()
|
||||
if ok1 != ok2 {
|
||||
return false
|
||||
}
|
||||
return !ok1 || maps.EqualFunc(v1, v2, comparerFor[V]())
|
||||
}
|
||||
|
||||
func deepCloneMap[K comparable, V views.Cloner[V]](m map[K]V) map[K]V {
|
||||
c := make(map[K]V, len(m))
|
||||
for i := range m {
|
||||
c[i] = m[i].Clone()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// StructMapView is a read-only view of a [StructMap].
|
||||
type StructMapView[K MapKeyType, T views.ViewCloner[T, V], V views.StructView[T]] struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *StructMap[K, T]
|
||||
}
|
||||
|
||||
// StructMapViewOf returns a readonly view of m.
|
||||
// It is used by [tailscale.com/cmd/viewer].
|
||||
func StructMapViewOf[K MapKeyType, T views.ViewCloner[T, V], V views.StructView[T]](m *StructMap[K, T]) StructMapView[K, T, V] {
|
||||
return StructMapView[K, T, V]{m}
|
||||
}
|
||||
|
||||
// Valid reports whether the underlying [StructMap] is non-nil.
|
||||
func (mv StructMapView[K, T, V]) Valid() bool {
|
||||
return mv.ж != nil
|
||||
}
|
||||
|
||||
// AsStruct implements [views.StructView] by returning a clone of the preference
|
||||
// which aliases no memory with the original.
|
||||
func (mv StructMapView[K, T, V]) AsStruct() *StructMap[K, T] {
|
||||
if mv.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return mv.ж.Clone()
|
||||
}
|
||||
|
||||
// IsSet reports whether the preference has a value set.
|
||||
func (mv StructMapView[K, T, V]) IsSet() bool {
|
||||
return mv.ж.IsSet()
|
||||
}
|
||||
|
||||
// Value returns a read-only view of the value if the preference has a value set.
|
||||
// Otherwise, it returns a read-only view of its default value.
|
||||
func (mv StructMapView[K, T, V]) Value() views.MapFn[K, T, V] {
|
||||
return views.MapFnOf(mv.ж.Value(), func(t T) V { return t.View() })
|
||||
}
|
||||
|
||||
// ValueOk returns a read-only view of the value and true if the preference has a value set.
|
||||
// Otherwise, it returns an invalid view and false.
|
||||
func (mv StructMapView[K, T, V]) ValueOk() (val views.MapFn[K, T, V], ok bool) {
|
||||
if v, ok := mv.ж.ValueOk(); ok {
|
||||
return views.MapFnOf(v, func(t T) V { return t.View() }), true
|
||||
}
|
||||
return views.MapFn[K, T, V]{}, false
|
||||
}
|
||||
|
||||
// DefaultValue returns a read-only view of the default value of the preference.
|
||||
func (mv StructMapView[K, T, V]) DefaultValue() views.MapFn[K, T, V] {
|
||||
return views.MapFnOf(mv.ж.DefaultValue(), func(t T) V { return t.View() })
|
||||
}
|
||||
|
||||
// Managed reports whether the preference is managed via MDM, Group Policy, or similar means.
|
||||
func (mv StructMapView[K, T, V]) IsManaged() bool {
|
||||
return mv.ж.IsManaged()
|
||||
}
|
||||
|
||||
// ReadOnly reports whether the preference is read-only and cannot be changed by user.
|
||||
func (mv StructMapView[K, T, V]) IsReadOnly() bool {
|
||||
return mv.ж.IsReadOnly()
|
||||
}
|
||||
|
||||
// Equal reports whether mv and mv2 are equal.
|
||||
func (mv StructMapView[K, T, V]) Equal(mv2 StructMapView[K, T, V]) bool {
|
||||
if !mv.Valid() && !mv2.Valid() {
|
||||
return true
|
||||
}
|
||||
if mv.Valid() != mv2.Valid() {
|
||||
return false
|
||||
}
|
||||
return mv.ж.Equal(*mv2.ж)
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (mv StructMapView[K, T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
return mv.ж.MarshalJSONV2(out, opts)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (mv *StructMapView[K, T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
var x StructMap[K, T]
|
||||
if err := x.UnmarshalJSONV2(in, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
mv.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (mv StructMapView[K, T, V]) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(mv) // uses MarshalJSONV2
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (mv *StructMapView[K, T, V]) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, mv) // uses UnmarshalJSONV2
|
||||
}
|
Loading…
Reference in New Issue
Block a user