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>main
parent
151b77f9d6
commit
af3d3c433b
@ -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
|
||||
} |
||||
@ -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
|
||||
} |
||||
@ -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
|
||||
} |
||||
@ -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 |
||||
} |
||||
@ -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
|
||||
} |
||||
@ -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] |
||||
}{}) |
||||
@ -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] |
||||
}{}) |
||||
@ -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] |
||||
}{}) |
||||
@ -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
|
||||
} |
||||
@ -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 } |
||||
@ -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)) |
||||
} |
||||
@ -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] |
||||
}{}) |
||||
@ -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
|
||||
} |
||||
@ -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