feat: add support for toml in addition to json
This commit is contained in:
parent
c3d51b1cc2
commit
e72c317bbb
22 changed files with 455 additions and 291 deletions
|
@ -577,6 +577,7 @@ linters-settings:
|
||||||
# List of allowed modules.
|
# List of allowed modules.
|
||||||
# Default: []
|
# Default: []
|
||||||
modules: # List of allowed modules
|
modules: # List of allowed modules
|
||||||
|
- github.com/pelletier/go-toml/v2
|
||||||
- github.com/stretchr/testify
|
- github.com/stretchr/testify
|
||||||
# - gopkg.in/yaml.v2
|
# - gopkg.in/yaml.v2
|
||||||
# List of allowed module domains.
|
# List of allowed module domains.
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Add support for TOML configuration files
|
||||||
- Use stdlib for tests instead of convey
|
- Use stdlib for tests instead of convey
|
||||||
- Update golangci-lint config
|
- Update golangci-lint config
|
||||||
|
|
||||||
|
@ -12,5 +13,5 @@
|
||||||
- Added a Loadfiles function to load at once several config files
|
- Added a Loadfiles function to load at once several config files
|
||||||
|
|
||||||
## v0.1.0 (2020-03-17)
|
## v0.1.0 (2020-03-17)
|
||||||
- Initial varsion
|
|
||||||
|
|
||||||
|
- Initial varsion with support for JSON files
|
||||||
|
|
86
adapters.go
Normal file
86
adapters.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
package conf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type filetype int
|
||||||
|
|
||||||
|
const (
|
||||||
|
typeInvalid filetype = iota
|
||||||
|
typeJSON
|
||||||
|
typeTOML
|
||||||
|
)
|
||||||
|
|
||||||
|
func getType(filename string) filetype {
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(filename, ".json"):
|
||||||
|
return typeJSON
|
||||||
|
case strings.HasSuffix(filename, ".toml"):
|
||||||
|
return typeTOML
|
||||||
|
default:
|
||||||
|
return typeInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshal(ft filetype, data []byte, v interface{}) error {
|
||||||
|
switch ft {
|
||||||
|
case typeJSON:
|
||||||
|
return unmarshalJSON(data, v)
|
||||||
|
case typeTOML:
|
||||||
|
return unmarshalTOML(data, v)
|
||||||
|
default:
|
||||||
|
return ErrUnsupportedFileType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshal(ft filetype, v interface{}) ([]byte, error) {
|
||||||
|
switch ft {
|
||||||
|
case typeJSON:
|
||||||
|
return marshalJSON(v)
|
||||||
|
case typeTOML:
|
||||||
|
return marshalTOML(v)
|
||||||
|
default:
|
||||||
|
return nil, ErrUnsupportedFileType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalJSON(data []byte, v interface{}) error {
|
||||||
|
err := json.Unmarshal(data, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalJSON(v interface{}) ([]byte, error) {
|
||||||
|
data, err := json.MarshalIndent(v, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot generate config content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalTOML(data []byte, v interface{}) error {
|
||||||
|
err := toml.Unmarshal(data, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot parse config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalTOML(v interface{}) ([]byte, error) {
|
||||||
|
data, err := toml.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot generate config content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
29
config.go
29
config.go
|
@ -1,15 +1,25 @@
|
||||||
// Package conf defines utils to simplify configuration
|
// Package conf defines utils to simplify configuration management.
|
||||||
// management.
|
//
|
||||||
|
// It provides functions to load and save config files.
|
||||||
|
//
|
||||||
|
// Several formats are supported. The encoders/decoders are selected according
|
||||||
|
// to the extension of the paths passed to functions:
|
||||||
|
//
|
||||||
|
// - JSON: ".json"
|
||||||
|
// - TOML: ".toml"
|
||||||
package conf
|
package conf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrUnsupportedFileType is returned when the type of the config file is not
|
||||||
|
// supported.
|
||||||
|
var ErrUnsupportedFileType = errors.New("unsupported config type")
|
||||||
|
|
||||||
// LoadFile reads the file at path, parses its json content and fills the struct
|
// LoadFile reads the file at path, parses its json content and fills the struct
|
||||||
// with the content of the file.
|
// with the content of the file.
|
||||||
func LoadFile(path string, data interface{}) error {
|
func LoadFile(path string, data interface{}) error {
|
||||||
|
@ -21,7 +31,7 @@ func LoadFile(path string, data interface{}) error {
|
||||||
// If a path does not exist, it is ignored.
|
// If a path does not exist, it is ignored.
|
||||||
//
|
//
|
||||||
// It returns an error only if the content of a file is invalid, i.e. it
|
// It returns an error only if the content of a file is invalid, i.e. it
|
||||||
// cannot be unmarshaled by json.
|
// cannot be unmarshaled to the struct.
|
||||||
func LoadFiles(data interface{}, paths ...string) error {
|
func LoadFiles(data interface{}, paths ...string) error {
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
err := read(p, data)
|
err := read(p, data)
|
||||||
|
@ -78,18 +88,13 @@ func read(path string, data interface{}) error {
|
||||||
return fmt.Errorf("cannot read config file: %w", err)
|
return fmt.Errorf("cannot read config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(content, &data)
|
return unmarshal(getType(path), content, data)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cannot parse config file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func write(path string, data interface{}) error {
|
func write(path string, data interface{}) error {
|
||||||
content, err := json.MarshalIndent(data, "", " ")
|
content, err := marshal(getType(path), data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot generate config content: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(path, content, 0o600)
|
err = os.WriteFile(path, content, 0o600)
|
||||||
|
|
596
config_test.go
596
config_test.go
|
@ -11,6 +11,68 @@ import (
|
||||||
"code.bcarlin.xyz/go/conf"
|
"code.bcarlin.xyz/go/conf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestJSONFiles(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
runTestSuite(t, "json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTOMLFiles(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
runTestSuite(t, "toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnknownFiles(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("LoadFile", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := struct{}{}
|
||||||
|
|
||||||
|
err := conf.LoadFile("test_data/valid.unknown", &s)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.ErrorIs(t, err, conf.ErrUnsupportedFileType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SaveFile", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := struct{}{}
|
||||||
|
|
||||||
|
err := conf.SaveFile("test.unknown", &s)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.ErrorIs(t, err, conf.ErrUnsupportedFileType)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoFileExists(t, "test.unknown")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LoadAndUpdateFile", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
s := struct{}{}
|
||||||
|
|
||||||
|
err := conf.LoadAndUpdateFile("test.unknown", &s)
|
||||||
|
if assert.Error(t, err) {
|
||||||
|
assert.ErrorIs(t, err, conf.ErrUnsupportedFileType)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoFileExists(t, "test.unknown")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestSuite(t *testing.T, ext string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
testLoadFile(t, ext)
|
||||||
|
testLoadFiles(t, ext)
|
||||||
|
testSaveFile(t, ext)
|
||||||
|
testLoadAndUpdateFile(t, ext)
|
||||||
|
}
|
||||||
|
|
||||||
type testconf struct {
|
type testconf struct {
|
||||||
inUpdate func()
|
inUpdate func()
|
||||||
String string
|
String string
|
||||||
|
@ -24,374 +86,352 @@ func (t testconf) Update() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadFile(t *testing.T) {
|
func testLoadFile(t *testing.T, ext string) {
|
||||||
t.Parallel()
|
t.Helper()
|
||||||
|
|
||||||
t.Run("with a valid file", func(t *testing.T) {
|
t.Run("LoadFile", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testconf{
|
t.Run("with a valid file", func(t *testing.T) {
|
||||||
String: "default string",
|
t.Parallel()
|
||||||
Int: 1,
|
|
||||||
Invariant: "should not change",
|
|
||||||
}
|
|
||||||
|
|
||||||
file := "test_data/valid.json"
|
c := testconf{
|
||||||
|
String: "default string",
|
||||||
|
Int: 1,
|
||||||
|
Invariant: "should not change",
|
||||||
|
}
|
||||||
|
|
||||||
err := conf.LoadFile(file, &c)
|
file := "test_data/valid." + ext
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, "config string", c.String)
|
err := conf.LoadFile(file, &c)
|
||||||
assert.Equal(t, 42, c.Int)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "should not change", c.Invariant)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with an invalid file", func(t *testing.T) {
|
assert.Equal(t, "config string", c.String)
|
||||||
t.Parallel()
|
assert.Equal(t, 42, c.Int)
|
||||||
|
assert.Equal(t, "should not change", c.Invariant)
|
||||||
|
})
|
||||||
|
|
||||||
c := testconf{
|
t.Run("with an invalid file", func(t *testing.T) {
|
||||||
String: "default string",
|
t.Parallel()
|
||||||
Int: 1,
|
|
||||||
Invariant: "should not change",
|
|
||||||
}
|
|
||||||
|
|
||||||
file := "test_data/invalid.json"
|
c := testconf{
|
||||||
|
String: "default string",
|
||||||
|
Int: 1,
|
||||||
|
Invariant: "should not change",
|
||||||
|
}
|
||||||
|
|
||||||
err := conf.LoadFile(file, &c)
|
file := "test_data/invalid." + ext
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, "default string", c.String)
|
err := conf.LoadFile(file, &c)
|
||||||
assert.Equal(t, 1, c.Int)
|
require.Error(t, err)
|
||||||
assert.Equal(t, "should not change", c.Invariant)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with a non existent file", func(t *testing.T) {
|
assert.Equal(t, "default string", c.String)
|
||||||
t.Parallel()
|
assert.Equal(t, 1, c.Int)
|
||||||
|
assert.Equal(t, "should not change", c.Invariant)
|
||||||
|
})
|
||||||
|
|
||||||
c := testconf{
|
t.Run("with a non existent file", func(t *testing.T) {
|
||||||
String: "default string",
|
t.Parallel()
|
||||||
Int: 1,
|
|
||||||
Invariant: "should not change",
|
|
||||||
}
|
|
||||||
|
|
||||||
file := "does-not-exist.conf"
|
c := testconf{
|
||||||
|
String: "default string",
|
||||||
|
Int: 1,
|
||||||
|
Invariant: "should not change",
|
||||||
|
}
|
||||||
|
|
||||||
err := conf.LoadFile(file, &c)
|
file := "does-not-exist." + ext
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, "default string", c.String)
|
err := conf.LoadFile(file, &c)
|
||||||
assert.Equal(t, 1, c.Int)
|
require.Error(t, err)
|
||||||
assert.Equal(t, "should not change", c.Invariant)
|
|
||||||
|
assert.Equal(t, "default string", c.String)
|
||||||
|
assert.Equal(t, 1, c.Int)
|
||||||
|
assert.Equal(t, "should not change", c.Invariant)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadFiles(t *testing.T) {
|
func testLoadFiles(t *testing.T, ext string) {
|
||||||
t.Parallel()
|
t.Helper()
|
||||||
|
|
||||||
t.Run("with two valid files with different options", func(t *testing.T) {
|
t.Run("LoadFiles", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testconf{}
|
t.Run("with two valid files with different options", func(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
t.Parallel()
|
||||||
|
|
||||||
content1 := []byte(`{"String": "foo"}`)
|
c := testconf{}
|
||||||
content2 := []byte(`{"Int": 42}`)
|
err := conf.LoadFiles(&c, "test_data/part1."+ext, "test_data/part2."+ext)
|
||||||
paths := []string{
|
require.NoError(t, err)
|
||||||
filepath.Join(tmpDir, "file1.json"),
|
|
||||||
filepath.Join(tmpDir, "file2.json"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.WriteFile(paths[0], content1, 0o600)
|
assert.Equal(t, "foo", c.String)
|
||||||
require.NoError(t, err)
|
assert.Equal(t, 42, c.Int)
|
||||||
err = os.WriteFile(paths[1], content2, 0o600)
|
})
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = conf.LoadFiles(&c, paths...)
|
t.Run("with two valid files with the same option", func(t *testing.T) {
|
||||||
require.NoError(t, err)
|
t.Parallel()
|
||||||
|
|
||||||
assert.Equal(t, "foo", c.String)
|
c := testconf{}
|
||||||
assert.Equal(t, 42, c.Int)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with two valid files with the same option", func(t *testing.T) {
|
err := conf.LoadFiles(&c, "test_data/same1."+ext, "test_data/same2."+ext)
|
||||||
t.Parallel()
|
require.NoError(t, err)
|
||||||
|
|
||||||
c := testconf{}
|
assert.Equal(t, "bar", c.String)
|
||||||
tmpDir := t.TempDir()
|
})
|
||||||
|
|
||||||
content1 := []byte(`{"String": "foo"}`)
|
t.Run("with one non-existing and one existing file", func(t *testing.T) {
|
||||||
content2 := []byte(`{"String": "bar"}`)
|
t.Parallel()
|
||||||
paths := []string{
|
|
||||||
filepath.Join(tmpDir, "file1.json"),
|
|
||||||
filepath.Join(tmpDir, "file2.json"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.WriteFile(paths[0], content1, 0o600)
|
c := testconf{}
|
||||||
require.NoError(t, err)
|
|
||||||
err = os.WriteFile(paths[1], content2, 0o600)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = conf.LoadFiles(&c, paths...)
|
err := conf.LoadFiles(&c, "does-not-exist."+ext, "test_data/valid."+ext)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "bar", c.String)
|
assert.Equal(t, "config string", c.String)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("with one non-existing and one existing file", func(t *testing.T) {
|
t.Run("with one invalid and one valid file", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testconf{}
|
c := testconf{}
|
||||||
tmpDir := t.TempDir()
|
|
||||||
|
|
||||||
content2 := []byte(`{"String": "bar"}`)
|
err := conf.LoadFiles(&c, "test_data/invalid."+ext, "test_data/valid."+ext)
|
||||||
paths := []string{
|
require.Error(t, err)
|
||||||
"does-not-exist.json",
|
|
||||||
filepath.Join(tmpDir, "file2.json"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.WriteFile(paths[1], content2, 0o600)
|
assert.Equal(t, "", c.String)
|
||||||
require.NoError(t, err)
|
})
|
||||||
|
|
||||||
err = conf.LoadFiles(&c, paths...)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, "bar", c.String)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with one valid and one invalid file", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
c := testconf{}
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
|
|
||||||
content1 := []byte(`{"`)
|
|
||||||
content2 := []byte(`{"String": "bar"}`)
|
|
||||||
paths := []string{
|
|
||||||
filepath.Join(tmpDir, "file1.json"),
|
|
||||||
filepath.Join(tmpDir, "file2.json"),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.WriteFile(paths[0], content1, 0o600)
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = os.WriteFile(paths[1], content2, 0o600)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = conf.LoadFiles(&c, paths...)
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, "", c.String)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSaveFile(t *testing.T) {
|
func testSaveFile(t *testing.T, ext string) {
|
||||||
t.Parallel()
|
t.Helper()
|
||||||
|
|
||||||
t.Run("with a valid path", func(t *testing.T) {
|
t.Run("SaveFile", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
c := testconf{
|
t.Run("with a valid path", func(t *testing.T) {
|
||||||
String: "default string",
|
t.Parallel()
|
||||||
Invariant: "should not change",
|
|
||||||
Int: 1,
|
|
||||||
}
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
file := filepath.Join(tmpDir, "test.conf")
|
|
||||||
|
|
||||||
err := conf.SaveFile(file, &c)
|
c := testconf{
|
||||||
require.NoError(t, err)
|
String: "default string",
|
||||||
|
Invariant: "should not change",
|
||||||
|
Int: 1,
|
||||||
|
}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
file := filepath.Join(tmpDir, "test."+ext)
|
||||||
|
|
||||||
assert.FileExists(t, file)
|
err := conf.SaveFile(file, &c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
expected := "{\n \"String\": \"default string\",\n \"Invariant\": \"should not change\",\n \"Int\": 1\n}"
|
require.FileExists(t, file)
|
||||||
got, err := os.ReadFile(file)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, string(got))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("with a valid path and invalid data", func(t *testing.T) {
|
expected, err := os.ReadFile("test_data/full." + ext)
|
||||||
t.Parallel()
|
require.NoError(t, err)
|
||||||
|
got, err := os.ReadFile(file)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
assert.Equal(t, string(expected), string(got))
|
||||||
file := filepath.Join(tmpDir, "test.conf")
|
})
|
||||||
|
|
||||||
err := conf.SaveFile(file, func() error { return nil })
|
t.Run("with a valid path and invalid data", func(t *testing.T) {
|
||||||
require.Error(t, err)
|
t.Parallel()
|
||||||
|
|
||||||
assert.NoFileExists(t, file)
|
tmpDir := t.TempDir()
|
||||||
})
|
file := filepath.Join(tmpDir, "test."+ext)
|
||||||
|
|
||||||
t.Run("with an invalid path", func(t *testing.T) {
|
err := conf.SaveFile(file, func() error { return nil })
|
||||||
t.Parallel()
|
require.Error(t, err)
|
||||||
|
|
||||||
c := testconf{
|
assert.NoFileExists(t, file)
|
||||||
String: "default string",
|
})
|
||||||
Invariant: "should not change",
|
|
||||||
Int: 1,
|
|
||||||
}
|
|
||||||
file := "cannot/write/here.conf"
|
|
||||||
|
|
||||||
err := conf.SaveFile(file, &c)
|
t.Run("with an invalid path", func(t *testing.T) {
|
||||||
require.Error(t, err)
|
t.Parallel()
|
||||||
|
|
||||||
assert.NoFileExists(t, file)
|
c := testconf{
|
||||||
|
String: "default string",
|
||||||
|
Invariant: "should not change",
|
||||||
|
Int: 1,
|
||||||
|
}
|
||||||
|
file := "cannot/write/here." + ext
|
||||||
|
|
||||||
|
err := conf.SaveFile(file, &c)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
assert.NoFileExists(t, file)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadAndUpdateFile(t *testing.T) {
|
func testLoadAndUpdateFile(t *testing.T, ext string) {
|
||||||
t.Parallel()
|
t.Helper()
|
||||||
|
|
||||||
t.Run("when the target file does not exist", func(t *testing.T) {
|
t.Run("LoadAndUpdateFile", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
updated := false
|
t.Run("when the target file does not exist", func(t *testing.T) {
|
||||||
c := testconf{
|
t.Parallel()
|
||||||
String: "default string",
|
|
||||||
Int: 1,
|
|
||||||
Invariant: "should not change",
|
|
||||||
inUpdate: func() { updated = true },
|
|
||||||
}
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
file := filepath.Join(tmpDir, "test.conf")
|
|
||||||
|
|
||||||
err := conf.LoadAndUpdateFile(file, &c)
|
updated := false
|
||||||
require.NoError(t, err)
|
c := testconf{
|
||||||
|
String: "default string",
|
||||||
|
Int: 1,
|
||||||
|
Invariant: "should not change",
|
||||||
|
inUpdate: func() { updated = true },
|
||||||
|
}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
file := filepath.Join(tmpDir, "does-not-exist."+ext)
|
||||||
|
|
||||||
var c2 testconf
|
err := conf.LoadAndUpdateFile(file, &c)
|
||||||
err = conf.LoadFile(file, &c2)
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, c.String, c2.String)
|
|
||||||
assert.Equal(t, c.Int, c2.Int)
|
|
||||||
assert.Equal(t, c.Invariant, c2.Invariant)
|
|
||||||
|
|
||||||
assert.False(t, updated)
|
require.FileExists(t, file)
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("when the path cannot be written", func(t *testing.T) {
|
var c2 testconf
|
||||||
t.Parallel()
|
err = conf.LoadFile(file, &c2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, c.String, c2.String)
|
||||||
|
assert.Equal(t, c.Int, c2.Int)
|
||||||
|
assert.Equal(t, c.Invariant, c2.Invariant)
|
||||||
|
|
||||||
updated := false
|
assert.False(t, updated)
|
||||||
c := testconf{
|
})
|
||||||
String: "default string",
|
|
||||||
Int: 1,
|
|
||||||
Invariant: "should not change",
|
|
||||||
inUpdate: func() { updated = true },
|
|
||||||
}
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
file := filepath.Join(tmpDir, "does-not-exist", "test.conf")
|
|
||||||
|
|
||||||
err := conf.LoadAndUpdateFile(file, &c)
|
t.Run("when the path cannot be written", func(t *testing.T) {
|
||||||
require.Error(t, err)
|
t.Parallel()
|
||||||
|
|
||||||
assert.NoFileExists(t, file)
|
updated := false
|
||||||
|
c := testconf{
|
||||||
|
String: "default string",
|
||||||
|
Int: 1,
|
||||||
|
Invariant: "should not change",
|
||||||
|
inUpdate: func() { updated = true },
|
||||||
|
}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
file := filepath.Join(tmpDir, "does-not-exist", "test."+ext)
|
||||||
|
|
||||||
assert.False(t, updated)
|
err := conf.LoadAndUpdateFile(file, &c)
|
||||||
})
|
require.Error(t, err)
|
||||||
|
|
||||||
t.Run("when the config file is invalid", func(t *testing.T) {
|
assert.NoFileExists(t, file)
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
updated := false
|
assert.False(t, updated)
|
||||||
c := testconf{
|
})
|
||||||
String: "default string",
|
|
||||||
Int: 1,
|
|
||||||
Invariant: "should not change",
|
|
||||||
inUpdate: func() { updated = true },
|
|
||||||
}
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
file := filepath.Join(tmpDir, "test.conf")
|
|
||||||
|
|
||||||
content := []byte(`String: not json`)
|
t.Run("when the config file is invalid", func(t *testing.T) {
|
||||||
err := os.WriteFile(file, content, 0o644)
|
t.Parallel()
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = conf.LoadAndUpdateFile(file, &c)
|
updated := false
|
||||||
require.Error(t, err)
|
c := testconf{
|
||||||
|
String: "default string",
|
||||||
|
Int: 1,
|
||||||
|
Invariant: "should not change",
|
||||||
|
inUpdate: func() { updated = true },
|
||||||
|
}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
file := filepath.Join(tmpDir, "test."+ext)
|
||||||
|
|
||||||
assert.False(t, updated)
|
content, err := os.ReadFile("test_data/invalid." + ext)
|
||||||
})
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("when the config file is valid", func(t *testing.T) {
|
err = os.WriteFile(file, content, 0o600)
|
||||||
t.Parallel()
|
require.NoError(t, err)
|
||||||
|
|
||||||
updated := false
|
err = conf.LoadAndUpdateFile("test_data/invalid."+ext, &c)
|
||||||
c := testconf{
|
require.Error(t, err)
|
||||||
String: "default string",
|
|
||||||
Int: 1,
|
|
||||||
Invariant: "should not change",
|
|
||||||
inUpdate: func() { updated = true },
|
|
||||||
}
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
file := filepath.Join(tmpDir, "test.conf")
|
|
||||||
|
|
||||||
content := []byte(`{"String": "config string", "Int": 42}`)
|
assert.False(t, updated)
|
||||||
err := os.WriteFile(file, content, 0o644)
|
})
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = conf.LoadAndUpdateFile(file, &c)
|
t.Run("when the config file is valid", func(t *testing.T) {
|
||||||
require.NoError(t, err)
|
t.Parallel()
|
||||||
|
|
||||||
var c2 testconf
|
updated := false
|
||||||
err = conf.LoadFile(file, &c2)
|
c := testconf{
|
||||||
require.NoError(t, err)
|
String: "default string",
|
||||||
assert.Equal(t, "config string", c2.String)
|
Int: 1,
|
||||||
assert.Equal(t, 42, c2.Int)
|
Invariant: "should not change",
|
||||||
|
inUpdate: func() { updated = true },
|
||||||
|
}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
file := filepath.Join(tmpDir, "test."+ext)
|
||||||
|
|
||||||
assert.True(t, updated)
|
content, err := os.ReadFile("test_data/valid." + ext)
|
||||||
})
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("when the config file is missing options", func(t *testing.T) {
|
err = os.WriteFile(file, content, 0o600)
|
||||||
t.Parallel()
|
require.NoError(t, err)
|
||||||
|
|
||||||
updated := false
|
err = conf.LoadAndUpdateFile(file, &c)
|
||||||
c := testconf{
|
require.NoError(t, err)
|
||||||
String: "default string",
|
|
||||||
Int: 1,
|
|
||||||
Invariant: "should not change",
|
|
||||||
inUpdate: func() { updated = true },
|
|
||||||
}
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
file := filepath.Join(tmpDir, "test.conf")
|
|
||||||
|
|
||||||
content := []byte(`{"String": "config string"}`)
|
var c2 testconf
|
||||||
err := os.WriteFile(file, content, 0o644)
|
err = conf.LoadFile(file, &c2)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "config string", c2.String)
|
||||||
|
assert.Equal(t, 42, c2.Int)
|
||||||
|
assert.Equal(t, "should not change", c2.Invariant)
|
||||||
|
|
||||||
err = conf.LoadAndUpdateFile(file, &c)
|
assert.True(t, updated)
|
||||||
require.NoError(t, err)
|
})
|
||||||
|
|
||||||
newContent, err := os.ReadFile(file)
|
t.Run("when the config file is missing options", func(t *testing.T) {
|
||||||
require.NoError(t, err)
|
t.Parallel()
|
||||||
|
|
||||||
assert.Contains(t, string(newContent), "Int") //nolint:usestdlibvars // not the constant here
|
updated := false
|
||||||
assert.Contains(t, string(newContent), "Invariant")
|
c := testconf{
|
||||||
|
String: "default string",
|
||||||
|
Int: 1,
|
||||||
|
Invariant: "should not change",
|
||||||
|
inUpdate: func() { updated = true },
|
||||||
|
}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
file := filepath.Join(tmpDir, "test."+ext)
|
||||||
|
|
||||||
assert.True(t, updated)
|
content, err := os.ReadFile("test_data/valid." + ext)
|
||||||
})
|
require.NoError(t, err)
|
||||||
|
|
||||||
t.Run("when the config contains unknown options", func(t *testing.T) {
|
err = os.WriteFile(file, content, 0o600)
|
||||||
t.Parallel()
|
require.NoError(t, err)
|
||||||
|
|
||||||
updated := false
|
err = conf.LoadAndUpdateFile(file, &c)
|
||||||
c := testconf{
|
require.NoError(t, err)
|
||||||
String: "default string",
|
|
||||||
Int: 1,
|
|
||||||
Invariant: "should not change",
|
|
||||||
inUpdate: func() { updated = true },
|
|
||||||
}
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
file := filepath.Join(tmpDir, "test.conf")
|
|
||||||
|
|
||||||
content := []byte(`{"String": "config string", "Foo": "blah"}`)
|
newContent, err := os.ReadFile(file)
|
||||||
err := os.WriteFile(file, content, 0o644)
|
require.NoError(t, err)
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = conf.LoadAndUpdateFile(file, &c)
|
assert.Contains(t, string(newContent), "Invariant")
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
newContent, err := os.ReadFile(file)
|
assert.True(t, updated)
|
||||||
require.NoError(t, err)
|
})
|
||||||
assert.NotContains(t, string(newContent), "Foo")
|
|
||||||
|
|
||||||
assert.True(t, updated)
|
t.Run("when the config contains unknown options", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
updated := false
|
||||||
|
c := testconf{
|
||||||
|
String: "default string",
|
||||||
|
Int: 1,
|
||||||
|
Invariant: "should not change",
|
||||||
|
inUpdate: func() { updated = true },
|
||||||
|
}
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
file := filepath.Join(tmpDir, "test."+ext)
|
||||||
|
|
||||||
|
content, err := os.ReadFile("test_data/unknown." + ext)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = os.WriteFile(file, content, 0o600)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = conf.LoadAndUpdateFile(file, &c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
newContent, err := os.ReadFile(file)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotContains(t, string(newContent), "Unknown")
|
||||||
|
|
||||||
|
assert.True(t, updated)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -6,6 +6,7 @@ require github.com/stretchr/testify v1.9.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -1,6 +1,8 @@
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|
5
test_data/full.json
Normal file
5
test_data/full.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"String": "default string",
|
||||||
|
"Invariant": "should not change",
|
||||||
|
"Int": 1
|
||||||
|
}
|
3
test_data/full.toml
Normal file
3
test_data/full.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
String = 'default string'
|
||||||
|
Invariant = 'should not change'
|
||||||
|
Int = 1
|
1
test_data/invalid.toml
Normal file
1
test_data/invalid.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
String: not toml
|
1
test_data/part1.json
Normal file
1
test_data/part1.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"String": "foo"}
|
1
test_data/part1.toml
Normal file
1
test_data/part1.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
String = "foo"
|
1
test_data/part2.json
Normal file
1
test_data/part2.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"Int": 42}
|
1
test_data/part2.toml
Normal file
1
test_data/part2.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Int = 42
|
1
test_data/same1.json
Normal file
1
test_data/same1.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"String": "foo"}
|
1
test_data/same1.toml
Normal file
1
test_data/same1.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
String = "foo"
|
1
test_data/same2.json
Normal file
1
test_data/same2.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"String": "bar"}
|
1
test_data/same2.toml
Normal file
1
test_data/same2.toml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
String = "bar"
|
5
test_data/unknown.json
Normal file
5
test_data/unknown.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"String": "config string",
|
||||||
|
"Int": 42,
|
||||||
|
"Unknown": "foo"
|
||||||
|
}
|
3
test_data/unknown.toml
Normal file
3
test_data/unknown.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
String = "config string"
|
||||||
|
Int = 42
|
||||||
|
Unknown = "foo"
|
2
test_data/valid.toml
Normal file
2
test_data/valid.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
string = "config string"
|
||||||
|
int = 42
|
1
test_data/valid.unknown
Normal file
1
test_data/valid.unknown
Normal file
|
@ -0,0 +1 @@
|
||||||
|
String: not json
|
Loading…
Reference in a new issue