1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2025-12-23 22:11:10 +02:00
Files
imgproxy/options/options.go

404 lines
9.1 KiB
Go

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...)
}