123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403 |
- package options
- import (
- "encoding/json"
- "fmt"
- "iter"
- "log/slog"
- "maps"
- "slices"
- "strconv"
- "strings"
- "time"
- )
- // Options is an interface for storing and retrieving dynamic option values.
- //
- // Copies of Options are shallow, meaning the underlying map is shared.
- type Options struct {
- m map[string]any
- main *Options // Pointer to the main Options if this is a child
- child *Options // Pointer to the child Options
- }
- // New creates a new Options map
- func New() *Options {
- return &Options{
- m: make(map[string]any),
- }
- }
- // Main returns the main Options if this is a child Options.
- // If this is the main Options, it returns itself.
- func (o *Options) Main() *Options {
- if o.main == nil {
- return o
- }
- return o.main
- }
- // Child returns the child Options if any.
- func (o *Options) Child() *Options {
- return o.child
- }
- // Descendants returns an iterator over the child Options if any.
- func (o *Options) Descendants() iter.Seq[*Options] {
- return func(yield func(*Options) bool) {
- for c := o.child; c != nil; c = c.child {
- if !yield(c) {
- return
- }
- }
- }
- }
- // HasChild checks if the Options has a child Options.
- func (o *Options) HasChild() bool {
- return o.child != nil
- }
- // AddChild creates a new child Options that inherits from the current Options.
- // If the current Options already has a child, it returns the existing child.
- func (o *Options) AddChild() *Options {
- if o.child != nil {
- return o.child
- }
- child := New()
- child.main = o.Main()
- o.child = child
- return child
- }
- // Depth returns the depth of the Options in the hierarchy.
- // The main Options has a depth of 0, its child has a depth of 1, and so on.
- func (o *Options) Depth() int {
- depth := 0
- for p := o.main; p != nil && p != o; p = p.child {
- depth++
- }
- return depth
- }
- // Get retrieves a value of the specified type from the options.
- // If the key does not exist, it returns the provided default value.
- // If the value exists but is of a different type, it panics.
- func Get[T any](o *Options, key string, def T) T {
- v, ok := o.m[key]
- if !ok {
- return def
- }
- if vt, ok := v.(T); ok {
- return vt
- }
- panic(newTypeMismatchError(key, v, def))
- }
- // AppendToSlice appends a value to a slice option.
- // If the option does not exist, it creates a new slice with the value.
- func AppendToSlice[T any](o *Options, key string, value ...T) {
- if v, ok := o.m[key]; ok {
- vt := v.([]T)
- o.m[key] = append(vt, value...)
- return
- }
- o.m[key] = append([]T(nil), value...)
- }
- // SliceContains checks if a slice option contains a specific value.
- // If the option does not exist, it returns false.
- // If the value exists but is of a different type, it panics.
- func SliceContains[T comparable](o *Options, key string, value T) bool {
- arr := Get(o, key, []T(nil))
- return slices.Contains(arr, value)
- }
- // Set sets a value for a specific option key.
- func (o *Options) Set(key string, value any) {
- o.m[key] = value
- }
- // Propagate propagates a value under the given key to the Options descendants if any.
- func (o *Options) Propagate(key string) {
- if o.child == nil {
- return
- }
- if v, ok := o.m[key]; ok {
- for c := range o.Descendants() {
- c.m[key] = v
- }
- }
- }
- // Delete removes an option by its key.
- func (o *Options) Delete(key string) {
- delete(o.m, key)
- }
- // DeleteByPrefix removes all options that start with the given prefix.
- func (o *Options) DeleteByPrefix(prefix string) {
- for k := range o.m {
- if strings.HasPrefix(k, prefix) {
- delete(o.m, k)
- }
- }
- }
- // DeleteFromDescendants removes an option by its key from the Options descendants if any.
- func (o *Options) DeleteFromDescendants(key string) {
- if o.child == nil {
- return
- }
- for c := range o.Descendants() {
- delete(c.m, key)
- }
- }
- // CopyValue copies a value from one option key to another.
- func (o *Options) CopyValue(fromKey, toKey string) {
- if v, ok := o.m[fromKey]; ok {
- o.m[toKey] = v
- }
- }
- // Has checks if an option key exists.
- func (o *Options) Has(key string) bool {
- _, ok := o.m[key]
- return ok
- }
- // GetInt retrieves an int value from the options.
- // If the key does not exist, GetInt returns the provided default value.
- // If the key exists but the value is of a different integer type,
- // GetInt converts it to int.
- // If the key exists but the value is not an integer type, GetInt panics.
- func (o *Options) GetInt(key string, def int) int {
- v, ok := o.m[key]
- if !ok {
- return def
- }
- switch t := v.(type) {
- case int:
- return t
- case int8:
- return int(t)
- case int16:
- return int(t)
- case int32:
- return int(t)
- case int64:
- return int(t)
- case uint:
- return int(t)
- case uint8:
- return int(t)
- case uint16:
- return int(t)
- case uint32:
- return int(t)
- case uint64:
- return int(t)
- default:
- panic(newTypeMismatchError(key, v, def))
- }
- }
- // GetFloat retrieves a float64 value from the options.
- // If the key does not exist, GetFloat returns the provided default value.
- // If the key value exists but the value is of a different float or integer type,
- // GetFloat converts it to float64.
- // If the key exists but the value is not a float or integer type, GetFloat panics.
- func (o *Options) GetFloat(key string, def float64) float64 {
- v, ok := o.m[key]
- if !ok {
- return def
- }
- switch t := v.(type) {
- case int:
- return float64(t)
- case int8:
- return float64(t)
- case int16:
- return float64(t)
- case int32:
- return float64(t)
- case int64:
- return float64(t)
- case uint:
- return float64(t)
- case uint8:
- return float64(t)
- case uint16:
- return float64(t)
- case uint32:
- return float64(t)
- case uint64:
- return float64(t)
- case float32:
- return float64(t)
- case float64:
- return t
- default:
- panic(newTypeMismatchError(key, v, def))
- }
- }
- // GetString retrieves a string value.
- // If the key doesn't exist, it returns the provided default value.
- // If the value exists but is of a different type, it panics.
- func (o *Options) GetString(key string, def string) string {
- return Get(o, key, def)
- }
- // GetBool retrieves a bool value.
- // If the key doesn't exist, it returns the provided default value.
- // If the value exists but is of a different type, it panics.
- func (o *Options) GetBool(key string, def bool) bool {
- return Get(o, key, def)
- }
- // GetTime retrieves a [time.Time] value.
- // If the key doesn't exist, it returns the zero time.
- // If the value exists but is of a different type, it panics.
- func (o *Options) GetTime(key string) time.Time {
- return Get(o, key, time.Time{})
- }
- // Map returns a copy of the Options as a map[string]any
- // If the Options has a child, it combines the main and child maps,
- // prepending each key with the options depth
- // (e.g., "0.key" for main options, "1.key" for child options, "2.key" for grandchild options, etc.)
- func (o *Options) Map() map[string]any {
- if o.child == nil {
- return maps.Clone(o.m)
- }
- totalEntries := len(o.m)
- for c := range o.Descendants() {
- totalEntries += len(c.m)
- }
- result := make(map[string]any, totalEntries)
- for k, v := range o.m {
- result["0."+k] = v
- }
- depth := 1
- for c := range o.Descendants() {
- for k, v := range c.m {
- result[strconv.Itoa(depth)+"."+k] = v
- }
- depth++
- }
- return result
- }
- // NestedMap returns Options as a nested map[string]any.
- // Each key is split by dots (.) and the resulting keys are used to create a nested structure.
- // If the Options has a child, it puts the main and child maps under "0", "1", "2", etc. keys
- // representing the options depth
- // (e.g., "0" for main options, "1" for child options, "2" for grandchild options, etc.)
- func (o *Options) NestedMap() map[string]any {
- if o.child == nil {
- return o.nestedMap()
- }
- totalMaps := 1
- for child := o.child; child != nil; child = child.child {
- totalMaps++
- }
- result := make(map[string]any, totalMaps)
- result["0"] = o.nestedMap()
- depth := 1
- for c := range o.Descendants() {
- result[strconv.Itoa(depth)] = c.nestedMap()
- depth++
- }
- return result
- }
- func (o *Options) nestedMap() map[string]any {
- nm := make(map[string]any)
- for k, v := range o.m {
- nestedMapSet(nm, k, v)
- }
- return nm
- }
- // String returns Options as a string representation of the map.
- func (o *Options) String() string {
- return fmt.Sprintf("%v", o.Map())
- }
- // MarshalJSON returns Options as a JSON byte slice.
- func (o *Options) MarshalJSON() ([]byte, error) {
- return json.Marshal(o.NestedMap())
- }
- // LogValue returns Options as [slog.Value]
- func (o *Options) LogValue() slog.Value {
- return toSlogValue(o.NestedMap())
- }
- // nestedMapSet sets a value in a nested map[string]any structure.
- // If the key has more than one element, it creates nested maps as needed.
- func nestedMapSet(m map[string]any, key string, value any) {
- key, rest, isGroup := strings.Cut(key, ".")
- if !isGroup {
- m[key] = value
- return
- }
- mm, ok := m[key].(map[string]any)
- if !ok {
- mm = make(map[string]any)
- }
- nestedMapSet(mm, rest, value)
- m[key] = mm
- }
- func toSlogValue(v any) slog.Value {
- m, ok := v.(map[string]any)
- if !ok {
- return slog.AnyValue(v)
- }
- attrs := make([]slog.Attr, 0, len(m))
- for k, v := range m {
- attrs = append(attrs, slog.Attr{Key: k, Value: toSlogValue(v)})
- }
- // Sort attributes by key to have a consistent order
- slices.SortFunc(attrs, func(a, b slog.Attr) int {
- return strings.Compare(a.Key, b.Key)
- })
- return slog.GroupValue(attrs...)
- }
|