summaryrefslogtreecommitdiffhomepage
path: root/types/prefs/item.go
blob: fdb9301f9fdf89f2235658d46dd4892c929e5aa8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// Copyright (c) Tailscale Inc & contributors
// 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.ж)
}

// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (iv ItemView[T, V]) MarshalJSONTo(out *jsontext.Encoder) error {
	return iv.ж.MarshalJSONTo(out)
}

// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (iv *ItemView[T, V]) UnmarshalJSONFrom(in *jsontext.Decoder) error {
	var x Item[T]
	if err := x.UnmarshalJSONFrom(in); 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 MarshalJSONTo
}

// UnmarshalJSON implements [json.Unmarshaler].
func (iv *ItemView[T, V]) UnmarshalJSON(b []byte) error {
	return jsonv2.Unmarshal(b, iv) // uses UnmarshalJSONFrom
}