tailscale/types/prefs/prefs.go
Nick Khyl af3d3c433b 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>
2024-08-21 12:44:38 -05:00

180 lines
5.8 KiB
Go

// 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
}