package config import ( "encoding/json" "fmt" "log/slog" "reflect" "strings" "github.com/spf13/viper" "go.yaml.in/yaml/v3" ) // EnvPrefix prefixes environment variables that override configuration keys, // e.g. JOBSEARCH_LOGGING_LEVEL for logging.level. const EnvPrefix string = "JOBSEARCH" // EtcConfigPath is searched for config.* when no config file is given, before // the working directory. const EtcConfigPath string = "/etc/job-search" // JobSearchConfig is the application's whole configuration, one field per // section. Each field's cfg tag must match the section's JSCK* constant. type JobSearchConfig struct { Logging JobSearchConfigLogging `cfg:"logging"` Sources JobSearchConfigSources `cfg:"sources"` } // LoadDefaultConfig sets Viper's defaults from the configuration's default // struct tags. func LoadDefaultConfig() { var conf JobSearchConfig if err := applyDefaults(&conf); err != nil { panic(fmt.Sprintf("config: %v", err)) } LoadToViper(conf) } // ---------- Section methods (see Section) ---------- func (c JobSearchConfig) Map() map[string]any { return mapOf(c) } func (c JobSearchConfig) Flatten() map[string]any { return flattenOf("", c) } func (c JobSearchConfig) TypeOf(key string) (reflect.Kind, error) { return typeOf(c, key) } func (c JobSearchConfig) ValueOf(key string) (any, error) { return valueOf(c, key) } func (c *JobSearchConfig) SetValue(key string, value any) error { return setValue(c, key, value) } // LoadFromViper copies every key Viper knows that conf also has into conf. // Unknown keys are skipped; invalid values are logged and ignored. func LoadFromViper(conf *JobSearchConfig) { keys := viper.AllKeys() for idx := range keys { key := keys[idx] keyType, _ := conf.TypeOf(key) if keyType == reflect.Invalid { continue } var err error switch keyType { case reflect.String: err = conf.SetValue(key, viper.GetString(key)) case reflect.Int: err = conf.SetValue(key, viper.GetInt(key)) case reflect.Bool: err = conf.SetValue(key, viper.GetBool(key)) default: slog.Error(fmt.Sprintf("unknown key type: %s", keyType)) } if err != nil { slog.Warn("ignoring invalid config value", "key", key, "err", err) } } } // LoadToViper sets every key of conf as a Viper default. func LoadToViper(conf JobSearchConfig) { flatMap := conf.Flatten() for key := range flatMap { viper.SetDefault(key, flatMap[key]) } } // String formats the configuration one "key = value " line per key, with // dotted keys. func (conf JobSearchConfig) String() string { var sb strings.Builder for k, v := range conf.Flatten() { typ, _ := conf.TypeOf(k) fmt.Fprint(&sb, k, " = ", v, " <", typ, ">\n") } return sb.String() } // JSON formats the configuration as indented JSON. func (conf JobSearchConfig) JSON() string { confMap := conf.Map() jbytes, _ := json.MarshalIndent(confMap, "", " ") return string(jbytes) } // YAML formats the configuration as YAML. func (conf JobSearchConfig) YAML() string { confMap := conf.Map() ybytes, _ := yaml.Marshal(confMap) return string(ybytes) }