♲ 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:
Dan Jones 2026-03-08 22:59:33 -05:00
commit 9f05f933dd
21 changed files with 338 additions and 360 deletions

View file

@ -32,23 +32,24 @@ var ConfigCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) (err error) {
print, _ := cmd.Flags().GetBool("print")
if print {
fmt.Fprintln(cmd.OutOrStdout(), config.ConfigPath)
fmt.Fprintln(cmd.OutOrStdout(), config.DefaultPath())
return nil
}
force, _ := cmd.Flags().GetBool("force")
configPath := config.DefaultPath()
if !force {
_, err = os.Stat(config.ConfigPath)
_, err = os.Stat(configPath)
if !os.IsNotExist(err) {
return fmt.Errorf("%s already exists. Use -f to overwrite", config.ConfigPath)
return fmt.Errorf("%s already exists. Use -f to overwrite", configPath)
}
}
dir := fp.Dir(config.ConfigPath)
dir := fp.Dir(configPath)
err = os.MkdirAll(dir, 0755)
if err != nil {
return
}
f, err := os.Create(config.ConfigPath)
f, err := os.Create(configPath)
if err != nil {
return
}

View file

@ -42,8 +42,9 @@ var DropCmd = &cobra.Command{
Args: cobra.ExactArgs(2),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
v, _ := config.RetrieveFromContext(cmd.Context())
if outJson {
config.Overrides["output.stdout.config.format"] = "json"
v.Set("output.stdout.config.format", "json")
}
log := args[0]
@ -60,12 +61,12 @@ var DropCmd = &cobra.Command{
}
e := models.Entry{Title: title, Date: d.Time(), Fields: *ms}
l := models.Log{Name: log, Entries: []models.Entry{e}}
err := files.Append(l)
err := files.Append(cmd.Context(), l)
if err != nil {
return err
}
form, err := formatters.Preferred()
form, err := formatters.Preferred(cmd.Context())
if err != nil {
return err
}

View file

@ -1,5 +1,5 @@
/*
Copyright © 2024 Dan Jones <danjones@goodevilgenius.org>
Copyright © 2026 Dan Jones <danjones@goodevilgenius.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
@ -17,6 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
package cli
import (
"context"
"os"
"codeberg.org/danjones000/my-log/config"
@ -26,16 +27,41 @@ import (
var RootCmd = &cobra.Command{
Use: "my-log",
Short: "A brief description of your application",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
ctx, v, err := config.New(cmd.Context())
if err != nil {
return err
}
func Execute() {
err := RootCmd.Execute()
if configPath != "" {
v.SetConfigFile(configPath)
err := v.ReadInConfig()
if err != nil {
return err
}
}
for k, val := range configValues {
v.Set(k, val)
}
cmd.SetContext(ctx)
return nil
},
}
func Execute(ctx context.Context) {
err := RootCmd.ExecuteContext(ctx)
if err != nil {
os.Exit(1)
}
}
var configPath string
var configValues map[string]string
func init() {
RootCmd.PersistentFlags().StringVarP(&config.ConfigPath, "config", "c", config.ConfigPath, "config file")
RootCmd.PersistentFlags().StringToStringVarP(&config.Overrides, "config-value", "v", config.Overrides, "Override config values. Use dot syntax to specify key. E.g. -v output.stdout.config.format=json")
RootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", config.DefaultPath(), "config file")
RootCmd.PersistentFlags().StringToStringVarP(&configValues, "config-value", "v", nil, "Override config values. Use dot syntax to specify key. E.g. -v output.stdout.config.format=json")
}

View file

@ -1,5 +1,5 @@
/*
Copyright © 2024 Dan Jones <danjones@goodevilgenius.org>
Copyright © 2026 Dan Jones <danjones@goodevilgenius.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
@ -16,8 +16,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package main
import "codeberg.org/danjones000/my-log/cli"
import (
"context"
"codeberg.org/danjones000/my-log/cli"
)
func main() {
cli.Execute()
cli.Execute(context.Background())
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}
enabled = o.Enabled
mapst.Decode(o.Config, &s)
return
func AddToContext(ctx context.Context, v *viper.Viper) context.Context {
return context.WithValue(ctx, viperKey, v)
}
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 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
}
func (ff Formatters) Json() (jf JsonFormat) {
o, ok := ff["json"]
if !ok {
return
v.SetConfigFile(DefaultPath())
v.SetEnvPrefix("MYLOG")
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
if err := v.ReadInConfig(); err != nil {
return ctx, nil, err
}
mapst.Decode(o, &jf)
return
return AddToContext(ctx, v), v, nil
}

View file

@ -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)
}

View file

@ -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"`
}

View file

@ -1,6 +1,7 @@
package files
import (
"context"
"fmt"
"io"
"os"
@ -11,11 +12,8 @@ import (
"codeberg.org/danjones000/my-log/models"
)
func Append(l models.Log) error {
conf, err := config.Load()
if err != nil {
return err
}
func Append(ctx context.Context, l models.Log) error {
_, conf := config.RetrieveFromContext(ctx)
filename := l.Name
if conf.Input.DotFolder {
@ -27,7 +25,7 @@ func Append(l models.Log) error {
}
path := fp.Join(conf.Input.Path, filename)
dir := fp.Dir(path)
err = os.MkdirAll(dir, 0750)
err := os.MkdirAll(dir, 0750)
if err != nil {
return err
}

View file

@ -1,6 +1,7 @@
package files
import (
"context"
"fmt"
"os"
"strings"
@ -10,6 +11,7 @@ import (
"codeberg.org/danjones000/my-log/config"
"codeberg.org/danjones000/my-log/models"
"github.com/nalgeon/be"
"github.com/spf13/viper"
)
func TestAppend(tt *testing.T) {
@ -24,26 +26,25 @@ func TestAppend(tt *testing.T) {
})
tt.Run("failure", func(t *testing.T) {
t.Run("badEntry", appendTestBadEntry)
t.Run("load-err", appendTestConfLoadErr)
t.Run("mkdir-err", appendTestMkdirErr)
t.Run("append-log-err", appendTestOpenErr)
})
}
func setupAppendTest(t *testing.T) string {
func setupAppendTest(t *testing.T) (string, context.Context) {
t.Helper()
dir := t.ArtifactDir()
config.Overrides["input.path"] = dir
config.Overrides["input.ext"] = "log"
t.Cleanup(func() {
delete(config.Overrides, "input.path")
delete(config.Overrides, "input.ext")
})
return dir
v := viper.New()
v.SetConfigType("toml")
v.Set("input.path", dir)
v.Set("input.ext", "log")
v.Set("input.dotFolder", false)
ctx := config.AddToContext(t.Context(), v)
return dir, ctx
}
func appendTestSingle(t *testing.T) {
dir := setupAppendTest(t)
dir, ctx := setupAppendTest(t)
when := time.Now().Local()
e := models.Entry{
Title: "Jimmy",
@ -57,7 +58,7 @@ func appendTestSingle(t *testing.T) {
Name: "test",
Entries: []models.Entry{e},
}
err := Append(l)
err := Append(ctx, l)
be.Err(t, err, nil)
by, err := os.ReadFile(dir + "/test.log")
be.Err(t, err, nil)
@ -68,7 +69,7 @@ func appendTestSingle(t *testing.T) {
}
func appendTestTwoEntries(t *testing.T) {
dir := setupAppendTest(t)
dir, ctx := setupAppendTest(t)
when := time.Now().Local()
whens := when.Format(models.DateFormat)
e := []models.Entry{
@ -79,7 +80,7 @@ func appendTestTwoEntries(t *testing.T) {
Name: "test",
Entries: e,
}
err := Append(l)
err := Append(ctx, l)
be.Err(t, err, nil)
by, _ := os.ReadFile(dir + "/test.log")
st := string(by)
@ -88,7 +89,7 @@ func appendTestTwoEntries(t *testing.T) {
}
func appendTestAddNewLine(t *testing.T) {
dir := setupAppendTest(t)
dir, ctx := setupAppendTest(t)
os.WriteFile(dir+"/test.log", []byte("foo"), 0644)
when := time.Now().Local()
whens := when.Format(models.DateFormat)
@ -99,7 +100,7 @@ func appendTestAddNewLine(t *testing.T) {
Name: "test",
Entries: e,
}
err := Append(l)
err := Append(ctx, l)
be.Err(t, err, nil)
by, _ := os.ReadFile(dir + "/test.log")
exp := fmt.Sprintf("foo\n@begin %s - one\n@id jimmy @end\n", whens)
@ -107,7 +108,7 @@ func appendTestAddNewLine(t *testing.T) {
}
func appendTestDontAddNewLine(t *testing.T) {
dir := setupAppendTest(t)
dir, ctx := setupAppendTest(t)
os.WriteFile(dir+"/test.log", []byte("foo\n"), 0644)
when := time.Now().Local()
whens := when.Format(models.DateFormat)
@ -118,7 +119,7 @@ func appendTestDontAddNewLine(t *testing.T) {
Name: "test",
Entries: e,
}
err := Append(l)
err := Append(ctx, l)
be.Err(t, err, nil)
by, _ := os.ReadFile(dir + "/test.log")
exp := fmt.Sprintf("foo\n@begin %s - one\n@id jimmy @end\n", whens)
@ -126,7 +127,7 @@ func appendTestDontAddNewLine(t *testing.T) {
}
func appendTestBadEntry(t *testing.T) {
dir := setupAppendTest(t)
dir, ctx := setupAppendTest(t)
e := models.Entry{
Title: "Jimmy",
}
@ -134,19 +135,21 @@ func appendTestBadEntry(t *testing.T) {
Name: "test",
Entries: []models.Entry{e},
}
err := Append(l)
err := Append(ctx, l)
be.Err(t, err, nil)
by, _ := os.ReadFile(dir + "/test.log")
be.Equal(t, by, []byte{})
}
func appendTestDotFolder(t *testing.T) {
config.Overrides["input.dotFolder"] = "true"
t.Cleanup(func() {
delete(config.Overrides, "input.dotFolder")
})
dir := t.ArtifactDir()
v := viper.New()
v.SetConfigType("toml")
v.Set("input.path", dir)
v.Set("input.ext", "log")
v.Set("input.dotFolder", true)
ctx := config.AddToContext(t.Context(), v)
dir := setupAppendTest(t)
e := models.Entry{
Title: "something",
Date: time.Now(),
@ -155,7 +158,7 @@ func appendTestDotFolder(t *testing.T) {
Name: "sub.test",
Entries: []models.Entry{e},
}
err := Append(l)
err := Append(ctx, l)
be.Err(t, err, nil)
by, err := os.ReadFile(dir + "/sub/test.log")
be.Err(t, err, nil)
@ -164,12 +167,7 @@ func appendTestDotFolder(t *testing.T) {
}
func appendTestDotFolderNo(t *testing.T) {
config.Overrides["input.dotFolder"] = "false"
t.Cleanup(func() {
delete(config.Overrides, "input.dotFolder")
})
dir := setupAppendTest(t)
dir, ctx := setupAppendTest(t)
e := models.Entry{
Title: "another",
Date: time.Now(),
@ -178,7 +176,7 @@ func appendTestDotFolderNo(t *testing.T) {
Name: "sub.test",
Entries: []models.Entry{e},
}
err := Append(l)
err := Append(ctx, l)
be.Err(t, err, nil)
by, err := os.ReadFile(dir + "/sub.test.log")
be.Err(t, err, nil)
@ -187,11 +185,13 @@ func appendTestDotFolderNo(t *testing.T) {
}
func appendTestNoExt(t *testing.T) {
dir := setupAppendTest(t)
config.Overrides["input.ext"] = ""
t.Cleanup(func() {
config.Overrides["input.ext"] = "log"
})
dir := t.ArtifactDir()
v := viper.New()
v.SetConfigType("toml")
v.Set("input.path", dir)
v.Set("input.ext", "")
v.Set("input.dotFolder", false)
ctx := config.AddToContext(t.Context(), v)
e := models.Entry{
Title: "baz",
@ -201,7 +201,7 @@ func appendTestNoExt(t *testing.T) {
Name: "foobar",
Entries: []models.Entry{e},
}
err := Append(l)
err := Append(ctx, l)
be.Err(t, err, nil)
by, err := os.ReadFile(dir + "/foobar")
be.Err(t, err, nil)
@ -209,33 +209,20 @@ func appendTestNoExt(t *testing.T) {
be.True(t, strings.Contains(st, fmt.Sprintf("@begin %s - %s", e.Date.Format(models.DateFormat), e.Title)))
}
func appendTestConfLoadErr(t *testing.T) {
dir := t.ArtifactDir()
currConf := config.ConfigPath
tmp, _ := os.CreateTemp(dir, "app-conf-*.toml")
fname := tmp.Name()
t.Cleanup(func() { tmp.Close() })
t.Cleanup(func() { os.Remove(fname) })
fmt.Fprintln(tmp, `{"not":"toml"}`)
config.ConfigPath = fname
t.Cleanup(func() { config.ConfigPath = currConf })
err := Append(models.Log{})
be.Err(t, err, "toml")
}
func appendTestMkdirErr(t *testing.T) {
// Don't run this test as root
config.Overrides["input.path"] = "/var/my-logs-test"
t.Cleanup(func() { delete(config.Overrides, "input.path") })
v := viper.New()
v.SetConfigType("toml")
v.Set("input.path", "/var/my-logs-test")
v.Set("input.ext", "log")
v.Set("input.dotFolder", false)
ctx := config.AddToContext(t.Context(), v)
err := Append(models.Log{})
err := Append(ctx, models.Log{})
be.Err(t, err, "permission denied")
}
func appendTestOpenErr(t *testing.T) {
dir := setupAppendTest(t)
dir, ctx := setupAppendTest(t)
l := models.Log{
Name: "test-open-err",
}
@ -244,6 +231,6 @@ func appendTestOpenErr(t *testing.T) {
f, _ := os.Create(fname)
f.Close()
os.Chmod(fname, 0400)
err := Append(l)
err := Append(ctx, l)
be.Err(t, err, "permission denied")
}

View file

@ -5,12 +5,17 @@ import (
"encoding/json"
"time"
"codeberg.org/danjones000/my-log/config"
"codeberg.org/danjones000/my-log/models"
)
func newJson(ff config.Formatters) (Formatter, error) {
return &Json{ff.Json().PrettyPrint}, nil
func newJson(ff map[string]any) (Formatter, error) {
prettyPrint := false
if jf, ok := ff["json"].(map[string]any); ok {
if pp, ok := jf["pretty_print"].(bool); ok {
prettyPrint = pp
}
}
return &Json{prettyPrint}, nil
}
type Json struct {

View file

@ -1,22 +1,35 @@
package formatters
import (
"context"
"fmt"
"testing"
"time"
"codeberg.org/danjones000/my-log/config"
"codeberg.org/danjones000/my-log/internal/testutil/bep"
"codeberg.org/danjones000/my-log/models"
"github.com/nalgeon/be"
"github.com/spf13/viper"
)
func setupJsonTestContext(t *testing.T) context.Context {
t.Helper()
v := viper.New()
v.SetConfigType("toml")
v.Set("formatters.json.pretty_print", false)
return config.AddToContext(t.Context(), v)
}
func TestJsonName(t *testing.T) {
f, _ := New("json")
ctx := setupJsonTestContext(t)
f, _ := New(ctx, "json")
be.Equal(t, f.Name(), "json")
}
func TestJsonMeta(t *testing.T) {
f, _ := New("json")
ctx := setupJsonTestContext(t)
f, _ := New(ctx, "json")
m := models.Meta{Key: "foo", Value: 42}
exp := `{"foo":42}`
o, err := f.Meta(m)
@ -25,8 +38,9 @@ func TestJsonMeta(t *testing.T) {
}
func TestJsonEntry(t *testing.T) {
ctx := setupJsonTestContext(t)
when := time.Now()
f, _ := New("json")
f, _ := New(ctx, "json")
m := models.Meta{Key: "foo", Value: 42}
e := models.Entry{
Title: "Homer",
@ -40,8 +54,9 @@ func TestJsonEntry(t *testing.T) {
}
func TestJsonLog(t *testing.T) {
ctx := setupJsonTestContext(t)
when := time.Now()
f, _ := New("json")
f, _ := New(ctx, "json")
m := models.Meta{Key: "foo", Value: 42}
e := models.Entry{
Title: "Homer",
@ -56,7 +71,8 @@ func TestJsonLog(t *testing.T) {
}
func TestJsonNoLogs(t *testing.T) {
f, _ := New("json")
ctx := setupJsonTestContext(t)
f, _ := New(ctx, "json")
o, err := f.Logs([]models.Log{})
var exp []byte
be.Err(t, err, nil)
@ -64,7 +80,8 @@ func TestJsonNoLogs(t *testing.T) {
}
func TestJsonErr(t *testing.T) {
f, _ := New("json")
ctx := setupJsonTestContext(t)
f, _ := New(ctx, "json")
o, err := f.Meta(models.Meta{Key: "foo", Value: make(chan bool)})
var exp []byte
be.Err(t, err)

View file

@ -1,12 +1,13 @@
package formatters
import (
"context"
"errors"
"codeberg.org/danjones000/my-log/config"
)
type formatMaker func(config.Formatters) (Formatter, error)
type formatMaker func(config map[string]any) (Formatter, error)
var formatterMap = map[string]formatMaker{
"plain": newPlain,
@ -14,23 +15,22 @@ var formatterMap = map[string]formatMaker{
"zero": newNull,
}
func Preferred() (f Formatter, err error) {
conf, err := config.Load()
if err != nil {
return
}
std, _ := conf.Outputs.Stdout()
return New(std.Format)
func Preferred(ctx context.Context) (f Formatter, err error) {
v, _ := config.RetrieveFromContext(ctx)
format := v.GetString("output.stdout.config.format")
return New(ctx, format)
}
func New(kind string) (f Formatter, err error) {
conf, err := config.Load()
if err != nil {
return
}
func New(ctx context.Context, kind string) (f Formatter, err error) {
_, c := config.RetrieveFromContext(ctx)
conf := c.Formatters
if make, ok := formatterMap[kind]; ok {
return make(conf.Formatters)
var formatterConf map[string]any
if cf, ok := conf[kind]; ok {
formatterConf = cf
}
return make(formatterConf)
}
return nil, errors.New("unimplemented")

View file

@ -1,13 +1,13 @@
package formatters
import (
"fmt"
"os"
"context"
"slices"
"testing"
"codeberg.org/danjones000/my-log/config"
"github.com/nalgeon/be"
"github.com/spf13/viper"
)
func TestKinds(t *testing.T) {
@ -17,33 +17,25 @@ func TestKinds(t *testing.T) {
}
}
func setupNewTest(t *testing.T) context.Context {
t.Helper()
v := viper.New()
v.SetConfigType("toml")
v.Set("output.stdout.config.format", "plain")
v.Set("formatters.json.pretty_print", false)
return config.AddToContext(t.Context(), v)
}
func TestNewUnsupported(t *testing.T) {
f, err := New("nope")
ctx := setupNewTest(t)
f, err := New(ctx, "nope")
be.Equal(t, f, nil)
be.Err(t, err)
}
func TestNewCantGetConfig(t *testing.T) {
f, _ := os.CreateTemp("", "test")
oldConf := config.ConfigPath
config.ConfigPath = f.Name()
defer f.Close()
defer func() {
config.ConfigPath = oldConf
}()
fmt.Fprint(f, `{"not":"toml"}`)
form, err := New("plain")
be.Equal(t, form, nil)
be.Err(t, err)
form, err = Preferred()
be.Equal(t, form, nil)
be.Err(t, err)
}
func TestPreferred(t *testing.T) {
form, err := Preferred()
ctx := setupNewTest(t)
form, err := Preferred(ctx)
be.Err(t, err, nil)
be.True(t, form != nil)
}

View file

@ -1,11 +1,10 @@
package formatters
import (
"codeberg.org/danjones000/my-log/config"
"codeberg.org/danjones000/my-log/models"
)
func newNull(ff config.Formatters) (Formatter, error) {
func newNull(ff map[string]any) (Formatter, error) {
return &Null{}, nil
}

View file

@ -1,44 +1,59 @@
package formatters
import (
"context"
"testing"
"time"
"codeberg.org/danjones000/my-log/config"
"codeberg.org/danjones000/my-log/models"
"github.com/nalgeon/be"
"github.com/spf13/viper"
)
var empty []byte
func setupNullTestContext(t *testing.T) context.Context {
t.Helper()
v := viper.New()
v.SetConfigType("toml")
return config.AddToContext(t.Context(), v)
}
func TestNullName(t *testing.T) {
f, err := New("zero")
ctx := setupNullTestContext(t)
f, err := New(ctx, "zero")
be.Err(t, err, nil)
be.Equal(t, f.Name(), "zero")
}
func TestNullMeta(t *testing.T) {
f, _ := New("zero")
ctx := setupNullTestContext(t)
f, _ := New(ctx, "zero")
o, err := f.Meta(models.Meta{Key: "foo", Value: 42})
be.Err(t, err, nil)
be.Equal(t, o, empty)
}
func TestNullEntry(t *testing.T) {
f, _ := New("zero")
ctx := setupNullTestContext(t)
f, _ := New(ctx, "zero")
o, err := f.Entry(models.Entry{Title: "title", Date: time.Now()})
be.Err(t, err, nil)
be.Equal(t, o, empty)
}
func TestNullLog(t *testing.T) {
f, _ := New("zero")
ctx := setupNullTestContext(t)
f, _ := New(ctx, "zero")
o, err := f.Log(models.Log{Name: "jim", Entries: []models.Entry{{Title: "title", Date: time.Now()}}})
be.Err(t, err, nil)
be.Equal(t, o, empty)
}
func TestNullLogs(t *testing.T) {
f, _ := New("zero")
ctx := setupNullTestContext(t)
f, _ := New(ctx, "zero")
o, err := f.Logs([]models.Log{{Name: "jim", Entries: []models.Entry{{Title: "title", Date: time.Now()}}}})
be.Err(t, err, nil)
be.Equal(t, o, empty)

View file

@ -3,12 +3,11 @@ package formatters
import (
"bytes"
"codeberg.org/danjones000/my-log/config"
"codeberg.org/danjones000/my-log/models"
"codeberg.org/danjones000/my-log/tools"
)
func newPlain(ff config.Formatters) (Formatter, error) {
func newPlain(ff map[string]any) (Formatter, error) {
return &PlainText{}, nil
}

View file

@ -3,16 +3,27 @@ package formatters
import (
"bufio"
"bytes"
"context"
"fmt"
"testing"
"time"
"codeberg.org/danjones000/my-log/config"
"codeberg.org/danjones000/my-log/models"
"codeberg.org/danjones000/my-log/tools"
"github.com/nalgeon/be"
"github.com/spf13/viper"
)
func setupPlainTestContext(t *testing.T) context.Context {
t.Helper()
v := viper.New()
v.SetConfigType("toml")
return config.AddToContext(t.Context(), v)
}
func TestPlainLogs(t *testing.T) {
ctx := setupPlainTestContext(t)
m := []models.Meta{
{Key: "foo", Value: "bar"},
{Key: "baz", Value: 42},
@ -29,7 +40,7 @@ func TestPlainLogs(t *testing.T) {
l2 := models.Log{Name: "more-stuff", Entries: []models.Entry{e2}}
logs := []models.Log{l, l2}
f, err := New("plain")
f, err := New(ctx, "plain")
be.Err(t, err, nil)
out, err := f.Logs(logs)
@ -95,40 +106,46 @@ func TestPlainLogs(t *testing.T) {
}
func TestPlainName(t *testing.T) {
f, _ := New("plain")
ctx := setupPlainTestContext(t)
f, _ := New(ctx, "plain")
be.Equal(t, f.Name(), "plain")
}
func TestPlainLogNone(t *testing.T) {
f, _ := New("plain")
ctx := setupPlainTestContext(t)
f, _ := New(ctx, "plain")
out, err := f.Logs([]models.Log{})
be.Err(t, err, nil)
be.Equal(t, len(out), 0)
}
func TestPlainLogNoEntries(t *testing.T) {
f, _ := New("plain")
ctx := setupPlainTestContext(t)
f, _ := New(ctx, "plain")
out, err := f.Log(models.Log{Name: "foo"})
be.Err(t, err, nil)
be.Equal(t, len(out), 0)
}
func TestPlainMetaEmpty(t *testing.T) {
f, _ := New("plain")
ctx := setupPlainTestContext(t)
f, _ := New(ctx, "plain")
out, err := f.Meta(models.Meta{Key: "foo", Value: ""})
be.Err(t, err, nil)
be.Equal(t, len(out), 0)
}
func TestPlainMetaError(t *testing.T) {
f, _ := New("plain")
ctx := setupPlainTestContext(t)
f, _ := New(ctx, "plain")
out, err := f.Meta(models.Meta{Key: "foo", Value: make(chan bool)})
be.Err(t, err)
be.Equal(t, len(out), 0)
}
func TestPlainEntry(t *testing.T) {
f, _ := New("plain")
ctx := setupPlainTestContext(t)
f, _ := New(ctx, "plain")
now := time.Now()
out, err := f.Entry(models.Entry{
Title: "foo",

18
go.mod
View file

@ -3,25 +3,33 @@ module codeberg.org/danjones000/my-log
go 1.26.0
require (
github.com/BurntSushi/toml v1.3.2
github.com/caarlos0/env/v10 v10.0.0
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/google/uuid v1.6.0
github.com/markusmobius/go-dateparser v1.2.3
github.com/nalgeon/be v0.3.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.21.0
)
require (
github.com/elliotchance/pie/v2 v2.7.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/hablullah/go-hijri v1.0.2 // indirect
github.com/hablullah/go-juliandays v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tetratelabs/wazero v1.2.1 // indirect
github.com/wasilibs/go-re2 v1.3.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.28.0 // indirect
)

47
go.sum
View file

@ -1,14 +1,16 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/caarlos0/env/v10 v10.0.0 h1:yIHUBZGsyqCnpTkbjk8asUlx6RFhhEs+h7TOBdgdzXA=
github.com/caarlos0/env/v10 v10.0.0/go.mod h1:ZfulV76NvVPw3tm591U4SwL3Xx9ldzBP9aGxzeN7G18=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg=
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
@ -19,31 +21,58 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/markusmobius/go-dateparser v1.2.3 h1:TvrsIvr5uk+3v6poDjaicnAFJ5IgtFHgLiuMY2Eb7Nw=
github.com/markusmobius/go-dateparser v1.2.3/go.mod h1:cMwQRrBUQlK1UI5TIFHEcvpsMbkWrQLXuaPNMFzuYLk=
github.com/nalgeon/be v0.3.0 h1:QsPANqEtcOD5qT2S3KAtIkDBBn8SXUf/Lb5Bi/z4UqM=
github.com/nalgeon/be v0.3.0/go.mod h1:PMwMuBLopwKJkSHnr2qHyLcZYUTqNejN7A8RAqNWO3E=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 h1:ba9YlqfDGTTQ5aZ2fwOoQ1hf32QySyQkR6ODGDzHlnE=
golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=