// feedkit/config/params.go package config import ( "math" "strconv" "strings" "time" ) // ---- SourceConfig param helpers ---- // ParamString returns the first non-empty string found for any of the provided keys. // Values must actually be strings in the decoded config; other types are ignored. // // This keeps cfg.Params flexible (map[string]any) while letting callers stay type-safe. func (cfg SourceConfig) ParamString(keys ...string) (string, bool) { return paramString(cfg.Params, keys...) } // ParamStringDefault returns ParamString(keys...) if present; otherwise it returns def. // This is the “polite default” helper used by drivers for optional fields like user-agent. func (cfg SourceConfig) ParamStringDefault(def string, keys ...string) string { if s, ok := cfg.ParamString(keys...); ok { return s } return strings.TrimSpace(def) } // ParamBool returns the first boolean found for any of the provided keys. // // Accepted types in Params: // - bool // - string: parsed via strconv.ParseBool ("true"/"false"/"1"/"0", etc.) func (cfg SourceConfig) ParamBool(keys ...string) (bool, bool) { return paramBool(cfg.Params, keys...) } func (cfg SourceConfig) ParamBoolDefault(def bool, keys ...string) bool { if v, ok := cfg.ParamBool(keys...); ok { return v } return def } // ParamInt returns the first integer-like value found for any of the provided keys. // // Accepted types in Params: // - any integer type (int, int64, uint32, ...) // - float32/float64 ONLY if it is an exact integer (e.g. 15.0) // - string: parsed via strconv.Atoi (e.g. "42") func (cfg SourceConfig) ParamInt(keys ...string) (int, bool) { return paramInt(cfg.Params, keys...) } func (cfg SourceConfig) ParamIntDefault(def int, keys ...string) int { if v, ok := cfg.ParamInt(keys...); ok { return v } return def } // ParamDuration returns the first duration-like value found for any of the provided keys. // // Accepted types in Params: // - time.Duration // - string: parsed via time.ParseDuration (e.g. "250ms", "30s", "5m") // - if the string is all digits (e.g. "30"), it is interpreted as SECONDS // - numeric: interpreted as SECONDS (e.g. 30 => 30s) // // Rationale: Param durations are usually timeouts/backoffs; seconds are a sane numeric default. // If you want minutes/hours, prefer a duration string like "5m" or "1h". func (cfg SourceConfig) ParamDuration(keys ...string) (time.Duration, bool) { return paramDuration(cfg.Params, keys...) } func (cfg SourceConfig) ParamDurationDefault(def time.Duration, keys ...string) time.Duration { if v, ok := cfg.ParamDuration(keys...); ok { return v } return def } // ParamStringSlice returns the first string-slice-like value found for any of the provided keys. // // Accepted types in Params: // - []string // - []any where each element is a string // - string: // - if it contains commas, split on commas (",") and trim each item // - otherwise treat as a single-item list // // Empty/blank items are removed. func (cfg SourceConfig) ParamStringSlice(keys ...string) ([]string, bool) { return paramStringSlice(cfg.Params, keys...) } // ---- SinkConfig param helpers ---- // ParamString returns the first non-empty string found for any of the provided keys // in SinkConfig.Params. (Same rationale as SourceConfig.ParamString.) func (cfg SinkConfig) ParamString(keys ...string) (string, bool) { return paramString(cfg.Params, keys...) } // ParamStringDefault returns ParamString(keys...) if present; otherwise it returns def. // Symmetric helper for sink implementations. func (cfg SinkConfig) ParamStringDefault(def string, keys ...string) string { if s, ok := cfg.ParamString(keys...); ok { return s } return strings.TrimSpace(def) } func (cfg SinkConfig) ParamBool(keys ...string) (bool, bool) { return paramBool(cfg.Params, keys...) } func (cfg SinkConfig) ParamBoolDefault(def bool, keys ...string) bool { if v, ok := cfg.ParamBool(keys...); ok { return v } return def } func (cfg SinkConfig) ParamInt(keys ...string) (int, bool) { return paramInt(cfg.Params, keys...) } func (cfg SinkConfig) ParamIntDefault(def int, keys ...string) int { if v, ok := cfg.ParamInt(keys...); ok { return v } return def } func (cfg SinkConfig) ParamDuration(keys ...string) (time.Duration, bool) { return paramDuration(cfg.Params, keys...) } func (cfg SinkConfig) ParamDurationDefault(def time.Duration, keys ...string) time.Duration { if v, ok := cfg.ParamDuration(keys...); ok { return v } return def } func (cfg SinkConfig) ParamStringSlice(keys ...string) ([]string, bool) { return paramStringSlice(cfg.Params, keys...) } // ---- shared implementations (package-private) ---- func paramAny(params map[string]any, keys ...string) (any, bool) { if params == nil { return nil, false } for _, k := range keys { v, ok := params[k] if !ok || v == nil { continue } return v, true } return nil, false } func paramString(params map[string]any, keys ...string) (string, bool) { for _, k := range keys { if params == nil { return "", false } v, ok := params[k] if !ok || v == nil { continue } s, ok := v.(string) if !ok { continue } s = strings.TrimSpace(s) if s == "" { continue } return s, true } return "", false } func paramBool(params map[string]any, keys ...string) (bool, bool) { v, ok := paramAny(params, keys...) if !ok { return false, false } switch t := v.(type) { case bool: return t, true case string: s := strings.TrimSpace(t) if s == "" { return false, false } parsed, err := strconv.ParseBool(s) if err != nil { return false, false } return parsed, true default: return false, false } } func paramInt(params map[string]any, keys ...string) (int, bool) { v, ok := paramAny(params, keys...) if !ok { return 0, false } switch t := v.(type) { case int: return t, true case int8: return int(t), true case int16: return int(t), true case int32: return int(t), true case int64: return int(t), true case uint: return int(t), true case uint8: return int(t), true case uint16: return int(t), true case uint32: return int(t), true case uint64: return int(t), true case float32: f := float64(t) if math.IsNaN(f) || math.IsInf(f, 0) { return 0, false } if math.Trunc(f) != f { return 0, false } return int(f), true case float64: if math.IsNaN(t) || math.IsInf(t, 0) { return 0, false } if math.Trunc(t) != t { return 0, false } return int(t), true case string: s := strings.TrimSpace(t) if s == "" { return 0, false } n, err := strconv.Atoi(s) if err != nil { return 0, false } return n, true default: return 0, false } } func paramDuration(params map[string]any, keys ...string) (time.Duration, bool) { v, ok := paramAny(params, keys...) if !ok { return 0, false } switch t := v.(type) { case time.Duration: if t <= 0 { return 0, false } return t, true case string: s := strings.TrimSpace(t) if s == "" { return 0, false } // Numeric strings are interpreted as seconds (see doc comment). if isAllDigits(s) { n, err := strconv.Atoi(s) if err != nil || n <= 0 { return 0, false } return time.Duration(n) * time.Second, true } d, err := time.ParseDuration(s) if err != nil || d <= 0 { return 0, false } return d, true case int: if t <= 0 { return 0, false } return time.Duration(t) * time.Second, true case int64: if t <= 0 { return 0, false } return time.Duration(t) * time.Second, true case float64: if math.IsNaN(t) || math.IsInf(t, 0) || t <= 0 { return 0, false } // Allow fractional seconds. secs := t * float64(time.Second) return time.Duration(secs), true case float32: f := float64(t) if math.IsNaN(f) || math.IsInf(f, 0) || f <= 0 { return 0, false } secs := f * float64(time.Second) return time.Duration(secs), true default: return 0, false } } func paramStringSlice(params map[string]any, keys ...string) ([]string, bool) { v, ok := paramAny(params, keys...) if !ok { return nil, false } clean := func(items []string) ([]string, bool) { out := make([]string, 0, len(items)) for _, it := range items { it = strings.TrimSpace(it) if it == "" { continue } out = append(out, it) } if len(out) == 0 { return nil, false } return out, true } switch t := v.(type) { case []string: return clean(t) case []any: tmp := make([]string, 0, len(t)) for _, it := range t { s, ok := it.(string) if !ok { continue } tmp = append(tmp, s) } return clean(tmp) case string: s := strings.TrimSpace(t) if s == "" { return nil, false } if strings.Contains(s, ",") { parts := strings.Split(s, ",") return clean(parts) } return clean([]string{s}) default: return nil, false } } func isAllDigits(s string) bool { for _, r := range s { if r < '0' || r > '9' { return false } } return len(s) > 0 }