types/prefs: add a package containing generic preference types

This adds a new package containing generic types to be used for defining preference hierarchies.
These include prefs.Item, prefs.List, prefs.StructList, and prefs.StructMap. Each of these types
represents a configurable preference, holding the preference's state, value, and metadata.
The metadata includes the default value (if it differs from the zero value of the Go type)
and flags indicating whether a preference is managed via syspolicy or is hidden/read-only for
another reason. This information can be marshaled and sent to the GUI, CLI and web clients
as a source of truth regarding preference configuration, management, and visibility/mutability states.

We plan to use these types to define device preferences, such as the updater preferences,
the permission mode to be used on Windows with #tailscale/corp#18342, and certain global options
that are currently exposed as tailscaled flags. We also aim to eventually use these types for
profile-local preferences in ipn.Prefs and and as a replacement for ipn.MaskedPrefs.

The generic preference types are compatible with the tailscale.com/cmd/viewer and
tailscale.com/cmd/cloner utilities.

Updates #12736

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl 2024-07-16 17:27:37 -05:00 committed by Nick Khyl
parent 151b77f9d6
commit af3d3c433b
14 changed files with 2877 additions and 0 deletions

178
types/prefs/item.go Normal file
View File

@ -0,0 +1,178 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prefs
import (
"fmt"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
"tailscale.com/util/must"
)
// Item is a single preference item that can be configured.
// T must either be an immutable type or implement the [views.ViewCloner] interface.
type Item[T any] struct {
preference[T]
}
// ItemOf returns an [Item] configured with the specified value and [Options].
func ItemOf[T any](v T, opts ...Options) Item[T] {
return Item[T]{preferenceOf(opt.ValueOf(must.Get(deepClone(v))), opts...)}
}
// ItemWithOpts returns an unconfigured [Item] with the specified [Options].
func ItemWithOpts[T any](opts ...Options) Item[T] {
return Item[T]{preferenceOf(opt.Value[T]{}, opts...)}
}
// SetValue configures the preference with the specified value.
// It fails and returns [ErrManaged] if p is a managed preference,
// and [ErrReadOnly] if p is a read-only preference.
func (i *Item[T]) SetValue(val T) error {
return i.preference.SetValue(must.Get(deepClone(val)))
}
// SetManagedValue configures the preference with the specified value
// and marks the preference as managed.
func (i *Item[T]) SetManagedValue(val T) {
i.preference.SetManagedValue(must.Get(deepClone(val)))
}
// Clone returns a copy of i that aliases no memory with i.
// It is a runtime error to call [Item.Clone] if T contains pointers
// but does not implement [views.Cloner].
func (i Item[T]) Clone() *Item[T] {
res := ptr.To(i)
if v, ok := i.ValueOk(); ok {
res.s.Value.Set(must.Get(deepClone(v)))
}
return res
}
// Equal reports whether i and i2 are equal.
// If the template type T implements an Equal(T) bool method, it will be used
// instead of the == operator for value comparison.
// If T is not comparable, it reports false.
func (i Item[T]) Equal(i2 Item[T]) bool {
if i.s.Metadata != i2.s.Metadata {
return false
}
return i.s.Value.Equal(i2.s.Value)
}
func deepClone[T any](v T) (T, error) {
if c, ok := any(v).(views.Cloner[T]); ok {
return c.Clone(), nil
}
if !views.ContainsPointers[T]() {
return v, nil
}
var zero T
return zero, fmt.Errorf("%T contains pointers, but does not implement Clone", v)
}
// ItemView is a read-only view of an [Item][T], where T is a mutable type
// implementing [views.ViewCloner].
type ItemView[T views.ViewCloner[T, V], V views.StructView[T]] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Item[T]
}
// ItemViewOf returns a read-only view of i.
// It is used by [tailscale.com/cmd/viewer].
func ItemViewOf[T views.ViewCloner[T, V], V views.StructView[T]](i *Item[T]) ItemView[T, V] {
return ItemView[T, V]{i}
}
// Valid reports whether the underlying [Item] is non-nil.
func (iv ItemView[T, V]) Valid() bool {
return iv.ж != nil
}
// AsStruct implements [views.StructView] by returning a clone of the preference
// which aliases no memory with the original.
func (iv ItemView[T, V]) AsStruct() *Item[T] {
if iv.ж == nil {
return nil
}
return iv.ж.Clone()
}
// IsSet reports whether the preference has a value set.
func (iv ItemView[T, V]) IsSet() bool {
return iv.ж.IsSet()
}
// Value returns a read-only view of the value if the preference has a value set.
// Otherwise, it returns a read-only view of its default value.
func (iv ItemView[T, V]) Value() V {
return iv.ж.Value().View()
}
// ValueOk returns a read-only view of the value and true if the preference has a value set.
// Otherwise, it returns an invalid view and false.
func (iv ItemView[T, V]) ValueOk() (val V, ok bool) {
if val, ok := iv.ж.ValueOk(); ok {
return val.View(), true
}
return val, false
}
// DefaultValue returns a read-only view of the default value of the preference.
func (iv ItemView[T, V]) DefaultValue() V {
return iv.ж.DefaultValue().View()
}
// IsManaged reports whether the preference is managed via MDM, Group Policy, or similar means.
func (iv ItemView[T, V]) IsManaged() bool {
return iv.ж.IsManaged()
}
// IsReadOnly reports whether the preference is read-only and cannot be changed by user.
func (iv ItemView[T, V]) IsReadOnly() bool {
return iv.ж.IsReadOnly()
}
// Equal reports whether iv and iv2 are equal.
func (iv ItemView[T, V]) Equal(iv2 ItemView[T, V]) bool {
if !iv.Valid() && !iv2.Valid() {
return true
}
if iv.Valid() != iv2.Valid() {
return false
}
return iv.ж.Equal(*iv2.ж)
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (iv ItemView[T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return iv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (iv *ItemView[T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x Item[T]
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
iv.ж = &x
return nil
}
// MarshalJSON implements [json.Marshaler].
func (iv ItemView[T, V]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(iv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (iv *ItemView[T, V]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, iv) // uses UnmarshalJSONV2
}

183
types/prefs/list.go Normal file
View File

@ -0,0 +1,183 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prefs
import (
"net/netip"
"slices"
"time"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"golang.org/x/exp/constraints"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
)
// BasicType is a constraint that allows types whose underlying type is a predeclared
// boolean, numeric, or string type.
type BasicType interface {
~bool | constraints.Integer | constraints.Float | constraints.Complex | ~string
}
// ImmutableType is a constraint that allows [BasicType]s and certain well-known immutable types.
type ImmutableType interface {
BasicType | time.Time | netip.Addr | netip.Prefix | netip.AddrPort
}
// List is a preference type that holds zero or more values of an [ImmutableType] T.
type List[T ImmutableType] struct {
preference[[]T]
}
// ListOf returns a [List] configured with the specified value and [Options].
func ListOf[T ImmutableType](v []T, opts ...Options) List[T] {
return List[T]{preferenceOf(opt.ValueOf(cloneSlice(v)), opts...)}
}
// ListWithOpts returns an unconfigured [List] with the specified [Options].
func ListWithOpts[T ImmutableType](opts ...Options) List[T] {
return List[T]{preferenceOf(opt.Value[[]T]{}, opts...)}
}
// SetValue configures the preference with the specified value.
// It fails and returns [ErrManaged] if p is a managed preference,
// and [ErrReadOnly] if p is a read-only preference.
func (l *List[T]) SetValue(val []T) error {
return l.preference.SetValue(cloneSlice(val))
}
// SetManagedValue configures the preference with the specified value
// and marks the preference as managed.
func (l *List[T]) SetManagedValue(val []T) {
l.preference.SetManagedValue(cloneSlice(val))
}
// View returns a read-only view of l.
func (l *List[T]) View() ListView[T] {
return ListView[T]{l}
}
// Clone returns a copy of l that aliases no memory with l.
func (l List[T]) Clone() *List[T] {
res := ptr.To(l)
if v, ok := l.s.Value.GetOk(); ok {
res.s.Value.Set(append(v[:0:0], v...))
}
return res
}
// Equal reports whether l and l2 are equal.
func (l List[T]) Equal(l2 List[T]) bool {
if l.s.Metadata != l2.s.Metadata {
return false
}
v1, ok1 := l.s.Value.GetOk()
v2, ok2 := l2.s.Value.GetOk()
if ok1 != ok2 {
return false
}
return !ok1 || slices.Equal(v1, v2)
}
func cloneSlice[T ImmutableType](s []T) []T {
c := make([]T, len(s))
copy(c, s)
return c
}
// ListView is a read-only view of a [List].
type ListView[T ImmutableType] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *List[T]
}
// Valid reports whether the underlying [List] is non-nil.
func (lv ListView[T]) Valid() bool {
return lv.ж != nil
}
// AsStruct implements [views.StructView] by returning a clone of the [List]
// which aliases no memory with the original.
func (lv ListView[T]) AsStruct() *List[T] {
if lv.ж == nil {
return nil
}
return lv.ж.Clone()
}
// IsSet reports whether the preference has a value set.
func (lv ListView[T]) IsSet() bool {
return lv.ж.IsSet()
}
// Value returns a read-only view of the value if the preference has a value set.
// Otherwise, it returns a read-only view of its default value.
func (lv ListView[T]) Value() views.Slice[T] {
return views.SliceOf(lv.ж.Value())
}
// ValueOk returns a read-only view of the value and true if the preference has a value set.
// Otherwise, it returns an invalid view and false.
func (lv ListView[T]) ValueOk() (val views.Slice[T], ok bool) {
if v, ok := lv.ж.ValueOk(); ok {
return views.SliceOf(v), true
}
return views.Slice[T]{}, false
}
// DefaultValue returns a read-only view of the default value of the preference.
func (lv ListView[T]) DefaultValue() views.Slice[T] {
return views.SliceOf(lv.ж.DefaultValue())
}
// IsManaged reports whether the preference is managed via MDM, Group Policy, or similar means.
func (lv ListView[T]) IsManaged() bool {
return lv.ж.IsManaged()
}
// IsReadOnly reports whether the preference is read-only and cannot be changed by user.
func (lv ListView[T]) IsReadOnly() bool {
return lv.ж.IsReadOnly()
}
// Equal reports whether lv and lv2 are equal.
func (lv ListView[T]) Equal(lv2 ListView[T]) bool {
if !lv.Valid() && !lv2.Valid() {
return true
}
if lv.Valid() != lv2.Valid() {
return false
}
return lv.ж.Equal(*lv2.ж)
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (lv ListView[T]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return lv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (lv *ListView[T]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x List[T]
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
lv.ж = &x
return nil
}
// MarshalJSON implements [json.Marshaler].
func (lv ListView[T]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(lv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (lv *ListView[T]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, lv) // uses UnmarshalJSONV2
}

159
types/prefs/map.go Normal file
View File

@ -0,0 +1,159 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prefs
import (
"maps"
"net/netip"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"golang.org/x/exp/constraints"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
)
// MapKeyType is a constraint allowing types that can be used as [Map] and [StructMap] keys.
// To satisfy this requirement, a type must be comparable and must encode as a JSON string.
// See [jsonv2.Marshal] for more details.
type MapKeyType interface {
~string | constraints.Integer | netip.Addr | netip.Prefix | netip.AddrPort
}
// Map is a preference type that holds immutable key-value pairs.
type Map[K MapKeyType, V ImmutableType] struct {
preference[map[K]V]
}
// MapOf returns a map configured with the specified value and [Options].
func MapOf[K MapKeyType, V ImmutableType](v map[K]V, opts ...Options) Map[K, V] {
return Map[K, V]{preferenceOf(opt.ValueOf(v), opts...)}
}
// MapWithOpts returns an unconfigured [Map] with the specified [Options].
func MapWithOpts[K MapKeyType, V ImmutableType](opts ...Options) Map[K, V] {
return Map[K, V]{preferenceOf(opt.Value[map[K]V]{}, opts...)}
}
// View returns a read-only view of m.
func (m *Map[K, V]) View() MapView[K, V] {
return MapView[K, V]{m}
}
// Clone returns a copy of m that aliases no memory with m.
func (m Map[K, V]) Clone() *Map[K, V] {
res := ptr.To(m)
if v, ok := m.s.Value.GetOk(); ok {
res.s.Value.Set(maps.Clone(v))
}
return res
}
// Equal reports whether m and m2 are equal.
func (m Map[K, V]) Equal(m2 Map[K, V]) bool {
if m.s.Metadata != m2.s.Metadata {
return false
}
v1, ok1 := m.s.Value.GetOk()
v2, ok2 := m2.s.Value.GetOk()
if ok1 != ok2 {
return false
}
return !ok1 || maps.Equal(v1, v2)
}
// MapView is a read-only view of a [Map].
type MapView[K MapKeyType, V ImmutableType] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Map[K, V]
}
// Valid reports whether the underlying [Map] is non-nil.
func (mv MapView[K, V]) Valid() bool {
return mv.ж != nil
}
// AsStruct implements [views.StructView] by returning a clone of the [Map]
// which aliases no memory with the original.
func (mv MapView[K, V]) AsStruct() *Map[K, V] {
if mv.ж == nil {
return nil
}
return mv.ж.Clone()
}
// IsSet reports whether the preference has a value set.
func (mv MapView[K, V]) IsSet() bool {
return mv.ж.IsSet()
}
// Value returns a read-only view of the value if the preference has a value set.
// Otherwise, it returns a read-only view of its default value.
func (mv MapView[K, V]) Value() views.Map[K, V] {
return views.MapOf(mv.ж.Value())
}
// ValueOk returns a read-only view of the value and true if the preference has a value set.
// Otherwise, it returns an invalid view and false.
func (mv MapView[K, V]) ValueOk() (val views.Map[K, V], ok bool) {
if v, ok := mv.ж.ValueOk(); ok {
return views.MapOf(v), true
}
return views.Map[K, V]{}, false
}
// DefaultValue returns a read-only view of the default value of the preference.
func (mv MapView[K, V]) DefaultValue() views.Map[K, V] {
return views.MapOf(mv.ж.DefaultValue())
}
// Managed reports whether the preference is managed via MDM, Group Policy, or similar means.
func (mv MapView[K, V]) Managed() bool {
return mv.ж.IsManaged()
}
// ReadOnly reports whether the preference is read-only and cannot be changed by user.
func (mv MapView[K, V]) ReadOnly() bool {
return mv.ж.IsReadOnly()
}
// Equal reports whether mv and mv2 are equal.
func (mv MapView[K, V]) Equal(mv2 MapView[K, V]) bool {
if !mv.Valid() && !mv2.Valid() {
return true
}
if mv.Valid() != mv2.Valid() {
return false
}
return mv.ж.Equal(*mv2.ж)
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (mv MapView[K, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return mv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (mv *MapView[K, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x Map[K, V]
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
mv.ж = &x
return nil
}
// MarshalJSON implements [json.Marshaler].
func (mv MapView[K, V]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(mv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (mv *MapView[K, V]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, mv) // uses UnmarshalJSONV2
}

22
types/prefs/options.go Normal file
View File

@ -0,0 +1,22 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prefs
// Options are used to configure additional parameters of a preference.
type Options func(s *metadata)
var (
// ReadOnly is an option that marks preference as read-only.
ReadOnly Options = markReadOnly
// Managed is an option that marks preference as managed.
Managed Options = markManaged
)
func markReadOnly(s *metadata) {
s.ReadOnly = true
}
func markManaged(s *metadata) {
s.Managed = true
}

179
types/prefs/prefs.go Normal file
View File

@ -0,0 +1,179 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package prefs contains types and functions to work with arbitrary
// preference hierarchies.
//
// Specifically, the package provides [Item], [List], [Map], [StructList] and [StructMap]
// types which represent individual preferences in a user-defined prefs struct.
// A valid prefs struct must contain one or more exported fields of the preference types,
// either directly or within nested structs, but not pointers to these types.
// Additionally to preferences, a prefs struct may contain any number of
// non-preference fields that will be marshalled and unmarshalled but are
// otherwise ignored by the prefs package.
//
// The preference types are compatible with the [tailscale.com/cmd/viewer] and
// [tailscale.com/cmd/cloner] utilities. It is recommended to generate a read-only view
// of the user-defined prefs structure and use it in place of prefs whenever the prefs
// should not be modified.
package prefs
import (
"errors"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/opt"
)
var (
// ErrManaged is the error returned when attempting to modify a managed preference.
ErrManaged = errors.New("cannot modify a managed preference")
// ErrReadOnly is the error returned when attempting to modify a readonly preference.
ErrReadOnly = errors.New("cannot modify a readonly preference")
)
// metadata holds type-agnostic preference metadata.
type metadata struct {
// Managed indicates whether the preference is managed via MDM, Group Policy, or other means.
Managed bool `json:",omitzero"`
// ReadOnly indicates whether the preference is read-only due to any other reasons,
// such as user's access rights.
ReadOnly bool `json:",omitzero"`
}
// serializable is a JSON-serializable preference data.
type serializable[T any] struct {
// Value is an optional preference value that is set when the preference is
// configured by the user or managed by an admin.
Value opt.Value[T] `json:",omitzero"`
// Default is the default preference value to be used
// when the preference has not been configured.
Default T `json:",omitzero"`
// Metadata is any additional type-agnostic preference metadata to be serialized.
Metadata metadata `json:",inline"`
}
// preference is an embeddable type that provides a common implementation for
// concrete preference types, such as [Item], [List], [Map], [StructList] and [StructMap].
type preference[T any] struct {
s serializable[T]
}
// preferenceOf returns a preference with the specified value and/or [Options].
func preferenceOf[T any](v opt.Value[T], opts ...Options) preference[T] {
var m metadata
for _, o := range opts {
o(&m)
}
return preference[T]{serializable[T]{Value: v, Metadata: m}}
}
// IsSet reports whether p has a value set.
func (p preference[T]) IsSet() bool {
return p.s.Value.IsSet()
}
// Value returns the value of p if the preference has a value set.
// Otherwise, it returns its default value.
func (p preference[T]) Value() T {
val, _ := p.ValueOk()
return val
}
// ValueOk returns the value of p and true if the preference has a value set.
// Otherwise, it returns its default value and false.
func (p preference[T]) ValueOk() (val T, ok bool) {
if val, ok = p.s.Value.GetOk(); ok {
return val, true
}
return p.DefaultValue(), false
}
// SetValue configures the preference with the specified value.
// It fails and returns [ErrManaged] if p is a managed preference,
// and [ErrReadOnly] if p is a read-only preference.
func (p *preference[T]) SetValue(val T) error {
switch {
case p.s.Metadata.Managed:
return ErrManaged
case p.s.Metadata.ReadOnly:
return ErrReadOnly
default:
p.s.Value.Set(val)
return nil
}
}
// ClearValue resets the preference to an unconfigured state.
// It fails and returns [ErrManaged] if p is a managed preference,
// and [ErrReadOnly] if p is a read-only preference.
func (p *preference[T]) ClearValue() error {
switch {
case p.s.Metadata.Managed:
return ErrManaged
case p.s.Metadata.ReadOnly:
return ErrReadOnly
default:
p.s.Value.Clear()
return nil
}
}
// DefaultValue returns the default value of p.
func (p preference[T]) DefaultValue() T {
return p.s.Default
}
// SetDefaultValue sets the default value of p.
func (p *preference[T]) SetDefaultValue(def T) {
p.s.Default = def
}
// IsManaged reports whether p is managed via MDM, Group Policy, or similar means.
func (p preference[T]) IsManaged() bool {
return p.s.Metadata.Managed
}
// SetManagedValue configures the preference with the specified value
// and marks the preference as managed.
func (p *preference[T]) SetManagedValue(val T) {
p.s.Value.Set(val)
p.s.Metadata.Managed = true
}
// ClearManaged clears the managed flag of the preference without altering its value.
func (p *preference[T]) ClearManaged() {
p.s.Metadata.Managed = false
}
// IsReadOnly reports whether p is read-only and cannot be changed by user.
func (p preference[T]) IsReadOnly() bool {
return p.s.Metadata.ReadOnly || p.s.Metadata.Managed
}
// SetReadOnly sets the read-only status of p, preventing changes by a user if set to true.
func (p *preference[T]) SetReadOnly(readonly bool) {
p.s.Metadata.ReadOnly = readonly
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (p preference[T]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return jsonv2.MarshalEncode(out, &p.s, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (p *preference[T]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
return jsonv2.UnmarshalDecode(in, &p.s, opts)
}
// MarshalJSON implements [json.Marshaler].
func (p preference[T]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(p) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (p *preference[T]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
}

View File

@ -0,0 +1,130 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package prefs
import (
"net/netip"
"tailscale.com/types/ptr"
)
// Clone makes a deep copy of TestPrefs.
// The result aliases no memory with the original.
func (src *TestPrefs) Clone() *TestPrefs {
if src == nil {
return nil
}
dst := new(TestPrefs)
*dst = *src
dst.StringSlice = *src.StringSlice.Clone()
dst.IntSlice = *src.IntSlice.Clone()
dst.StringStringMap = *src.StringStringMap.Clone()
dst.IntStringMap = *src.IntStringMap.Clone()
dst.AddrIntMap = *src.AddrIntMap.Clone()
dst.Bundle1 = *src.Bundle1.Clone()
dst.Bundle2 = *src.Bundle2.Clone()
dst.Generic = *src.Generic.Clone()
dst.BundleList = *src.BundleList.Clone()
dst.StringBundleMap = *src.StringBundleMap.Clone()
dst.IntBundleMap = *src.IntBundleMap.Clone()
dst.AddrBundleMap = *src.AddrBundleMap.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TestPrefsCloneNeedsRegeneration = TestPrefs(struct {
Int32Item Item[int32]
UInt64Item Item[uint64]
StringItem1 Item[string]
StringItem2 Item[string]
BoolItem1 Item[bool]
BoolItem2 Item[bool]
StringSlice List[string]
IntSlice List[int]
AddrItem Item[netip.Addr]
StringStringMap Map[string, string]
IntStringMap Map[int, string]
AddrIntMap Map[netip.Addr, int]
Bundle1 Item[*TestBundle]
Bundle2 Item[*TestBundle]
Generic Item[*TestGenericStruct[int]]
BundleList StructList[*TestBundle]
StringBundleMap StructMap[string, *TestBundle]
IntBundleMap StructMap[int, *TestBundle]
AddrBundleMap StructMap[netip.Addr, *TestBundle]
Group TestPrefsGroup
}{})
// Clone makes a deep copy of TestBundle.
// The result aliases no memory with the original.
func (src *TestBundle) Clone() *TestBundle {
if src == nil {
return nil
}
dst := new(TestBundle)
*dst = *src
if dst.Nested != nil {
dst.Nested = ptr.To(*src.Nested)
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TestBundleCloneNeedsRegeneration = TestBundle(struct {
Name string
Nested *TestValueStruct
}{})
// Clone makes a deep copy of TestValueStruct.
// The result aliases no memory with the original.
func (src *TestValueStruct) Clone() *TestValueStruct {
if src == nil {
return nil
}
dst := new(TestValueStruct)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TestValueStructCloneNeedsRegeneration = TestValueStruct(struct {
Value int
}{})
// Clone makes a deep copy of TestGenericStruct.
// The result aliases no memory with the original.
func (src *TestGenericStruct[T]) Clone() *TestGenericStruct[T] {
if src == nil {
return nil
}
dst := new(TestGenericStruct[T])
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
func _TestGenericStructCloneNeedsRegeneration[T ImmutableType](TestGenericStruct[T]) {
_TestGenericStructCloneNeedsRegeneration(struct {
Value T
}{})
}
// Clone makes a deep copy of TestPrefsGroup.
// The result aliases no memory with the original.
func (src *TestPrefsGroup) Clone() *TestPrefsGroup {
if src == nil {
return nil
}
dst := new(TestPrefsGroup)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TestPrefsGroupCloneNeedsRegeneration = TestPrefsGroup(struct {
FloatItem Item[float64]
TestStringItem Item[TestStringType]
}{})

View File

@ -0,0 +1,99 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale.com/cmd/cloner; DO NOT EDIT.
package prefs_example
import (
"net/netip"
"tailscale.com/drive"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/prefs"
"tailscale.com/types/preftype"
)
// Clone makes a deep copy of Prefs.
// The result aliases no memory with the original.
func (src *Prefs) Clone() *Prefs {
if src == nil {
return nil
}
dst := new(Prefs)
*dst = *src
dst.AdvertiseTags = *src.AdvertiseTags.Clone()
dst.AdvertiseRoutes = *src.AdvertiseRoutes.Clone()
dst.DriveShares = *src.DriveShares.Clone()
dst.Persist = src.Persist.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PrefsCloneNeedsRegeneration = Prefs(struct {
ControlURL prefs.Item[string]
RouteAll prefs.Item[bool]
ExitNodeID prefs.Item[tailcfg.StableNodeID]
ExitNodeIP prefs.Item[netip.Addr]
ExitNodePrior tailcfg.StableNodeID
ExitNodeAllowLANAccess prefs.Item[bool]
CorpDNS prefs.Item[bool]
RunSSH prefs.Item[bool]
RunWebClient prefs.Item[bool]
WantRunning prefs.Item[bool]
LoggedOut prefs.Item[bool]
ShieldsUp prefs.Item[bool]
AdvertiseTags prefs.List[string]
Hostname prefs.Item[string]
NotepadURLs prefs.Item[bool]
ForceDaemon prefs.Item[bool]
Egg prefs.Item[bool]
AdvertiseRoutes prefs.List[netip.Prefix]
NoSNAT prefs.Item[bool]
NoStatefulFiltering prefs.Item[opt.Bool]
NetfilterMode prefs.Item[preftype.NetfilterMode]
OperatorUser prefs.Item[string]
ProfileName prefs.Item[string]
AutoUpdate AutoUpdatePrefs
AppConnector AppConnectorPrefs
PostureChecking prefs.Item[bool]
NetfilterKind prefs.Item[string]
DriveShares prefs.StructList[*drive.Share]
AllowSingleHosts prefs.Item[marshalAsTrueInJSON]
Persist *persist.Persist
}{})
// Clone makes a deep copy of AutoUpdatePrefs.
// The result aliases no memory with the original.
func (src *AutoUpdatePrefs) Clone() *AutoUpdatePrefs {
if src == nil {
return nil
}
dst := new(AutoUpdatePrefs)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _AutoUpdatePrefsCloneNeedsRegeneration = AutoUpdatePrefs(struct {
Check prefs.Item[bool]
Apply prefs.Item[opt.Bool]
}{})
// Clone makes a deep copy of AppConnectorPrefs.
// The result aliases no memory with the original.
func (src *AppConnectorPrefs) Clone() *AppConnectorPrefs {
if src == nil {
return nil
}
dst := new(AppConnectorPrefs)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _AppConnectorPrefsCloneNeedsRegeneration = AppConnectorPrefs(struct {
Advertise prefs.Item[bool]
}{})

View File

@ -0,0 +1,239 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
package prefs_example
import (
"encoding/json"
"errors"
"net/netip"
"tailscale.com/drive"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/prefs"
"tailscale.com/types/preftype"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Prefs,AutoUpdatePrefs,AppConnectorPrefs
// View returns a readonly view of Prefs.
func (p *Prefs) View() PrefsView {
return PrefsView{ж: p}
}
// PrefsView provides a read-only view over Prefs.
//
// Its methods should only be called if `Valid()` returns true.
type PrefsView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Prefs
}
// Valid reports whether underlying value is non-nil.
func (v PrefsView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v PrefsView) AsStruct() *Prefs {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v PrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *PrefsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Prefs
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v PrefsView) ControlURL() prefs.Item[string] { return v.ж.ControlURL }
func (v PrefsView) RouteAll() prefs.Item[bool] { return v.ж.RouteAll }
func (v PrefsView) ExitNodeID() prefs.Item[tailcfg.StableNodeID] { return v.ж.ExitNodeID }
func (v PrefsView) ExitNodeIP() prefs.Item[netip.Addr] { return v.ж.ExitNodeIP }
func (v PrefsView) ExitNodePrior() tailcfg.StableNodeID { return v.ж.ExitNodePrior }
func (v PrefsView) ExitNodeAllowLANAccess() prefs.Item[bool] { return v.ж.ExitNodeAllowLANAccess }
func (v PrefsView) CorpDNS() prefs.Item[bool] { return v.ж.CorpDNS }
func (v PrefsView) RunSSH() prefs.Item[bool] { return v.ж.RunSSH }
func (v PrefsView) RunWebClient() prefs.Item[bool] { return v.ж.RunWebClient }
func (v PrefsView) WantRunning() prefs.Item[bool] { return v.ж.WantRunning }
func (v PrefsView) LoggedOut() prefs.Item[bool] { return v.ж.LoggedOut }
func (v PrefsView) ShieldsUp() prefs.Item[bool] { return v.ж.ShieldsUp }
func (v PrefsView) AdvertiseTags() prefs.ListView[string] { return v.ж.AdvertiseTags.View() }
func (v PrefsView) Hostname() prefs.Item[string] { return v.ж.Hostname }
func (v PrefsView) NotepadURLs() prefs.Item[bool] { return v.ж.NotepadURLs }
func (v PrefsView) ForceDaemon() prefs.Item[bool] { return v.ж.ForceDaemon }
func (v PrefsView) Egg() prefs.Item[bool] { return v.ж.Egg }
func (v PrefsView) AdvertiseRoutes() prefs.ListView[netip.Prefix] { return v.ж.AdvertiseRoutes.View() }
func (v PrefsView) NoSNAT() prefs.Item[bool] { return v.ж.NoSNAT }
func (v PrefsView) NoStatefulFiltering() prefs.Item[opt.Bool] { return v.ж.NoStatefulFiltering }
func (v PrefsView) NetfilterMode() prefs.Item[preftype.NetfilterMode] { return v.ж.NetfilterMode }
func (v PrefsView) OperatorUser() prefs.Item[string] { return v.ж.OperatorUser }
func (v PrefsView) ProfileName() prefs.Item[string] { return v.ж.ProfileName }
func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate }
func (v PrefsView) AppConnector() AppConnectorPrefs { return v.ж.AppConnector }
func (v PrefsView) PostureChecking() prefs.Item[bool] { return v.ж.PostureChecking }
func (v PrefsView) NetfilterKind() prefs.Item[string] { return v.ж.NetfilterKind }
func (v PrefsView) DriveShares() prefs.StructListView[*drive.Share, drive.ShareView] {
return prefs.StructListViewOf(&v.ж.DriveShares)
}
func (v PrefsView) AllowSingleHosts() prefs.Item[marshalAsTrueInJSON] { return v.ж.AllowSingleHosts }
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PrefsViewNeedsRegeneration = Prefs(struct {
ControlURL prefs.Item[string]
RouteAll prefs.Item[bool]
ExitNodeID prefs.Item[tailcfg.StableNodeID]
ExitNodeIP prefs.Item[netip.Addr]
ExitNodePrior tailcfg.StableNodeID
ExitNodeAllowLANAccess prefs.Item[bool]
CorpDNS prefs.Item[bool]
RunSSH prefs.Item[bool]
RunWebClient prefs.Item[bool]
WantRunning prefs.Item[bool]
LoggedOut prefs.Item[bool]
ShieldsUp prefs.Item[bool]
AdvertiseTags prefs.List[string]
Hostname prefs.Item[string]
NotepadURLs prefs.Item[bool]
ForceDaemon prefs.Item[bool]
Egg prefs.Item[bool]
AdvertiseRoutes prefs.List[netip.Prefix]
NoSNAT prefs.Item[bool]
NoStatefulFiltering prefs.Item[opt.Bool]
NetfilterMode prefs.Item[preftype.NetfilterMode]
OperatorUser prefs.Item[string]
ProfileName prefs.Item[string]
AutoUpdate AutoUpdatePrefs
AppConnector AppConnectorPrefs
PostureChecking prefs.Item[bool]
NetfilterKind prefs.Item[string]
DriveShares prefs.StructList[*drive.Share]
AllowSingleHosts prefs.Item[marshalAsTrueInJSON]
Persist *persist.Persist
}{})
// View returns a readonly view of AutoUpdatePrefs.
func (p *AutoUpdatePrefs) View() AutoUpdatePrefsView {
return AutoUpdatePrefsView{ж: p}
}
// AutoUpdatePrefsView provides a read-only view over AutoUpdatePrefs.
//
// Its methods should only be called if `Valid()` returns true.
type AutoUpdatePrefsView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *AutoUpdatePrefs
}
// Valid reports whether underlying value is non-nil.
func (v AutoUpdatePrefsView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v AutoUpdatePrefsView) AsStruct() *AutoUpdatePrefs {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v AutoUpdatePrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *AutoUpdatePrefsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x AutoUpdatePrefs
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v AutoUpdatePrefsView) Check() prefs.Item[bool] { return v.ж.Check }
func (v AutoUpdatePrefsView) Apply() prefs.Item[opt.Bool] { return v.ж.Apply }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _AutoUpdatePrefsViewNeedsRegeneration = AutoUpdatePrefs(struct {
Check prefs.Item[bool]
Apply prefs.Item[opt.Bool]
}{})
// View returns a readonly view of AppConnectorPrefs.
func (p *AppConnectorPrefs) View() AppConnectorPrefsView {
return AppConnectorPrefsView{ж: p}
}
// AppConnectorPrefsView provides a read-only view over AppConnectorPrefs.
//
// Its methods should only be called if `Valid()` returns true.
type AppConnectorPrefsView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *AppConnectorPrefs
}
// Valid reports whether underlying value is non-nil.
func (v AppConnectorPrefsView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v AppConnectorPrefsView) AsStruct() *AppConnectorPrefs {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v AppConnectorPrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *AppConnectorPrefsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x AppConnectorPrefs
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v AppConnectorPrefsView) Advertise() prefs.Item[bool] { return v.ж.Advertise }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _AppConnectorPrefsViewNeedsRegeneration = AppConnectorPrefs(struct {
Advertise prefs.Item[bool]
}{})

View File

@ -0,0 +1,140 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prefs_example
import (
"fmt"
"net/netip"
"tailscale.com/ipn"
"tailscale.com/types/prefs"
)
func ExamplePrefs_AdvertiseRoutes_setValue() {
p := &Prefs{}
// Initially, preferences are not configured.
fmt.Println("IsSet:", p.AdvertiseRoutes.IsSet()) // prints false
// And the Value method returns the default (or zero) value.
fmt.Println("Initial:", p.AdvertiseRoutes.Value()) // prints []
// Preferences can be configured with user-provided values using the
// SetValue method. It may fail if the preference is managed via syspolicy
// or is otherwise read-only.
routes := []netip.Prefix{netip.MustParsePrefix("192.168.1.1/24")}
if err := p.AdvertiseRoutes.SetValue(routes); err != nil {
// This block is never executed in the example because the
// AdvertiseRoutes preference is neither managed nor read-only.
fmt.Println("SetValue:", err)
}
fmt.Println("IsSet:", p.AdvertiseRoutes.IsSet()) // prints true
fmt.Println("Value:", p.AdvertiseRoutes.Value()) // prints 192.168.1.1/24
// Preference values are copied on use; you cannot not modify them after they are set.
routes[0] = netip.MustParsePrefix("10.10.10.0/24") // this has no effect
fmt.Println("Unchanged:", p.AdvertiseRoutes.Value()) // still prints 192.168.1.1/24
// If necessary, the value can be changed by calling the SetValue method again.
p.AdvertiseRoutes.SetValue(routes)
fmt.Println("Changed:", p.AdvertiseRoutes.Value()) // prints 10.10.10.0/24
// The following code is fine when defining default or baseline prefs, or
// in tests. However, assigning to a preference field directly overwrites
// syspolicy-managed values and metadata, so it should generally be avoided
// when working with the actual profile or device preferences.
// It is caller's responsibility to use the mutable Prefs struct correctly.
defaults := &Prefs{WantRunning: prefs.ItemOf(true)}
defaults.CorpDNS = prefs.Item[bool]{}
defaults.ExitNodeAllowLANAccess = prefs.ItemOf(true)
_, _, _ = defaults.WantRunning, defaults.CorpDNS, defaults.ExitNodeAllowLANAccess
// In most contexts, preferences should only be read and never mutated.
// To make it easier to enforce this guarantee, a view type generated with
// [tailscale.com/cmd/viewer] can be used instead of the mutable Prefs struct.
// Preferences accessed via a view have the same set of non-mutating
// methods as the underlying preferences but do not expose [prefs.Item.SetValue] or
// other methods that modify the preference's value or state.
v := p.View()
// Additionally, non-mutating methods like [prefs.ItemView.Value] and [prefs.ItemView.ValueOk]
// return read-only views of the underlying values instead of the actual potentially mutable values.
// For example, on the next line Value() returns a views.Slice[netip.Prefix], not a []netip.Prefix.
_ = v.AdvertiseRoutes().Value()
fmt.Println("Via View:", v.AdvertiseRoutes().Value().At(0)) // prints 10.10.10.0/24
fmt.Println("IsSet:", v.AdvertiseRoutes().IsSet()) // prints true
fmt.Println("IsManaged:", v.AdvertiseRoutes().IsManaged()) // prints false
fmt.Println("IsReadOnly:", v.AdvertiseRoutes().IsReadOnly()) // prints false
// Output:
// IsSet: false
// Initial: []
// IsSet: true
// Value: [192.168.1.1/24]
// Unchanged: [192.168.1.1/24]
// Changed: [10.10.10.0/24]
// Via View: 10.10.10.0/24
// IsSet: true
// IsManaged: false
// IsReadOnly: false
}
func ExamplePrefs_ControlURL_setDefaultValue() {
p := &Prefs{}
v := p.View()
// We can set default values for preferences when their default values
// should differ from the zero values of the corresponding Go types.
//
// Note that in this example, we configure preferences via a mutable
// [Prefs] struct but fetch values via a read-only [PrefsView].
// Typically, we set and get preference values in different parts
// of the codebase.
p.ControlURL.SetDefaultValue(ipn.DefaultControlURL)
// The default value is used if the preference is not configured...
fmt.Println("Default:", v.ControlURL().Value())
p.ControlURL.SetValue("https://control.example.com")
fmt.Println("User Set:", v.ControlURL().Value())
// ...including when it has been reset.
p.ControlURL.ClearValue()
fmt.Println("Reset to Default:", v.ControlURL().Value())
// Output:
// Default: https://controlplane.tailscale.com
// User Set: https://control.example.com
// Reset to Default: https://controlplane.tailscale.com
}
func ExamplePrefs_ExitNodeID_setManagedValue() {
p := &Prefs{}
v := p.View()
// We can mark preferences as being managed via syspolicy (e.g., via GP/MDM)
// by setting its managed value.
//
// Note that in this example, we enforce syspolicy-managed values
// via a mutable [Prefs] struct but fetch values via a read-only [PrefsView].
// This is typically spread throughout the codebase.
p.ExitNodeID.SetManagedValue("ManagedExitNode")
// Marking a preference as managed prevents it from being changed by the user.
if err := p.ExitNodeID.SetValue("CustomExitNode"); err != nil {
fmt.Println("SetValue:", err) // reports an error
}
fmt.Println("Exit Node:", v.ExitNodeID().Value()) // prints ManagedExitNode
// Clients can hide or disable preferences that are managed or read-only.
fmt.Println("IsManaged:", v.ExitNodeID().IsManaged()) // prints true
fmt.Println("IsReadOnly:", v.ExitNodeID().IsReadOnly()) // prints true; managed preferences are always read-only.
// ClearManaged is called when the preference is no longer managed,
// allowing the user to change it.
p.ExitNodeID.ClearManaged()
fmt.Println("IsManaged:", v.ExitNodeID().IsManaged()) // prints false
fmt.Println("IsReadOnly:", v.ExitNodeID().IsReadOnly()) // prints false
// Output:
// SetValue: cannot modify a managed preference
// Exit Node: ManagedExitNode
// IsManaged: true
// IsReadOnly: true
// IsManaged: false
// IsReadOnly: false
}

View File

@ -0,0 +1,166 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package prefs_example contains a [Prefs] type, which is like [tailscale.com/ipn.Prefs],
// but uses the [prefs] package to enhance individual preferences with state and metadata.
//
// It also includes testable examples utilizing the [Prefs] type.
// We made it a separate package to avoid circular dependencies
// and due to limitations in [tailscale.com/cmd/viewer] when
// generating code for test packages.
package prefs_example
import (
"net/netip"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/drive"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/prefs"
"tailscale.com/types/preftype"
)
//go:generate go run tailscale.com/cmd/viewer --type=Prefs,AutoUpdatePrefs,AppConnectorPrefs
// Prefs is like [tailscale.com/ipn.Prefs], but with individual preferences wrapped in
// [prefs.Item], [prefs.List], and [prefs.StructList] to include preference
// state and metadata. Related preferences can be grouped together in a nested
// struct (e.g., [AutoUpdatePrefs] or [AppConnectorPrefs]), whereas each
// individual preference that can be configured by a user or managed via
// syspolicy is wrapped.
//
// Non-preference fields, such as ExitNodePrior and Persist, can be included as-is.
//
// Just like [tailscale.com/ipn.Prefs], [Prefs] is a mutable struct. It should
// only be used in well-defined contexts where mutability is expected and desired,
// such as when the LocalBackend receives a request from the GUI/CLI to change a
// preference, when a preference is managed via syspolicy and needs to be
// configured with an admin-provided value, or when the internal state (e.g.,
// [persist.Persist]) has changed and needs to be preserved.
// In other contexts, a [PrefsView] should be used to provide a read-only view
// of the preferences.
//
// It is recommended to use [jsonv2] for [Prefs] marshaling and unmarshalling to
// improve performance and enable the omission of unconfigured preferences with
// the `omitzero` JSON tag option. This option is not supported by the
// [encoding/json] package as of 2024-08-21; see golang/go#45669.
// It is recommended that a prefs type implements both
// [jsonv2.MarshalerV2]/[jsonv2.UnmarshalerV2] and [json.Marshaler]/[json.Unmarshaler]
// to ensure consistent and more performant marshaling, regardless of the JSON package
// used at the call sites; the standard marshalers can be implemented via [jsonv2].
// See [Prefs.MarshalJSONV2], [Prefs.UnmarshalJSONV2], [Prefs.MarshalJSON],
// and [Prefs.UnmarshalJSON] for an example implementation.
type Prefs struct {
ControlURL prefs.Item[string] `json:",omitzero"`
RouteAll prefs.Item[bool] `json:",omitzero"`
ExitNodeID prefs.Item[tailcfg.StableNodeID] `json:",omitzero"`
ExitNodeIP prefs.Item[netip.Addr] `json:",omitzero"`
// ExitNodePrior is an internal state rather than a preference.
// It can be kept in the Prefs structure but should not be wrapped
// and is ignored by the [prefs] package.
ExitNodePrior tailcfg.StableNodeID
ExitNodeAllowLANAccess prefs.Item[bool] `json:",omitzero"`
CorpDNS prefs.Item[bool] `json:",omitzero"`
RunSSH prefs.Item[bool] `json:",omitzero"`
RunWebClient prefs.Item[bool] `json:",omitzero"`
WantRunning prefs.Item[bool] `json:",omitzero"`
LoggedOut prefs.Item[bool] `json:",omitzero"`
ShieldsUp prefs.Item[bool] `json:",omitzero"`
// AdvertiseTags is a preference whose value is a slice of strings.
// The value is atomic, and individual items in the slice should
// not be modified after the preference is set.
// Since the item type (string) is immutable, we can use [prefs.List].
AdvertiseTags prefs.List[string] `json:",omitzero"`
Hostname prefs.Item[string] `json:",omitzero"`
NotepadURLs prefs.Item[bool] `json:",omitzero"`
ForceDaemon prefs.Item[bool] `json:",omitzero"`
Egg prefs.Item[bool] `json:",omitzero"`
// AdvertiseRoutes is a preference whose value is a slice of netip.Prefix.
// The value is atomic, and individual items in the slice should
// not be modified after the preference is set.
// Since the item type (netip.Prefix) is immutable, we can use [prefs.List].
AdvertiseRoutes prefs.List[netip.Prefix] `json:",omitzero"`
NoSNAT prefs.Item[bool] `json:",omitzero"`
NoStatefulFiltering prefs.Item[opt.Bool] `json:",omitzero"`
NetfilterMode prefs.Item[preftype.NetfilterMode] `json:",omitzero"`
OperatorUser prefs.Item[string] `json:",omitzero"`
ProfileName prefs.Item[string] `json:",omitzero"`
// AutoUpdate contains auto-update preferences.
// Each preference in the group can be configured and managed individually.
AutoUpdate AutoUpdatePrefs `json:",omitzero"`
// AppConnector contains app connector-related preferences.
// Each preference in the group can be configured and managed individually.
AppConnector AppConnectorPrefs `json:",omitzero"`
PostureChecking prefs.Item[bool] `json:",omitzero"`
NetfilterKind prefs.Item[string] `json:",omitzero"`
// DriveShares is a preference whose value is a slice of *[drive.Share].
// The value is atomic, and individual items in the slice should
// not be modified after the preference is set.
// Since the item type (*drive.Share) is mutable and implements [views.ViewCloner],
// we need to use [prefs.StructList] instead of [prefs.List].
DriveShares prefs.StructList[*drive.Share] `json:",omitzero"`
AllowSingleHosts prefs.Item[marshalAsTrueInJSON] `json:",omitzero"`
// Persist is an internal state rather than a preference.
// It can be kept in the Prefs structure but should not be wrapped
// and is ignored by the [prefs] package.
Persist *persist.Persist `json:"Config"`
}
// AutoUpdatePrefs is like [ipn.AutoUpdatePrefs], but it wraps individual preferences with [prefs.Item].
// It groups related preferences together while allowing each to be configured individually.
type AutoUpdatePrefs struct {
Check prefs.Item[bool] `json:",omitzero"`
Apply prefs.Item[opt.Bool] `json:",omitzero"`
}
// AppConnectorPrefs is like [ipn.AppConnectorPrefs], but it wraps individual preferences with [prefs.Item].
// It groups related preferences together while allowing each to be configured individually.
type AppConnectorPrefs struct {
Advertise prefs.Item[bool] `json:",omitzero"`
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
// It is implemented as a performance improvement and to enable omission of
// unconfigured preferences from the JSON output. See the [Prefs] doc for details.
func (p Prefs) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
// The prefs type shadows the Prefs's method set,
// causing [jsonv2] to use the default marshaler and avoiding
// infinite recursion.
type prefs Prefs
return jsonv2.MarshalEncode(out, (*prefs)(&p), opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (p *Prefs) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
// The prefs type shadows the Prefs's method set,
// causing [jsonv2] to use the default unmarshaler and avoiding
// infinite recursion.
type prefs Prefs
return jsonv2.UnmarshalDecode(in, (*prefs)(p), opts)
}
// MarshalJSON implements [json.Marshaler].
func (p Prefs) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(p) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (p *Prefs) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
}
type marshalAsTrueInJSON struct{}
var trueJSON = []byte("true")
func (marshalAsTrueInJSON) MarshalJSON() ([]byte, error) { return trueJSON, nil }
func (*marshalAsTrueInJSON) UnmarshalJSON([]byte) error { return nil }

670
types/prefs/prefs_test.go Normal file
View File

@ -0,0 +1,670 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prefs
import (
"bytes"
"encoding/json"
"errors"
"net/netip"
"reflect"
"testing"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"github.com/google/go-cmp/cmp"
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/viewer --tags=test --type=TestPrefs,TestBundle,TestValueStruct,TestGenericStruct,TestPrefsGroup
type TestPrefs struct {
Int32Item Item[int32] `json:",omitzero"`
UInt64Item Item[uint64] `json:",omitzero"`
StringItem1 Item[string] `json:",omitzero"`
StringItem2 Item[string] `json:",omitzero"`
BoolItem1 Item[bool] `json:",omitzero"`
BoolItem2 Item[bool] `json:",omitzero"`
StringSlice List[string] `json:",omitzero"`
IntSlice List[int] `json:",omitzero"`
AddrItem Item[netip.Addr] `json:",omitzero"`
StringStringMap Map[string, string] `json:",omitzero"`
IntStringMap Map[int, string] `json:",omitzero"`
AddrIntMap Map[netip.Addr, int] `json:",omitzero"`
// Bundles are complex preferences that usually consist of
// multiple parameters that must be configured atomically.
Bundle1 Item[*TestBundle] `json:",omitzero"`
Bundle2 Item[*TestBundle] `json:",omitzero"`
Generic Item[*TestGenericStruct[int]] `json:",omitzero"`
BundleList StructList[*TestBundle] `json:",omitzero"`
StringBundleMap StructMap[string, *TestBundle] `json:",omitzero"`
IntBundleMap StructMap[int, *TestBundle] `json:",omitzero"`
AddrBundleMap StructMap[netip.Addr, *TestBundle] `json:",omitzero"`
// Group is a nested struct that contains one or more preferences.
// Each preference in a group can be configured individually.
// Preference groups should be included directly rather than by pointers.
Group TestPrefsGroup `json:",omitzero"`
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (p TestPrefs) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
// The testPrefs type shadows the TestPrefs's method set,
// causing jsonv2 to use the default marshaler and avoiding
// infinite recursion.
type testPrefs TestPrefs
return jsonv2.MarshalEncode(out, (*testPrefs)(&p), opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (p *TestPrefs) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
// The testPrefs type shadows the TestPrefs's method set,
// causing jsonv2 to use the default unmarshaler and avoiding
// infinite recursion.
type testPrefs TestPrefs
return jsonv2.UnmarshalDecode(in, (*testPrefs)(p), opts)
}
// MarshalJSON implements [json.Marshaler].
func (p TestPrefs) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(p) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (p *TestPrefs) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, p) // uses UnmarshalJSONV2
}
// TestBundle is an example structure type that,
// despite containing multiple values, represents
// a single configurable preference item.
type TestBundle struct {
Name string `json:",omitzero"`
Nested *TestValueStruct `json:",omitzero"`
}
func (b *TestBundle) Equal(b2 *TestBundle) bool {
if b == b2 {
return true
}
if b == nil || b2 == nil {
return false
}
return b.Name == b2.Name && b.Nested.Equal(b2.Nested)
}
// TestPrefsGroup contains logically grouped preference items.
// Each preference item in a group can be configured individually.
type TestPrefsGroup struct {
FloatItem Item[float64] `json:",omitzero"`
TestStringItem Item[TestStringType] `json:",omitzero"`
}
type TestValueStruct struct {
Value int
}
func (s *TestValueStruct) Equal(s2 *TestValueStruct) bool {
if s == s2 {
return true
}
if s == nil || s2 == nil {
return false
}
return *s == *s2
}
type TestGenericStruct[T ImmutableType] struct {
Value T
}
func (s *TestGenericStruct[T]) Equal(s2 *TestGenericStruct[T]) bool {
if s == s2 {
return true
}
if s == nil || s2 == nil {
return false
}
return *s == *s2
}
type TestStringType string
func TestMarshalUnmarshal(t *testing.T) {
tests := []struct {
name string
prefs *TestPrefs
indent bool
want string
}{
{
name: "string",
prefs: &TestPrefs{StringItem1: ItemOf("Value1")},
want: `{"StringItem1": {"Value": "Value1"}}`,
},
{
name: "empty-string",
prefs: &TestPrefs{StringItem1: ItemOf("")},
want: `{"StringItem1": {"Value": ""}}`,
},
{
name: "managed-string",
prefs: &TestPrefs{StringItem1: ItemOf("Value1", Managed)},
want: `{"StringItem1": {"Value": "Value1", "Managed": true}}`,
},
{
name: "readonly-item",
prefs: &TestPrefs{StringItem1: ItemWithOpts[string](ReadOnly)},
want: `{"StringItem1": {"ReadOnly": true}}`,
},
{
name: "readonly-item-with-value",
prefs: &TestPrefs{StringItem1: ItemOf("RO", ReadOnly)},
want: `{"StringItem1": {"Value": "RO", "ReadOnly": true}}`,
},
{
name: "int32",
prefs: &TestPrefs{Int32Item: ItemOf[int32](101)},
want: `{"Int32Item": {"Value": 101}}`,
},
{
name: "uint64",
prefs: &TestPrefs{UInt64Item: ItemOf[uint64](42)},
want: `{"UInt64Item": {"Value": 42}}`,
},
{
name: "bool-true",
prefs: &TestPrefs{BoolItem1: ItemOf(true)},
want: `{"BoolItem1": {"Value": true}}`,
},
{
name: "bool-false",
prefs: &TestPrefs{BoolItem1: ItemOf(false)},
want: `{"BoolItem1": {"Value": false}}`,
},
{
name: "empty-slice",
prefs: &TestPrefs{StringSlice: ListOf([]string{})},
want: `{"StringSlice": {"Value": []}}`,
},
{
name: "string-slice",
prefs: &TestPrefs{StringSlice: ListOf([]string{"1", "2", "3"})},
want: `{"StringSlice": {"Value": ["1", "2", "3"]}}`,
},
{
name: "int-slice",
prefs: &TestPrefs{IntSlice: ListOf([]int{4, 8, 15, 16, 23})},
want: `{"IntSlice": {"Value": [4, 8, 15, 16, 23]}}`,
},
{
name: "managed-int-slice",
prefs: &TestPrefs{IntSlice: ListOf([]int{4, 8, 15, 16, 23}, Managed)},
want: `{"IntSlice": {"Value": [4, 8, 15, 16, 23], "Managed": true}}`,
},
{
name: "netip-addr",
prefs: &TestPrefs{AddrItem: ItemOf(netip.MustParseAddr("127.0.0.1"))},
want: `{"AddrItem": {"Value": "127.0.0.1"}}`,
},
{
name: "string-string-map",
prefs: &TestPrefs{StringStringMap: MapOf(map[string]string{"K1": "V1"})},
want: `{"StringStringMap": {"Value": {"K1": "V1"}}}`,
},
{
name: "int-string-map",
prefs: &TestPrefs{IntStringMap: MapOf(map[int]string{42: "V1"})},
want: `{"IntStringMap": {"Value": {"42": "V1"}}}`,
},
{
name: "addr-int-map",
prefs: &TestPrefs{AddrIntMap: MapOf(map[netip.Addr]int{netip.MustParseAddr("127.0.0.1"): 42})},
want: `{"AddrIntMap": {"Value": {"127.0.0.1": 42}}}`,
},
{
name: "bundle-list",
prefs: &TestPrefs{BundleList: StructListOf([]*TestBundle{{Name: "Bundle1"}, {Name: "Bundle2"}})},
want: `{"BundleList": {"Value": [{"Name": "Bundle1"},{"Name": "Bundle2"}]}}`,
},
{
name: "string-bundle-map",
prefs: &TestPrefs{StringBundleMap: StructMapOf(map[string]*TestBundle{
"K1": {Name: "Bundle1"},
"K2": {Name: "Bundle2"},
})},
want: `{"StringBundleMap": {"Value": {"K1": {"Name": "Bundle1"}, "K2": {"Name": "Bundle2"}}}}`,
},
{
name: "int-bundle-map",
prefs: &TestPrefs{IntBundleMap: StructMapOf(map[int]*TestBundle{42: {Name: "Bundle1"}})},
want: `{"IntBundleMap": {"Value": {"42": {"Name": "Bundle1"}}}}`,
},
{
name: "addr-bundle-map",
prefs: &TestPrefs{AddrBundleMap: StructMapOf(map[netip.Addr]*TestBundle{netip.MustParseAddr("127.0.0.1"): {Name: "Bundle1"}})},
want: `{"AddrBundleMap": {"Value": {"127.0.0.1": {"Name": "Bundle1"}}}}`,
},
{
name: "bundle",
prefs: &TestPrefs{Bundle1: ItemOf(&TestBundle{Name: "Bundle1"})},
want: `{"Bundle1": {"Value": {"Name": "Bundle1"}}}`,
},
{
name: "managed-bundle",
prefs: &TestPrefs{Bundle2: ItemOf(&TestBundle{Name: "Bundle2", Nested: &TestValueStruct{Value: 17}}, Managed)},
want: `{"Bundle2": {"Value": {"Name": "Bundle2", "Nested": {"Value": 17}}, "Managed": true}}`,
},
{
name: "subgroup",
prefs: &TestPrefs{Group: TestPrefsGroup{FloatItem: ItemOf(1.618), TestStringItem: ItemOf(TestStringType("Value"))}},
want: `{"Group": {"FloatItem": {"Value": 1.618}, "TestStringItem": {"Value": "Value"}}}`,
},
{
name: "various",
prefs: &TestPrefs{
Int32Item: ItemOf[int32](101),
UInt64Item: ItemOf[uint64](42),
StringItem1: ItemOf("Value1"),
StringItem2: ItemWithOpts[string](ReadOnly),
BoolItem1: ItemOf(true),
BoolItem2: ItemOf(false, Managed),
StringSlice: ListOf([]string{"1", "2", "3"}),
IntSlice: ListOf([]int{4, 8, 15, 16, 23}, Managed),
AddrItem: ItemOf(netip.MustParseAddr("127.0.0.1")),
StringStringMap: MapOf(map[string]string{"K1": "V1"}),
IntStringMap: MapOf(map[int]string{42: "V1"}),
AddrIntMap: MapOf(map[netip.Addr]int{netip.MustParseAddr("127.0.0.1"): 42}),
BundleList: StructListOf([]*TestBundle{{Name: "Bundle1"}}),
StringBundleMap: StructMapOf(map[string]*TestBundle{"K1": {Name: "Bundle1"}}),
IntBundleMap: StructMapOf(map[int]*TestBundle{42: {Name: "Bundle1"}}),
AddrBundleMap: StructMapOf(map[netip.Addr]*TestBundle{netip.MustParseAddr("127.0.0.1"): {Name: "Bundle1"}}),
Bundle1: ItemOf(&TestBundle{Name: "Bundle1"}),
Bundle2: ItemOf(&TestBundle{Name: "Bundle2", Nested: &TestValueStruct{Value: 17}}, Managed),
Group: TestPrefsGroup{
FloatItem: ItemOf(1.618),
TestStringItem: ItemOf(TestStringType("Value")),
},
},
want: `{
"Int32Item": {"Value": 101},
"UInt64Item": {"Value": 42},
"StringItem1": {"Value": "Value1"},
"StringItem2": {"ReadOnly": true},
"BoolItem1": {"Value": true},
"BoolItem2": {"Value": false, "Managed": true},
"StringSlice": {"Value": ["1", "2", "3"]},
"IntSlice": {"Value": [4, 8, 15, 16, 23], "Managed": true},
"AddrItem": {"Value": "127.0.0.1"},
"StringStringMap": {"Value": {"K1": "V1"}},
"IntStringMap": {"Value": {"42": "V1"}},
"AddrIntMap": {"Value": {"127.0.0.1": 42}},
"BundleList": {"Value": [{"Name": "Bundle1"}]},
"StringBundleMap": {"Value": {"K1": {"Name": "Bundle1"}}},
"IntBundleMap": {"Value": {"42": {"Name": "Bundle1"}}},
"AddrBundleMap": {"Value": {"127.0.0.1": {"Name": "Bundle1"}}},
"Bundle1": {"Value": {"Name": "Bundle1"}},
"Bundle2": {"Value": {"Name": "Bundle2", "Nested": {"Value": 17}}, "Managed": true},
"Group": {
"FloatItem": {"Value": 1.618},
"TestStringItem": {"Value": "Value"}
}
}`,
},
}
arshalers := []struct {
name string
marshal func(in any) (out []byte, err error)
unmarshal func(in []byte, out any) (err error)
}{
{
name: "json",
marshal: json.Marshal,
unmarshal: json.Unmarshal,
},
{
name: "jsonv2",
marshal: func(in any) (out []byte, err error) { return jsonv2.Marshal(in) },
unmarshal: func(in []byte, out any) (err error) { return jsonv2.Unmarshal(in, out) },
},
}
for _, a := range arshalers {
t.Run(a.name, func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Run("marshal-directly", func(t *testing.T) {
gotJSON, err := a.marshal(tt.prefs)
if err != nil {
t.Fatalf("marshalling failed: %v", err)
}
checkJSON(t, gotJSON, jsontext.Value(tt.want))
var gotPrefs TestPrefs
if err = a.unmarshal(gotJSON, &gotPrefs); err != nil {
t.Fatalf("unmarshalling failed: %v", err)
}
if diff := cmp.Diff(tt.prefs, &gotPrefs); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
t.Run("marshal-via-view", func(t *testing.T) {
gotJSON, err := a.marshal(tt.prefs.View())
if err != nil {
t.Fatalf("marshalling failed: %v", err)
}
checkJSON(t, gotJSON, jsontext.Value(tt.want))
var gotPrefs TestPrefsView
if err = a.unmarshal(gotJSON, &gotPrefs); err != nil {
t.Fatalf("unmarshalling failed: %v", err)
}
if diff := cmp.Diff(tt.prefs, gotPrefs.AsStruct()); diff != "" {
t.Errorf("mismatch (-want +got):\n%s", diff)
}
})
})
}
})
}
}
func TestPreferenceStates(t *testing.T) {
const (
zeroValue = 0
defValue = 5
userValue = 42
mdmValue = 1001
)
i := ItemWithOpts[int]()
checkIsSet(t, &i, false)
checkIsManaged(t, &i, false)
checkIsReadOnly(t, &i, false)
checkValueOk(t, &i, zeroValue, false)
i.SetDefaultValue(defValue)
checkValue(t, &i, defValue)
checkValueOk(t, &i, defValue, false)
checkSetValue(t, &i, userValue)
checkValue(t, &i, userValue)
checkValueOk(t, &i, userValue, true)
i2 := ItemOf(userValue)
checkIsSet(t, &i2, true)
checkValue(t, &i2, userValue)
checkValueOk(t, &i2, userValue, true)
checkEqual(t, i2, i, true)
i2.SetManagedValue(mdmValue)
// Setting a managed value should set the value, mark the preference
// as managed and read-only, and prevent it from being modified with SetValue.
checkIsSet(t, &i2, true)
checkIsManaged(t, &i2, true)
checkIsReadOnly(t, &i2, true)
checkValue(t, &i2, mdmValue)
checkValueOk(t, &i2, mdmValue, true)
checkCanNotSetValue(t, &i2, userValue, ErrManaged)
checkValue(t, &i2, mdmValue) // the value must not be changed
checkCanNotClearValue(t, &i2, ErrManaged)
i2.ClearManaged()
// Clearing the managed flag should change the IsManaged and IsReadOnly flags...
checkIsManaged(t, &i2, false)
checkIsReadOnly(t, &i2, false)
// ...but not the value.
checkValue(t, &i2, mdmValue)
// We should be able to change the value after clearing the managed flag.
checkSetValue(t, &i2, userValue)
checkIsSet(t, &i2, true)
checkValue(t, &i2, userValue)
checkValueOk(t, &i2, userValue, true)
checkEqual(t, i2, i, true)
i2.SetReadOnly(true)
checkIsReadOnly(t, &i2, true)
checkIsManaged(t, &i2, false)
checkCanNotSetValue(t, &i2, userValue, ErrReadOnly)
checkCanNotClearValue(t, &i2, ErrReadOnly)
i2.SetReadOnly(false)
i2.SetDefaultValue(defValue)
checkClearValue(t, &i2)
checkIsSet(t, &i2, false)
checkValue(t, &i2, defValue)
checkValueOk(t, &i2, defValue, false)
}
func TestItemView(t *testing.T) {
i := ItemOf(&TestBundle{Name: "B1"})
iv := ItemViewOf(&i)
checkIsSet(t, iv, true)
checkIsManaged(t, iv, false)
checkIsReadOnly(t, iv, false)
checkValue(t, iv, TestBundleView{i.Value()})
checkValueOk(t, iv, TestBundleView{i.Value()}, true)
i2 := *iv.AsStruct()
checkEqual(t, i, i2, true)
i2.SetValue(&TestBundle{Name: "B2"})
iv2 := ItemViewOf(&i2)
checkEqual(t, iv, iv2, false)
}
func TestListView(t *testing.T) {
l := ListOf([]int{4, 8, 15, 16, 23, 42}, ReadOnly)
lv := l.View()
checkIsSet(t, lv, true)
checkIsManaged(t, lv, false)
checkIsReadOnly(t, lv, true)
checkValue(t, lv, views.SliceOf(l.Value()))
checkValueOk(t, lv, views.SliceOf(l.Value()), true)
l2 := *lv.AsStruct()
checkEqual(t, l, l2, true)
}
func TestStructListView(t *testing.T) {
l := StructListOf([]*TestBundle{{Name: "E1"}, {Name: "E2"}}, ReadOnly)
lv := StructListViewOf(&l)
checkIsSet(t, lv, true)
checkIsManaged(t, lv, false)
checkIsReadOnly(t, lv, true)
checkValue(t, lv, views.SliceOfViews(l.Value()))
checkValueOk(t, lv, views.SliceOfViews(l.Value()), true)
l2 := *lv.AsStruct()
checkEqual(t, l, l2, true)
}
func TestStructMapView(t *testing.T) {
m := StructMapOf(map[string]*TestBundle{
"K1": {Name: "E1"},
"K2": {Name: "E2"},
}, ReadOnly)
mv := StructMapViewOf(&m)
checkIsSet(t, mv, true)
checkIsManaged(t, mv, false)
checkIsReadOnly(t, mv, true)
checkValue(t, *mv.AsStruct(), m.Value())
checkValueOk(t, *mv.AsStruct(), m.Value(), true)
m2 := *mv.AsStruct()
checkEqual(t, m, m2, true)
}
// check that the preference types implement the test [pref] interface.
var (
_ pref[int] = (*Item[int])(nil)
_ pref[*TestBundle] = (*Item[*TestBundle])(nil)
_ pref[[]int] = (*List[int])(nil)
_ pref[[]*TestBundle] = (*StructList[*TestBundle])(nil)
_ pref[map[string]*TestBundle] = (*StructMap[string, *TestBundle])(nil)
)
// pref is an interface used by [checkSetValue], [checkClearValue], and similar test
// functions that mutate preferences. It is implemented by all preference types, such
// as [Item], [List], [StructList], and [StructMap], and provides both read and write
// access to the preference's value and state.
type pref[T any] interface {
prefView[T]
SetValue(v T) error
ClearValue() error
SetDefaultValue(v T)
SetManagedValue(v T)
ClearManaged()
SetReadOnly(readonly bool)
}
// check that the preference view types implement the test [prefView] interface.
var (
_ prefView[int] = (*Item[int])(nil)
_ prefView[TestBundleView] = (*ItemView[*TestBundle, TestBundleView])(nil)
_ prefView[views.Slice[int]] = (*ListView[int])(nil)
_ prefView[views.SliceView[*TestBundle, TestBundleView]] = (*StructListView[*TestBundle, TestBundleView])(nil)
_ prefView[views.MapFn[string, *TestBundle, TestBundleView]] = (*StructMapView[string, *TestBundle, TestBundleView])(nil)
)
// prefView is an interface used by [checkIsSet], [checkIsManaged], and similar non-mutating
// test functions. It is implemented by all preference types, such as [Item], [List], [StructList],
// and [StructMap], as well as their corresponding views, such as [ItemView], [ListView], [StructListView],
// and [StructMapView], and provides read-only access to the preference's value and state.
type prefView[T any] interface {
IsSet() bool
Value() T
ValueOk() (T, bool)
DefaultValue() T
IsManaged() bool
IsReadOnly() bool
}
func checkIsSet[T any](tb testing.TB, p prefView[T], wantSet bool) {
tb.Helper()
if gotSet := p.IsSet(); gotSet != wantSet {
tb.Errorf("IsSet: got %v; want %v", gotSet, wantSet)
}
}
func checkIsManaged[T any](tb testing.TB, p prefView[T], wantManaged bool) {
tb.Helper()
if gotManaged := p.IsManaged(); gotManaged != wantManaged {
tb.Errorf("IsManaged: got %v; want %v", gotManaged, wantManaged)
}
}
func checkIsReadOnly[T any](tb testing.TB, p prefView[T], wantReadOnly bool) {
tb.Helper()
if gotReadOnly := p.IsReadOnly(); gotReadOnly != wantReadOnly {
tb.Errorf("IsReadOnly: got %v; want %v", gotReadOnly, wantReadOnly)
}
}
func checkValue[T any](tb testing.TB, p prefView[T], wantValue T) {
tb.Helper()
if gotValue := p.Value(); !testComparerFor[T]()(gotValue, wantValue) {
tb.Errorf("Value: got %v; want %v", gotValue, wantValue)
}
}
func checkValueOk[T any](tb testing.TB, p prefView[T], wantValue T, wantOk bool) {
tb.Helper()
gotValue, gotOk := p.ValueOk()
if gotOk != wantOk || !testComparerFor[T]()(gotValue, wantValue) {
tb.Errorf("ValueOk: got (%v, %v); want (%v, %v)", gotValue, gotOk, wantValue, wantOk)
}
}
func checkEqual[T equatable[T]](tb testing.TB, a, b T, wantEqual bool) {
tb.Helper()
if gotEqual := a.Equal(b); gotEqual != wantEqual {
tb.Errorf("Equal: got %v; want %v", gotEqual, wantEqual)
}
}
func checkSetValue[T any](tb testing.TB, p pref[T], v T) {
tb.Helper()
if err := p.SetValue(v); err != nil {
tb.Fatalf("SetValue: gotErr %v, wantErr: nil", err)
}
}
func checkCanNotSetValue[T any](tb testing.TB, p pref[T], v T, wantErr error) {
tb.Helper()
if err := p.SetValue(v); err == nil || !errors.Is(err, wantErr) {
tb.Fatalf("SetValue: gotErr %v, wantErr: %v", err, wantErr)
}
}
func checkClearValue[T any](tb testing.TB, p pref[T]) {
tb.Helper()
if err := p.ClearValue(); err != nil {
tb.Fatalf("ClearValue: gotErr %v, wantErr: nil", err)
}
}
func checkCanNotClearValue[T any](tb testing.TB, p pref[T], wantErr error) {
tb.Helper()
err := p.ClearValue()
if err == nil || !errors.Is(err, wantErr) {
tb.Fatalf("ClearValue: gotErr %v, wantErr: %v", err, wantErr)
}
}
// testComparerFor is like [comparerFor], but uses [reflect.DeepEqual]
// unless T is [equatable].
func testComparerFor[T any]() func(a, b T) bool {
return func(a, b T) bool {
switch a := any(a).(type) {
case equatable[T]:
return a.Equal(b)
default:
return reflect.DeepEqual(a, b)
}
}
}
func checkJSON(tb testing.TB, got, want jsontext.Value) {
tb.Helper()
got = got.Clone()
want = want.Clone()
// Compare canonical forms.
if err := got.Canonicalize(); err != nil {
tb.Error(err)
}
if err := want.Canonicalize(); err != nil {
tb.Error(err)
}
if bytes.Equal(got, want) {
return
}
gotMap := make(map[string]any)
if err := jsonv2.Unmarshal(got, &gotMap); err != nil {
tb.Fatal(err)
}
wantMap := make(map[string]any)
if err := jsonv2.Unmarshal(want, &wantMap); err != nil {
tb.Fatal(err)
}
tb.Errorf("mismatch (-want +got):\n%s", cmp.Diff(wantMap, gotMap))
}

View File

@ -0,0 +1,342 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
package prefs
import (
"encoding/json"
"errors"
"net/netip"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=TestPrefs,TestBundle,TestValueStruct,TestGenericStruct,TestPrefsGroup -tags=test
// View returns a readonly view of TestPrefs.
func (p *TestPrefs) View() TestPrefsView {
return TestPrefsView{ж: p}
}
// TestPrefsView provides a read-only view over TestPrefs.
//
// Its methods should only be called if `Valid()` returns true.
type TestPrefsView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *TestPrefs
}
// Valid reports whether underlying value is non-nil.
func (v TestPrefsView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v TestPrefsView) AsStruct() *TestPrefs {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v TestPrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *TestPrefsView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x TestPrefs
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v TestPrefsView) Int32Item() Item[int32] { return v.ж.Int32Item }
func (v TestPrefsView) UInt64Item() Item[uint64] { return v.ж.UInt64Item }
func (v TestPrefsView) StringItem1() Item[string] { return v.ж.StringItem1 }
func (v TestPrefsView) StringItem2() Item[string] { return v.ж.StringItem2 }
func (v TestPrefsView) BoolItem1() Item[bool] { return v.ж.BoolItem1 }
func (v TestPrefsView) BoolItem2() Item[bool] { return v.ж.BoolItem2 }
func (v TestPrefsView) StringSlice() ListView[string] { return v.ж.StringSlice.View() }
func (v TestPrefsView) IntSlice() ListView[int] { return v.ж.IntSlice.View() }
func (v TestPrefsView) AddrItem() Item[netip.Addr] { return v.ж.AddrItem }
func (v TestPrefsView) StringStringMap() MapView[string, string] { return v.ж.StringStringMap.View() }
func (v TestPrefsView) IntStringMap() MapView[int, string] { return v.ж.IntStringMap.View() }
func (v TestPrefsView) AddrIntMap() MapView[netip.Addr, int] { return v.ж.AddrIntMap.View() }
func (v TestPrefsView) Bundle1() ItemView[*TestBundle, TestBundleView] {
return ItemViewOf(&v.ж.Bundle1)
}
func (v TestPrefsView) Bundle2() ItemView[*TestBundle, TestBundleView] {
return ItemViewOf(&v.ж.Bundle2)
}
func (v TestPrefsView) Generic() ItemView[*TestGenericStruct[int], TestGenericStructView[int]] {
return ItemViewOf(&v.ж.Generic)
}
func (v TestPrefsView) BundleList() StructListView[*TestBundle, TestBundleView] {
return StructListViewOf(&v.ж.BundleList)
}
func (v TestPrefsView) StringBundleMap() StructMapView[string, *TestBundle, TestBundleView] {
return StructMapViewOf(&v.ж.StringBundleMap)
}
func (v TestPrefsView) IntBundleMap() StructMapView[int, *TestBundle, TestBundleView] {
return StructMapViewOf(&v.ж.IntBundleMap)
}
func (v TestPrefsView) AddrBundleMap() StructMapView[netip.Addr, *TestBundle, TestBundleView] {
return StructMapViewOf(&v.ж.AddrBundleMap)
}
func (v TestPrefsView) Group() TestPrefsGroup { return v.ж.Group }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TestPrefsViewNeedsRegeneration = TestPrefs(struct {
Int32Item Item[int32]
UInt64Item Item[uint64]
StringItem1 Item[string]
StringItem2 Item[string]
BoolItem1 Item[bool]
BoolItem2 Item[bool]
StringSlice List[string]
IntSlice List[int]
AddrItem Item[netip.Addr]
StringStringMap Map[string, string]
IntStringMap Map[int, string]
AddrIntMap Map[netip.Addr, int]
Bundle1 Item[*TestBundle]
Bundle2 Item[*TestBundle]
Generic Item[*TestGenericStruct[int]]
BundleList StructList[*TestBundle]
StringBundleMap StructMap[string, *TestBundle]
IntBundleMap StructMap[int, *TestBundle]
AddrBundleMap StructMap[netip.Addr, *TestBundle]
Group TestPrefsGroup
}{})
// View returns a readonly view of TestBundle.
func (p *TestBundle) View() TestBundleView {
return TestBundleView{ж: p}
}
// TestBundleView provides a read-only view over TestBundle.
//
// Its methods should only be called if `Valid()` returns true.
type TestBundleView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *TestBundle
}
// Valid reports whether underlying value is non-nil.
func (v TestBundleView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v TestBundleView) AsStruct() *TestBundle {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v TestBundleView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *TestBundleView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x TestBundle
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v TestBundleView) Name() string { return v.ж.Name }
func (v TestBundleView) Nested() *TestValueStruct {
if v.ж.Nested == nil {
return nil
}
x := *v.ж.Nested
return &x
}
func (v TestBundleView) Equal(v2 TestBundleView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TestBundleViewNeedsRegeneration = TestBundle(struct {
Name string
Nested *TestValueStruct
}{})
// View returns a readonly view of TestValueStruct.
func (p *TestValueStruct) View() TestValueStructView {
return TestValueStructView{ж: p}
}
// TestValueStructView provides a read-only view over TestValueStruct.
//
// Its methods should only be called if `Valid()` returns true.
type TestValueStructView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *TestValueStruct
}
// Valid reports whether underlying value is non-nil.
func (v TestValueStructView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v TestValueStructView) AsStruct() *TestValueStruct {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v TestValueStructView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *TestValueStructView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x TestValueStruct
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v TestValueStructView) Value() int { return v.ж.Value }
func (v TestValueStructView) Equal(v2 TestValueStructView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TestValueStructViewNeedsRegeneration = TestValueStruct(struct {
Value int
}{})
// View returns a readonly view of TestGenericStruct.
func (p *TestGenericStruct[T]) View() TestGenericStructView[T] {
return TestGenericStructView[T]{ж: p}
}
// TestGenericStructView[T] provides a read-only view over TestGenericStruct[T].
//
// Its methods should only be called if `Valid()` returns true.
type TestGenericStructView[T ImmutableType] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *TestGenericStruct[T]
}
// Valid reports whether underlying value is non-nil.
func (v TestGenericStructView[T]) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v TestGenericStructView[T]) AsStruct() *TestGenericStruct[T] {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v TestGenericStructView[T]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *TestGenericStructView[T]) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x TestGenericStruct[T]
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v TestGenericStructView[T]) Value() T { return v.ж.Value }
func (v TestGenericStructView[T]) Equal(v2 TestGenericStructView[T]) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
func _TestGenericStructViewNeedsRegeneration[T ImmutableType](TestGenericStruct[T]) {
_TestGenericStructViewNeedsRegeneration(struct {
Value T
}{})
}
// View returns a readonly view of TestPrefsGroup.
func (p *TestPrefsGroup) View() TestPrefsGroupView {
return TestPrefsGroupView{ж: p}
}
// TestPrefsGroupView provides a read-only view over TestPrefsGroup.
//
// Its methods should only be called if `Valid()` returns true.
type TestPrefsGroupView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *TestPrefsGroup
}
// Valid reports whether underlying value is non-nil.
func (v TestPrefsGroupView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v TestPrefsGroupView) AsStruct() *TestPrefsGroup {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v TestPrefsGroupView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *TestPrefsGroupView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x TestPrefsGroup
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v TestPrefsGroupView) FloatItem() Item[float64] { return v.ж.FloatItem }
func (v TestPrefsGroupView) TestStringItem() Item[TestStringType] { return v.ж.TestStringItem }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TestPrefsGroupViewNeedsRegeneration = TestPrefsGroup(struct {
FloatItem Item[float64]
TestStringItem Item[TestStringType]
}{})

195
types/prefs/struct_list.go Normal file
View File

@ -0,0 +1,195 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prefs
import (
"fmt"
"reflect"
"slices"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
)
// StructList is a preference type that holds zero or more potentially mutable struct values.
type StructList[T views.Cloner[T]] struct {
preference[[]T]
}
// StructListOf returns a [StructList] configured with the specified value and [Options].
func StructListOf[T views.Cloner[T]](v []T, opts ...Options) StructList[T] {
return StructList[T]{preferenceOf(opt.ValueOf(deepCloneSlice(v)), opts...)}
}
// StructListWithOpts returns an unconfigured [StructList] with the specified [Options].
func StructListWithOpts[T views.Cloner[T]](opts ...Options) StructList[T] {
return StructList[T]{preferenceOf(opt.Value[[]T]{}, opts...)}
}
// SetValue configures the preference with the specified value.
// It fails and returns [ErrManaged] if p is a managed preference,
// and [ErrReadOnly] if p is a read-only preference.
func (l *StructList[T]) SetValue(val []T) error {
return l.preference.SetValue(deepCloneSlice(val))
}
// SetManagedValue configures the preference with the specified value
// and marks the preference as managed.
func (l *StructList[T]) SetManagedValue(val []T) {
l.preference.SetManagedValue(deepCloneSlice(val))
}
// Clone returns a copy of l that aliases no memory with l.
func (l StructList[T]) Clone() *StructList[T] {
res := ptr.To(l)
if v, ok := l.s.Value.GetOk(); ok {
res.s.Value.Set(deepCloneSlice(v))
}
return res
}
// Equal reports whether l and l2 are equal.
// If the template type T implements an Equal(T) bool method, it will be used
// instead of the == operator for value comparison.
// It panics if T is not comparable.
func (l StructList[T]) Equal(l2 StructList[T]) bool {
if l.s.Metadata != l2.s.Metadata {
return false
}
v1, ok1 := l.s.Value.GetOk()
v2, ok2 := l2.s.Value.GetOk()
if ok1 != ok2 {
return false
}
if ok1 != ok2 {
return false
}
return !ok1 || slices.EqualFunc(v1, v2, comparerFor[T]())
}
func deepCloneSlice[T views.Cloner[T]](s []T) []T {
c := make([]T, len(s))
for i := range s {
c[i] = s[i].Clone()
}
return c
}
type equatable[T any] interface {
Equal(other T) bool
}
func comparerFor[T any]() func(a, b T) bool {
switch t := reflect.TypeFor[T](); {
case t.Implements(reflect.TypeFor[equatable[T]]()):
return func(a, b T) bool { return any(a).(equatable[T]).Equal(b) }
case t.Comparable():
return func(a, b T) bool { return any(a) == any(b) }
default:
panic(fmt.Errorf("%v is not comparable", t))
}
}
// StructListView is a read-only view of a [StructList].
type StructListView[T views.ViewCloner[T, V], V views.StructView[T]] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *StructList[T]
}
// StructListViewOf returns a read-only view of l.
// It is used by [tailscale.com/cmd/viewer].
func StructListViewOf[T views.ViewCloner[T, V], V views.StructView[T]](l *StructList[T]) StructListView[T, V] {
return StructListView[T, V]{l}
}
// Valid reports whether the underlying [StructList] is non-nil.
func (lv StructListView[T, V]) Valid() bool {
return lv.ж != nil
}
// AsStruct implements [views.StructView] by returning a clone of the preference
// which aliases no memory with the original.
func (lv StructListView[T, V]) AsStruct() *StructList[T] {
if lv.ж == nil {
return nil
}
return lv.ж.Clone()
}
// IsSet reports whether the preference has a value set.
func (lv StructListView[T, V]) IsSet() bool {
return lv.ж.IsSet()
}
// Value returns a read-only view of the value if the preference has a value set.
// Otherwise, it returns a read-only view of its default value.
func (lv StructListView[T, V]) Value() views.SliceView[T, V] {
return views.SliceOfViews(lv.ж.Value())
}
// ValueOk returns a read-only view of the value and true if the preference has a value set.
// Otherwise, it returns an invalid view and false.
func (lv StructListView[T, V]) ValueOk() (val views.SliceView[T, V], ok bool) {
if v, ok := lv.ж.ValueOk(); ok {
return views.SliceOfViews(v), true
}
return views.SliceView[T, V]{}, false
}
// DefaultValue returns a read-only view of the default value of the preference.
func (lv StructListView[T, V]) DefaultValue() views.SliceView[T, V] {
return views.SliceOfViews(lv.ж.DefaultValue())
}
// IsManaged reports whether the preference is managed via MDM, Group Policy, or similar means.
func (lv StructListView[T, V]) IsManaged() bool {
return lv.ж.IsManaged()
}
// IsReadOnly reports whether the preference is read-only and cannot be changed by user.
func (lv StructListView[T, V]) IsReadOnly() bool {
return lv.ж.IsReadOnly()
}
// Equal reports whether iv and iv2 are equal.
func (lv StructListView[T, V]) Equal(lv2 StructListView[T, V]) bool {
if !lv.Valid() && !lv2.Valid() {
return true
}
if lv.Valid() != lv2.Valid() {
return false
}
return lv.ж.Equal(*lv2.ж)
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (lv StructListView[T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return lv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (lv *StructListView[T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x StructList[T]
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
lv.ж = &x
return nil
}
// MarshalJSON implements [json.Marshaler].
func (lv StructListView[T, V]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(lv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (lv *StructListView[T, V]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, lv) // uses UnmarshalJSONV2
}

175
types/prefs/struct_map.go Normal file
View File

@ -0,0 +1,175 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package prefs
import (
"maps"
jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
)
// StructMap is a preference type that holds potentially mutable key-value pairs.
type StructMap[K MapKeyType, V views.Cloner[V]] struct {
preference[map[K]V]
}
// StructMapOf returns a [StructMap] configured with the specified value and [Options].
func StructMapOf[K MapKeyType, V views.Cloner[V]](v map[K]V, opts ...Options) StructMap[K, V] {
return StructMap[K, V]{preferenceOf(opt.ValueOf(deepCloneMap(v)), opts...)}
}
// StructMapWithOpts returns an unconfigured [StructMap] with the specified [Options].
func StructMapWithOpts[K MapKeyType, V views.Cloner[V]](opts ...Options) StructMap[K, V] {
return StructMap[K, V]{preferenceOf(opt.Value[map[K]V]{}, opts...)}
}
// SetValue configures the preference with the specified value.
// It fails and returns [ErrManaged] if p is a managed preference,
// and [ErrReadOnly] if p is a read-only preference.
func (l *StructMap[K, V]) SetValue(val map[K]V) error {
return l.preference.SetValue(deepCloneMap(val))
}
// SetManagedValue configures the preference with the specified value
// and marks the preference as managed.
func (l *StructMap[K, V]) SetManagedValue(val map[K]V) {
l.preference.SetManagedValue(deepCloneMap(val))
}
// Clone returns a copy of m that aliases no memory with m.
func (m StructMap[K, V]) Clone() *StructMap[K, V] {
res := ptr.To(m)
if v, ok := m.s.Value.GetOk(); ok {
res.s.Value.Set(deepCloneMap(v))
}
return res
}
// Equal reports whether m and m2 are equal.
// If the template type V implements an Equal(V) bool method, it will be used
// instead of the == operator for value comparison.
// It panics if T is not comparable.
func (m StructMap[K, V]) Equal(m2 StructMap[K, V]) bool {
if m.s.Metadata != m2.s.Metadata {
return false
}
v1, ok1 := m.s.Value.GetOk()
v2, ok2 := m2.s.Value.GetOk()
if ok1 != ok2 {
return false
}
return !ok1 || maps.EqualFunc(v1, v2, comparerFor[V]())
}
func deepCloneMap[K comparable, V views.Cloner[V]](m map[K]V) map[K]V {
c := make(map[K]V, len(m))
for i := range m {
c[i] = m[i].Clone()
}
return c
}
// StructMapView is a read-only view of a [StructMap].
type StructMapView[K MapKeyType, T views.ViewCloner[T, V], V views.StructView[T]] struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *StructMap[K, T]
}
// StructMapViewOf returns a readonly view of m.
// It is used by [tailscale.com/cmd/viewer].
func StructMapViewOf[K MapKeyType, T views.ViewCloner[T, V], V views.StructView[T]](m *StructMap[K, T]) StructMapView[K, T, V] {
return StructMapView[K, T, V]{m}
}
// Valid reports whether the underlying [StructMap] is non-nil.
func (mv StructMapView[K, T, V]) Valid() bool {
return mv.ж != nil
}
// AsStruct implements [views.StructView] by returning a clone of the preference
// which aliases no memory with the original.
func (mv StructMapView[K, T, V]) AsStruct() *StructMap[K, T] {
if mv.ж == nil {
return nil
}
return mv.ж.Clone()
}
// IsSet reports whether the preference has a value set.
func (mv StructMapView[K, T, V]) IsSet() bool {
return mv.ж.IsSet()
}
// Value returns a read-only view of the value if the preference has a value set.
// Otherwise, it returns a read-only view of its default value.
func (mv StructMapView[K, T, V]) Value() views.MapFn[K, T, V] {
return views.MapFnOf(mv.ж.Value(), func(t T) V { return t.View() })
}
// ValueOk returns a read-only view of the value and true if the preference has a value set.
// Otherwise, it returns an invalid view and false.
func (mv StructMapView[K, T, V]) ValueOk() (val views.MapFn[K, T, V], ok bool) {
if v, ok := mv.ж.ValueOk(); ok {
return views.MapFnOf(v, func(t T) V { return t.View() }), true
}
return views.MapFn[K, T, V]{}, false
}
// DefaultValue returns a read-only view of the default value of the preference.
func (mv StructMapView[K, T, V]) DefaultValue() views.MapFn[K, T, V] {
return views.MapFnOf(mv.ж.DefaultValue(), func(t T) V { return t.View() })
}
// Managed reports whether the preference is managed via MDM, Group Policy, or similar means.
func (mv StructMapView[K, T, V]) IsManaged() bool {
return mv.ж.IsManaged()
}
// ReadOnly reports whether the preference is read-only and cannot be changed by user.
func (mv StructMapView[K, T, V]) IsReadOnly() bool {
return mv.ж.IsReadOnly()
}
// Equal reports whether mv and mv2 are equal.
func (mv StructMapView[K, T, V]) Equal(mv2 StructMapView[K, T, V]) bool {
if !mv.Valid() && !mv2.Valid() {
return true
}
if mv.Valid() != mv2.Valid() {
return false
}
return mv.ж.Equal(*mv2.ж)
}
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
func (mv StructMapView[K, T, V]) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
return mv.ж.MarshalJSONV2(out, opts)
}
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
func (mv *StructMapView[K, T, V]) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
var x StructMap[K, T]
if err := x.UnmarshalJSONV2(in, opts); err != nil {
return err
}
mv.ж = &x
return nil
}
// MarshalJSON implements [json.Marshaler].
func (mv StructMapView[K, T, V]) MarshalJSON() ([]byte, error) {
return jsonv2.Marshal(mv) // uses MarshalJSONV2
}
// UnmarshalJSON implements [json.Unmarshaler].
func (mv *StructMapView[K, T, V]) UnmarshalJSON(b []byte) error {
return jsonv2.Unmarshal(b, mv) // uses UnmarshalJSONV2
}