sq/libsq/core/options/options.go
Neil O'Toole c7bba4dfe4
go1.21: changes to support slog as part of stdlib (#299)
* go1.21: changes to support slog as part of stdlib

* Removed accidentially checked-in line of code

* Fixed minor linting issues; reenable typecheck

* go1.21: switched to stdlib slices pkg
2023-08-12 12:54:14 -06:00

271 lines
5.9 KiB
Go

// Package options implements config options. This package is currently a bit
// of an experiment. Objectives:
// - Options are key-value pairs.
// - Options can come from default config, individual source config, and flags.
// - Support the ability to edit config in $EDITOR, providing contextual information
// about the Opt instance.
// - Values are strongly typed (e.g. int, time.Duration)
// - An individual Opt instance can be specified near where it is used.
// - New types of Opt can be defined, near where they are used.
//
// It is noted that these requirements could probably largely be met using
// packages such as spf13/viper. AGain, this is largely an experiment.
package options
import (
"context"
"fmt"
"log/slog"
"slices"
"sync"
"time"
"github.com/samber/lo"
)
type contextKey struct{}
// NewContext returns a context that contains the given Options.
// Use FromContext to retrieve the Options.
//
// NOTE: It's questionable whether we need to engage in this context
// business with Options. This is a bit of an experiment.
func NewContext(ctx context.Context, o Options) context.Context {
return context.WithValue(ctx, contextKey{}, o)
}
// FromContext returns the Options stored in ctx by NewContext, or nil
// if no such Options.
func FromContext(ctx context.Context) Options {
v := ctx.Value(contextKey{})
if v == nil {
return nil
}
if v, ok := v.(Options); ok {
return v
}
return nil
}
// Registry is a registry of Opt instances.
type Registry struct {
mu sync.Mutex
opts []Opt
}
// Add adds opts to r. It panics if any element of opts is already registered.
func (r *Registry) Add(opts ...Opt) {
r.mu.Lock()
defer r.mu.Unlock()
for _, opt := range opts {
for i := range r.opts {
if r.opts[i].Key() == opt.Key() {
panic(fmt.Sprintf("Opt %s is already registered", opt.Key()))
}
}
r.opts = append(r.opts, opt)
}
}
// LogValue implements slog.LogValuer.
func (r *Registry) LogValue() slog.Value {
r.mu.Lock()
defer r.mu.Unlock()
as := make([]slog.Attr, len(r.opts))
for i, opt := range r.opts {
as[i] = slog.String(opt.Key(), fmt.Sprintf("%T", opt))
}
return slog.GroupValue(as...)
}
// Visit visits each Opt in r. Be careful with concurrent access
// via this method.
func (r *Registry) Visit(fn func(opt Opt) error) error {
if r == nil {
return nil
}
for i := range r.opts {
if err := fn(r.opts[i]); err != nil {
return err
}
}
return nil
}
// Get returns the Opt registered in r using key, or nil.
func (r *Registry) Get(key string) Opt {
r.mu.Lock()
defer r.mu.Unlock()
for _, opt := range r.opts {
if opt.Key() == key {
return opt
}
}
return nil
}
// Keys returns the keys of each Opt in r.
func (r *Registry) Keys() []string {
r.mu.Lock()
defer r.mu.Unlock()
keys := make([]string, len(r.opts))
for i := range r.opts {
keys[i] = r.opts[i].Key()
}
return keys
}
// Opts returns a new slice containing each Opt registered with r.
func (r *Registry) Opts() []Opt {
r.mu.Lock()
defer r.mu.Unlock()
opts := make([]Opt, len(r.opts))
copy(opts, r.opts)
return opts
}
// Process processes o, returning a new Options. Process should be invoked
// after the Options has been loaded from config, but before it is used by the
// program. Process iterates over the registered Opts, and invokes Process for
// each Opt that implements Processor. This facilitates munging of backing
// values, e.g. for options.Duration, a string "1m30s" is converted to a time.Duration.
func (r *Registry) Process(o Options) (Options, error) {
if o == nil {
return nil, nil //nolint:nilnil
}
opts := r.opts
o2 := Options{}
for _, opt := range opts {
if v, ok := o[opt.Key()]; ok {
o2[opt.Key()] = v
}
}
var err error
for _, opt := range opts {
if o2, err = opt.Process(o2); err != nil {
return nil, err
}
}
return o2, nil
}
// Options is a map of Opt.Key to a value.
type Options map[string]any
// Clone clones o.
func (o Options) Clone() Options {
if o == nil {
return nil
}
o2 := Options{}
for k, v := range o {
o2[k] = v
}
return o2
}
// Keys returns the sorted set of keys in o.
func (o Options) Keys() []string {
keys := lo.Keys(o)
slices.Sort(keys)
return keys
}
// IsSet returns true if opt is set on o.
func (o Options) IsSet(opt Opt) bool {
_, ok := o[opt.Key()]
return ok
}
// LogValue implements slog.LogValuer.
func (o Options) LogValue() slog.Value {
if o == nil {
return slog.Value{}
}
attrs := make([]slog.Attr, 0, len(o))
for k, v := range o {
switch v := v.(type) {
case int:
attrs = append(attrs, slog.Int(k, v))
case string:
attrs = append(attrs, slog.String(k, v))
case bool:
attrs = append(attrs, slog.Bool(k, v))
case time.Duration:
attrs = append(attrs, slog.Duration(k, v))
default:
attrs = append(attrs, slog.Any(k, v))
}
}
return slog.GroupValue(attrs...)
}
// Merge overlays each of overlays onto base, returning a new Options.
// It is acceptable for base to be nil.
func Merge(base Options, overlays ...Options) Options {
var o Options
if base == nil {
o = Options{}
} else {
o = base.Clone()
}
for _, overlay := range overlays {
for k, v := range overlay {
o[k] = v
}
}
return o
}
// Effective returns a new Options containing the effective values
// of each Opt. That is to say, the returned Options contains either
// the actual value of each Opt in o, or the default value for that Opt,
// but it will not contain values for any Opt not in opts.
func Effective(o Options, opts ...Opt) Options {
o2 := Options{}
for _, opt := range opts {
v := opt.GetAny(o)
o2[opt.Key()] = v
}
return o2
}
// Processor performs processing on o.
type Processor interface {
// Process processes o. The returned Options may be a new instance,
// with mutated values.
Process(o Options) (Options, error)
}
// DeleteNil deletes any keys with nil values.
func DeleteNil(o Options) Options {
if o == nil {
return nil
}
o = o.Clone()
for k, v := range o {
if v == nil {
delete(o, k)
}
}
return o
}