cloud-game/pkg/config/loader.go
2023-11-26 22:39:46 +03:00

163 lines
3.6 KiB
Go

package config
import (
"bytes"
"embed"
"os"
"path/filepath"
"strings"
"github.com/knadh/koanf/maps"
"github.com/knadh/koanf/v2"
"gopkg.in/yaml.v3"
)
const EnvPrefix = "CLOUD_GAME_"
var (
//go:embed config.yaml
conf embed.FS
)
type Kv = map[string]any
type Bytes []byte
func (b *Bytes) ReadBytes() ([]byte, error) { return *b, nil }
func (b *Bytes) Read() (Kv, error) { return nil, nil }
type File string
func (f *File) ReadBytes() ([]byte, error) { return os.ReadFile(string(*f)) }
func (f *File) Read() (Kv, error) { return nil, nil }
type YAML struct{}
func (p *YAML) Marshal(Kv) ([]byte, error) { return nil, nil }
func (p *YAML) Unmarshal(b []byte) (Kv, error) {
var out Kv
klw := keysToLower(b)
decoder := yaml.NewDecoder(bytes.NewReader(klw))
if err := decoder.Decode(&out); err != nil {
return nil, err
}
return out, nil
}
// keysToLower iterates YAML bytes and tries to lower the keys.
// Used for merging with environment vars which are lowered as well.
func keysToLower(in []byte) []byte {
l, r, ignore := 0, 0, false
for i, b := range in {
switch b {
case '#': // skip comments
ignore = true
case ':': // lower left chunk before the next : symbol
if ignore {
continue
}
r = i
ignore = true
for j := l; j <= r; j++ {
c := in[j]
// we skip the line with the first explicit " string symbol
if c == '"' {
break
}
if 'A' <= c && c <= 'Z' {
in[j] += 'a' - 'A'
}
}
case '\n':
l = i
ignore = false
}
}
return in
}
type Env string
func (e *Env) ReadBytes() ([]byte, error) { return nil, nil }
func (e *Env) Read() (Kv, error) {
var keys []string
for _, k := range os.Environ() {
if strings.HasPrefix(k, string(*e)) {
keys = append(keys, k)
}
}
mp := make(Kv)
for _, k := range keys {
parts := strings.SplitN(k, "=", 2)
if parts == nil {
continue
}
n := strings.ToLower(strings.TrimPrefix(parts[0], string(*e)))
if n == "" {
continue
}
// convert VAR_VAR to VAR.VAR or if we need to preserve _
// i.e. VAR_VAR__KEY_HAS_SLASHES to VAR.VAR.KEY_HAS_SLASHES
// with the result: VAR: { VAR: { KEY_HAS_SLASHES: '' } } }
x := strings.Index(n, "__")
var key string
if x == -1 {
key = strings.Replace(n, "_", ".", -1)
} else {
key = strings.Replace(n[:x+1], "_", ".", -1) + n[x+2:]
}
if len(parts) > 1 {
mp[key] = parts[1]
}
}
return maps.Unflatten(mp, "."), nil
}
// LoadConfig loads a configuration file into the given struct.
// The path param specifies a custom path to the configuration file.
// Reads and puts environment variables with the prefix CLOUD_GAME_.
func LoadConfig(config any, path string) (loaded []string, err error) {
dirs := []string{".", "configs", "../../../configs"}
if path != "" {
dirs = append([]string{path}, dirs...)
}
homeDir := ""
if home, err := os.UserHomeDir(); err == nil {
homeDir = home + "/.cr"
dirs = append(dirs, homeDir)
}
k := koanf.New("_") // move to global scope if configs become dynamic
defer k.Delete("")
data, err := conf.ReadFile("config.yaml")
if err != nil {
return nil, err
}
conf := Bytes(data)
if err := k.Load(&conf, &YAML{}); err != nil {
return nil, err
}
loaded = append(loaded, "default")
for _, dir := range dirs {
path := filepath.Join(filepath.Clean(dir), "config.yaml")
f := File(path)
if _, err := os.Stat(string(f)); !os.IsNotExist(err) {
if err := k.Load(&f, &YAML{}); err != nil {
return loaded, err
}
loaded = append(loaded, path)
}
}
env := Env(EnvPrefix)
if err := k.Load(&env, nil); err != nil {
return loaded, err
}
if err := k.Unmarshal("", config); err != nil {
return loaded, err
}
return loaded, nil
}