140 lines
3.0 KiB
Go
140 lines
3.0 KiB
Go
|
|
package config
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"errors"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"apigo.cc/go/cast"
|
||
|
|
"apigo.cc/go/convert"
|
||
|
|
"apigo.cc/go/file"
|
||
|
|
)
|
||
|
|
|
||
|
|
type Duration time.Duration
|
||
|
|
|
||
|
|
func (d *Duration) UnmarshalJSON(value []byte) error {
|
||
|
|
result, err := time.ParseDuration(string(bytes.Trim(value, "\"")))
|
||
|
|
if err == nil {
|
||
|
|
*d = Duration(result)
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d *Duration) UnmarshalYAML(value interface{}) error {
|
||
|
|
result, err := time.ParseDuration(cast.String(value))
|
||
|
|
if err == nil {
|
||
|
|
*d = Duration(result)
|
||
|
|
}
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
||
|
|
return []byte("\"" + time.Duration(d).String() + "\""), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d Duration) MarshalYAML() (interface{}, error) {
|
||
|
|
return time.Duration(d).String(), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (d Duration) TimeDuration() time.Duration {
|
||
|
|
return time.Duration(d)
|
||
|
|
}
|
||
|
|
|
||
|
|
// LoadConfig loads configuration from a file, then applies env.json/yaml if exists,
|
||
|
|
// and finally overrides with environment variables.
|
||
|
|
func LoadConfig(name string, conf interface{}) error {
|
||
|
|
var errs []error
|
||
|
|
|
||
|
|
// 1. Load base file
|
||
|
|
filename := resolveConfigFilePath(name)
|
||
|
|
if filename != "" {
|
||
|
|
if err := file.UnmarshalFile(filename, conf); err != nil {
|
||
|
|
errs = append(errs, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Load env.json/yaml if present
|
||
|
|
envFile := resolveConfigFilePath("env")
|
||
|
|
if envFile != "" {
|
||
|
|
envConf := map[string]interface{}{}
|
||
|
|
if err := file.UnmarshalFile(envFile, &envConf); err == nil {
|
||
|
|
convert.To(envConf, conf)
|
||
|
|
} else {
|
||
|
|
errs = append(errs, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Apply Environment Variable overrides
|
||
|
|
applyEnvOverrides(conf)
|
||
|
|
|
||
|
|
return errors.Join(errs...)
|
||
|
|
}
|
||
|
|
|
||
|
|
func resolveConfigPath(name string) string {
|
||
|
|
// If it already has an extension, check directly
|
||
|
|
if strings.HasSuffix(name, ".json") || strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") {
|
||
|
|
if file.Exists(name) {
|
||
|
|
return name
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
// Otherwise, check for .yml then .json
|
||
|
|
if file.Exists(name + ".yml") {
|
||
|
|
return name + ".yml"
|
||
|
|
}
|
||
|
|
if file.Exists(name + ".json") {
|
||
|
|
return name + ".json"
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func resolveConfigFilePath(name string) string {
|
||
|
|
// Search in: current dir, executable dir, user home dir
|
||
|
|
dirs := []string{"."}
|
||
|
|
if execPath, err := os.Executable(); err == nil {
|
||
|
|
dirs = append(dirs, filepath.Dir(execPath))
|
||
|
|
}
|
||
|
|
if home, err := os.UserHomeDir(); err == nil {
|
||
|
|
dirs = append(dirs, home)
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, d := range dirs {
|
||
|
|
p := filepath.Join(d, name)
|
||
|
|
if f := resolveConfigPath(p); f != "" {
|
||
|
|
return f
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
|
||
|
|
func applyEnvOverrides(conf interface{}) {
|
||
|
|
envMap := make(map[string]interface{})
|
||
|
|
for _, e := range os.Environ() {
|
||
|
|
pair := strings.SplitN(e, "=", 2)
|
||
|
|
if len(pair) != 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
// Convert DB_HOST to {"db": {"host": value}}
|
||
|
|
parts := strings.Split(strings.ToLower(pair[0]), "_")
|
||
|
|
curr := envMap
|
||
|
|
for i := 0; i < len(parts)-1; i++ {
|
||
|
|
if _, ok := curr[parts[i]]; !ok {
|
||
|
|
curr[parts[i]] = make(map[string]interface{})
|
||
|
|
}
|
||
|
|
if next, ok := curr[parts[i]].(map[string]interface{}); ok {
|
||
|
|
curr = next
|
||
|
|
} else {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
curr[parts[len(parts)-1]] = pair[1]
|
||
|
|
}
|
||
|
|
|
||
|
|
convert.To(envMap, conf)
|
||
|
|
}
|