Short and single-letter names in application code become full words (tests keep theirs). Wrapped calls, signatures and composite literals put one item per line, and one-line function bodies are expanded. Exported names and groups of constants get doc comments, and comments refer to the renamed identifiers.
147 lines
4.0 KiB
Go
147 lines
4.0 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"maps"
|
|
"reflect"
|
|
"slices"
|
|
"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 {
|
|
Database JSCDatabase `cfg:"database"`
|
|
Logging JSCLogging `cfg:"logging"`
|
|
Server JSCServer `cfg:"server"`
|
|
Sources JSCSources `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 i := range keys {
|
|
key := keys[i]
|
|
keyType, _ := conf.TypeOf(key)
|
|
if keyType == reflect.Invalid {
|
|
continue
|
|
}
|
|
value, ok := viperValue(key, keyType)
|
|
if !ok {
|
|
slog.Error("unknown config key type", "key", key, "kind", keyType.String())
|
|
continue
|
|
}
|
|
if err := conf.SetValue(key, value); err != nil {
|
|
slog.Warn("ignoring invalid config value", "key", key, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// viperValue reads key from Viper as a value of kind. ok is false for a kind
|
|
// LoadFromViper doesn't handle.
|
|
func viperValue(key string, kind reflect.Kind) (value any, ok bool) {
|
|
switch kind {
|
|
case reflect.String:
|
|
return viper.GetString(key), true
|
|
case reflect.Int:
|
|
return viper.GetInt(key), true
|
|
case reflect.Bool:
|
|
return viper.GetBool(key), true
|
|
case reflect.Slice:
|
|
// A string, e.g. from an environment variable, is passed on for
|
|
// SetValue to split on commas; GetStringSlice would split it on
|
|
// spaces. A list of objects (a list of sections) is passed on as it
|
|
// is, as GetStringSlice would flatten it.
|
|
switch value := viper.Get(key).(type) {
|
|
case string:
|
|
return value, true
|
|
case []map[string]any: // as LoadToViper sets it
|
|
return value, true
|
|
case []any:
|
|
if len(value) > 0 {
|
|
if _, ok := value[0].(map[string]any); ok {
|
|
return value, true
|
|
}
|
|
}
|
|
}
|
|
return viper.GetStringSlice(key), true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
// 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 <type>" line per key, with
|
|
// dotted keys, sorted by key.
|
|
func (c JobSearchConfig) String() string {
|
|
var builder strings.Builder
|
|
flat := c.Flatten()
|
|
for _, key := range slices.Sorted(maps.Keys(flat)) {
|
|
kind, _ := c.TypeOf(key)
|
|
fmt.Fprint(&builder, key, " = ", flat[key], " <", kind, ">\n")
|
|
}
|
|
return builder.String()
|
|
}
|
|
|
|
// JSON formats the configuration as indented JSON.
|
|
func (c JobSearchConfig) JSON() string {
|
|
confMap := c.Map()
|
|
jbytes, _ := json.MarshalIndent(confMap, "", " ")
|
|
return string(jbytes)
|
|
}
|
|
|
|
// YAML formats the configuration as YAML.
|
|
func (c JobSearchConfig) YAML() string {
|
|
confMap := c.Map()
|
|
ybytes, _ := yaml.Marshal(confMap)
|
|
return string(ybytes)
|
|
}
|