♲ Refactor configuration to use viper with context propagation
- Replace global ConfigPath and Overrides with viper-based configuration - Add viper.New() to create configurable viper instances - Store viper and unmarshaled Config struct in context for testability - Add RetrieveFromContext and AddToContext helper functions - Update files.Append to accept context and retrieve config from it - Update formatters.Preferred and formatters.New to accept context - Add PersistentPreRunE in CLI to create and configure viper instance - Support -c flag for custom config file path - Support -v flag for config value overrides - Update all test files to create viper and add to context - Remove unused config types and load functions - Add viper as dependency with automatic env var support (MYLOG_*)
This commit is contained in:
parent
d34363b8c0
commit
9f05f933dd
21 changed files with 338 additions and 360 deletions
|
|
@ -4,8 +4,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
fp "path/filepath"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
const ConfigStr = `# Configuration for my-log
|
||||
|
|
@ -38,15 +36,13 @@ pretty_print = false
|
|||
|
||||
`
|
||||
|
||||
func DefaultPath() string {
|
||||
conf, _ := os.UserConfigDir()
|
||||
return fp.Join(conf, "my-log", "config.toml")
|
||||
}
|
||||
|
||||
func DefaultStr() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
inDir := fp.Join(home, "my-log")
|
||||
return fmt.Sprintf(ConfigStr, inDir)
|
||||
}
|
||||
|
||||
func DefaultConfig() (Config, error) {
|
||||
s := DefaultStr()
|
||||
c := Config{}
|
||||
_, err := toml.Decode(s, &c)
|
||||
return c, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
fp "path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/nalgeon/be"
|
||||
)
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
home, _ := os.UserHomeDir()
|
||||
inDir := fp.Join(home, "my-log")
|
||||
c, err := DefaultConfig()
|
||||
be.Err(t, err, nil)
|
||||
be.Equal(t, c.Input.Path, inDir)
|
||||
}
|
||||
119
config/load.go
119
config/load.go
|
|
@ -1,98 +1,53 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
fp "path/filepath"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"codeberg.org/danjones000/my-log/tools"
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/caarlos0/env/v10"
|
||||
mapst "github.com/go-viper/mapstructure/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var ConfigPath string
|
||||
var Overrides = map[string]string{}
|
||||
type confKeyType uint8
|
||||
|
||||
func init() {
|
||||
conf, _ := os.UserConfigDir()
|
||||
ConfigPath = fp.Join(conf, "my-log", "config.toml")
|
||||
}
|
||||
const (
|
||||
_ confKeyType = iota
|
||||
viperKey
|
||||
)
|
||||
|
||||
func Load() (Config, error) {
|
||||
c, _ := DefaultConfig()
|
||||
_, err := os.Stat(ConfigPath)
|
||||
if !os.IsNotExist(err) {
|
||||
_, err = toml.DecodeFile(ConfigPath, &c)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
}
|
||||
env.Parse(&c)
|
||||
c.Outputs["stdout"] = loadStdout(c.Outputs["stdout"])
|
||||
c.Formatters["json"] = loadJsonFormat(c.Formatters["json"])
|
||||
|
||||
l := ""
|
||||
for k, v := range Overrides {
|
||||
val := tools.ParseString(v)
|
||||
if val == nil {
|
||||
continue
|
||||
}
|
||||
if _, isJson := val.(json.RawMessage); isJson {
|
||||
continue
|
||||
}
|
||||
valout := fmt.Sprintf("%v", val)
|
||||
if vals, isString := val.(string); isString {
|
||||
valout = fmt.Sprintf(`"%s"`, vals)
|
||||
}
|
||||
if valt, isTime := val.(time.Time); isTime {
|
||||
valout = valt.Format(time.RFC3339)
|
||||
}
|
||||
l = l + "\n" + fmt.Sprintf("%s = %s", k, valout)
|
||||
}
|
||||
_, err = toml.Decode(l, &c)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func loadStdout(stdout Output) Output {
|
||||
st := stdoutEnabled{stdout.Enabled}
|
||||
env.Parse(&st)
|
||||
stdout.Enabled = st.Enabled
|
||||
var std Stdout
|
||||
mapst.Decode(stdout.Config, &std)
|
||||
env.Parse(&std)
|
||||
mapst.Decode(std, &stdout.Config)
|
||||
return stdout
|
||||
}
|
||||
|
||||
func (oo Outputs) Stdout() (s Stdout, enabled bool) {
|
||||
o, ok := oo["stdout"]
|
||||
func RetrieveFromContext(ctx context.Context) (*viper.Viper, Config) {
|
||||
v, ok := ctx.Value(viperKey).(*viper.Viper)
|
||||
if !ok {
|
||||
return s, false
|
||||
panic("config not found in context")
|
||||
}
|
||||
var c Config
|
||||
if err := v.Unmarshal(&c); err != nil {
|
||||
panic(fmt.Errorf("failed to unmarshal config: %w", err))
|
||||
}
|
||||
return v, c
|
||||
}
|
||||
|
||||
func AddToContext(ctx context.Context, v *viper.Viper) context.Context {
|
||||
return context.WithValue(ctx, viperKey, v)
|
||||
}
|
||||
|
||||
func New(ctx context.Context) (context.Context, *viper.Viper, error) {
|
||||
v := viper.New()
|
||||
v.SetConfigType("toml")
|
||||
|
||||
if err := v.ReadConfig(bytes.NewBufferString(DefaultStr())); err != nil {
|
||||
return ctx, nil, err
|
||||
}
|
||||
|
||||
enabled = o.Enabled
|
||||
mapst.Decode(o.Config, &s)
|
||||
v.SetConfigFile(DefaultPath())
|
||||
v.SetEnvPrefix("MYLOG")
|
||||
v.AutomaticEnv()
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func loadJsonFormat(c map[string]any) map[string]any {
|
||||
jf := JsonFormat{}
|
||||
mapst.Decode(c, &jf)
|
||||
env.Parse(&jf)
|
||||
mapst.Decode(jf, &c)
|
||||
return c
|
||||
}
|
||||
|
||||
func (ff Formatters) Json() (jf JsonFormat) {
|
||||
o, ok := ff["json"]
|
||||
if !ok {
|
||||
return
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return ctx, nil, err
|
||||
}
|
||||
|
||||
mapst.Decode(o, &jf)
|
||||
return
|
||||
return AddToContext(ctx, v), v, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,93 +1,51 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/nalgeon/be"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
f, _ := os.CreateTemp("", "test")
|
||||
ConfigPath = f.Name()
|
||||
defer f.Close()
|
||||
fmt.Fprint(f, `[input]
|
||||
func TestNew(t *testing.T) {
|
||||
_, v, err := New(t.Context())
|
||||
be.Err(t, err, nil)
|
||||
be.True(t, v != nil)
|
||||
}
|
||||
|
||||
func TestNewWithEnvOverrides(t *testing.T) {
|
||||
os.Setenv("MYLOG_INPUT_PATH", "/test/path")
|
||||
defer os.Unsetenv("MYLOG_INPUT_PATH")
|
||||
|
||||
_, v, err := New(t.Context())
|
||||
be.Err(t, err, nil)
|
||||
be.Equal(t, v.GetString("input.path"), "/test/path")
|
||||
}
|
||||
|
||||
func TestNewWithConfigFile(t *testing.T) {
|
||||
dir := t.ArtifactDir()
|
||||
f, _ := os.CreateTemp(dir, "test*.toml")
|
||||
defer os.Remove(f.Name())
|
||||
f.WriteString(`[input]
|
||||
path = "/file/path"
|
||||
ext = "log"`)
|
||||
c, err := Load()
|
||||
f.Close()
|
||||
|
||||
_, v, err := New(t.Context())
|
||||
be.Err(t, err, nil)
|
||||
be.Equal(t, c.Input.Ext, "log")
|
||||
}
|
||||
|
||||
func TestLoadBadFile(t *testing.T) {
|
||||
f, _ := os.CreateTemp("", "test")
|
||||
ConfigPath = f.Name()
|
||||
defer f.Close()
|
||||
fmt.Fprint(f, `{"not":"toml"}`)
|
||||
_, err := Load()
|
||||
be.Err(t, err)
|
||||
}
|
||||
|
||||
func TestLoadIgnoreMissingFile(t *testing.T) {
|
||||
def, _ := DefaultConfig()
|
||||
ConfigPath = "/not/a/real/file"
|
||||
c, err := Load()
|
||||
v.SetConfigFile(f.Name())
|
||||
v.SetConfigType("toml")
|
||||
err = v.ReadInConfig()
|
||||
be.Err(t, err, nil)
|
||||
be.Equal(t, c, def)
|
||||
be.Equal(t, v.GetString("input.path"), "/file/path")
|
||||
be.Equal(t, v.GetString("input.ext"), "log")
|
||||
}
|
||||
|
||||
func TestOverride(t *testing.T) {
|
||||
Overrides = map[string]string{
|
||||
"input.path": "/path/to/it",
|
||||
"input.ext": "~",
|
||||
}
|
||||
c, err := Load()
|
||||
be.Err(t, err, nil)
|
||||
be.Equal(t, c.Input.Path, Overrides["input.path"])
|
||||
be.Equal(t, c.Input.Ext, "txt")
|
||||
}
|
||||
|
||||
func TestOverrideJson(t *testing.T) {
|
||||
Overrides = map[string]string{"input.ext": `{"a":"b"}`}
|
||||
c, err := Load()
|
||||
be.Err(t, err, nil)
|
||||
be.Equal(t, c.Input.Ext, "txt")
|
||||
}
|
||||
|
||||
func TestTimeParse(t *testing.T) {
|
||||
Overrides = map[string]string{"input.ext": "now"}
|
||||
c, err := Load()
|
||||
be.Err(t, err, "incompatible types: TOML value has type time.Time; destination has type string")
|
||||
be.Equal(t, c.Input.Ext, "txt")
|
||||
}
|
||||
|
||||
func TestStdoutMissing(t *testing.T) {
|
||||
var oo Outputs = map[string]Output{}
|
||||
std, en := oo.Stdout()
|
||||
be.True(t, !en)
|
||||
be.Equal(t, std, Stdout{})
|
||||
}
|
||||
|
||||
func TestStdoutLoad(t *testing.T) {
|
||||
os.Setenv("LOG_STDOUT_FORMAT", "json")
|
||||
defer os.Unsetenv("LOG_STDOUT_FORMAT")
|
||||
os.Setenv("LOG_STDOUT_ENABLED", "true")
|
||||
defer os.Unsetenv("LOG_STDOUT_ENABLED")
|
||||
c, _ := Load()
|
||||
std, en := c.Outputs.Stdout()
|
||||
be.True(t, en)
|
||||
be.Equal(t, std.Format, "json")
|
||||
}
|
||||
|
||||
func TestFormatJson(t *testing.T) {
|
||||
ff := Formatters{
|
||||
"json": map[string]any{"pretty_print": true},
|
||||
}
|
||||
|
||||
js := ff.Json()
|
||||
be.True(t, js.PrettyPrint)
|
||||
|
||||
ff = Formatters{}
|
||||
js = ff.Json()
|
||||
be.True(t, !js.PrettyPrint)
|
||||
func TestRetrieveFromContext(t *testing.T) {
|
||||
v := viper.New()
|
||||
ctx := AddToContext(t.Context(), v)
|
||||
result, _ := RetrieveFromContext(ctx)
|
||||
be.True(t, v == result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ package config
|
|||
|
||||
type Config struct {
|
||||
Input Input
|
||||
Outputs Outputs `toml:"output"`
|
||||
Outputs Outputs `mapstructure:"output"`
|
||||
Formatters Formatters
|
||||
}
|
||||
|
||||
type Input struct {
|
||||
Path string `env:"LOG_PATH"`
|
||||
Recurse bool `env:"LOG_RECURSE"`
|
||||
Ext string `env:"LOG_EXT"`
|
||||
DotFolder bool `env:"LOG_DOT_FOLDER"`
|
||||
Path string
|
||||
Recurse bool
|
||||
Ext string
|
||||
DotFolder bool `mapstructure:"dotFolder"`
|
||||
}
|
||||
|
||||
type Outputs map[string]Output
|
||||
|
|
@ -20,16 +20,4 @@ type Output struct {
|
|||
Config map[string]any
|
||||
}
|
||||
|
||||
type Stdout struct {
|
||||
Format string `env:"LOG_STDOUT_FORMAT" mapstructure:"format"`
|
||||
}
|
||||
|
||||
type stdoutEnabled struct {
|
||||
Enabled bool `env:"LOG_STDOUT_ENABLED"`
|
||||
}
|
||||
|
||||
type Formatters map[string]map[string]any
|
||||
|
||||
type JsonFormat struct {
|
||||
PrettyPrint bool `env:"LOG_JSON_PRETTY_PRINT" mapstructure:"pretty_print"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue