commit 54eaab60f7e718dccc8958aed9f47e2696df4941
parent a599f69c8285cd3ac63d08c80f49054a88d99cdd
Author: Sean Enck <sean@ttypty.com>
Date: Sun, 28 Sep 2025 19:46:20 -0400
this is better handling of included files
Diffstat:
3 files changed, 136 insertions(+), 54 deletions(-)
diff --git a/internal/config/toml.go b/internal/config/toml.go
@@ -15,20 +15,24 @@ import (
)
const (
- isStrict = "strict"
- isInclude = "include"
- maxDepth = 10
- tomlInt = "integer"
- tomlBool = "boolean"
- tomlString = "string"
- tomlArray = "[]string"
- strictDefault = true
+ isInclude = "include"
+ maxDepth = 10
+ tomlInt = "integer"
+ tomlBool = "boolean"
+ tomlString = "string"
+ tomlArray = "[]string"
+ fileKey = "file"
+ requiredKey = "required"
)
type (
tomlType string
// Loader indicates how included files should be sourced
- Loader func(string) (io.Reader, error)
+ Loader func(string) (io.Reader, error)
+ stringWrapper struct {
+ value string
+ required bool
+ }
)
// DefaultTOML will load the internal, default TOML with additional comment markups
@@ -77,15 +81,15 @@ func DefaultTOML() (string, error) {
# depth allowed up to %d include levels
#
# it is ONLY used during TOML configuration loading
-%s = []
-
-# strict, when enabled, requires the configuration entries
-# to adhere to all loading rules.
#
-# it is currently only used to ignore included files that
-# are not found
-%s = %t
-`, maxDepth, isInclude, isStrict, strictDefault), "\n"} {
+# can be an array of strings: ['file.toml']
+# or an array of objects: [{%s = 'file.toml', %s = false}]
+#
+# this is useful to control optional includes (includes
+# are required by default, set to false to allow ignoring
+# files)
+%s = []
+`, maxDepth, fileKey, requiredKey, isInclude), "\n"} {
if _, err := builder.WriteString(header); err != nil {
return "", err
}
@@ -160,11 +164,15 @@ func Load(r io.Reader, loader Loader) error {
md := env.display()
switch md.tomlType {
case tomlArray:
- array, err := parseStringArray(v, md.canExpand)
+ array, err := parseArray(v, md.canExpand, false)
if err != nil {
return err
}
- store.SetArray(export, array)
+ var s []string
+ for _, item := range array {
+ s = append(s, item.value)
+ }
+ store.SetArray(export, s)
case tomlInt:
i, ok := v.(int64)
if !ok {
@@ -220,27 +228,18 @@ func readConfigs(r io.Reader, depth int, loader Loader) ([]map[string]any, error
return nil, err
}
maps := []map[string]any{m}
- strict := strictDefault
- if v, ok := m[isStrict]; ok {
- delete(m, isStrict)
- b, err := parseBool(v)
- if err != nil {
- return nil, err
- }
- strict = b
- }
includes, ok := m[isInclude]
if ok {
delete(m, isInclude)
- including, err := parseStringArray(includes, true)
+ including, err := parseArray(includes, true, true)
if err != nil {
return nil, err
}
if len(including) > 0 {
- for _, s := range including {
- files := []string{s}
- if strings.Contains(s, "*") {
- matched, err := filepath.Glob(s)
+ for _, item := range including {
+ files := []string{item.value}
+ if strings.Contains(item.value, "*") {
+ matched, err := filepath.Glob(item.value)
if err != nil {
return nil, err
}
@@ -252,7 +251,7 @@ func readConfigs(r io.Reader, depth int, loader Loader) ([]map[string]any, error
return nil, err
}
if reader == nil {
- if strict {
+ if item.required {
return nil, fmt.Errorf("failed to load the included file: %s", file)
}
continue
@@ -269,21 +268,56 @@ func readConfigs(r io.Reader, depth int, loader Loader) ([]map[string]any, error
return maps, nil
}
-func parseStringArray(value any, expand bool) ([]string, error) {
- var res []string
+func parseArray(value any, expand, allowMap bool) ([]stringWrapper, error) {
+ expander := func(s string) string {
+ return s
+ }
+ if expand {
+ expander = func(s string) string {
+ return os.Expand(s, os.Getenv)
+ }
+ }
+ var res []stringWrapper
switch t := value.(type) {
case []any:
for _, item := range t {
+ err := fmt.Errorf("value is not valid array value: %v", item)
+ val := ""
+ required := true
switch s := item.(type) {
case string:
- val := s
- if expand {
- val = os.Expand(s, os.Getenv)
+ val = s
+ case map[string]any:
+ if !allowMap {
+ return nil, err
+ }
+ length := len(s)
+ if length > 2 {
+ return nil, fmt.Errorf("invalid map array, too many keys: %v", item)
+ }
+ file, ok := s[fileKey]
+ if !ok {
+ return nil, fmt.Errorf("'%s' is required, missing: %v", fileKey, item)
+ }
+ val, ok = file.(string)
+ if !ok {
+ return nil, newTypeError("string", file)
+ }
+ if length == 2 {
+ req, ok := s[requiredKey]
+ if !ok {
+ return nil, fmt.Errorf("only '%s' key is allowed here: %v", requiredKey, item)
+ }
+ b, err := parseBool(req)
+ if err != nil {
+ return nil, err
+ }
+ required = b
}
- res = append(res, val)
default:
- return nil, fmt.Errorf("value is not string in array: %v", item)
+ return nil, err
}
+ res = append(res, stringWrapper{value: expander(val), required: required})
}
default:
return nil, fmt.Errorf("value is not of array type: %v", value)
diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go
@@ -61,7 +61,7 @@ func TestLoadIncludes(t *testing.T) {
} else {
return nil, errors.New("invalid path")
}
- }); err == nil || err.Error() != "value is not string in array: 1" {
+ }); err == nil || err.Error() != "value is not valid array value: 1" {
t.Errorf("invalid error: %v", err)
}
data = `include = ["$TEST/abc"]
@@ -95,7 +95,7 @@ func TestArrayLoad(t *testing.T) {
copy = ["'xyz/$TEST'", "s", 1]
`
r := strings.NewReader(data)
- if err := config.Load(r, emptyRead); err == nil || err.Error() != "value is not string in array: 1" {
+ if err := config.Load(r, emptyRead); err == nil || err.Error() != "value is not valid array value: 1" {
t.Errorf("invalid error: %v", err)
}
data = `include = []
@@ -267,27 +267,25 @@ otp_format = "$TEST"
}
}
-func TestLoadIncludesStrictControls(t *testing.T) {
+func TestLoadIncludesControls(t *testing.T) {
store.Clear()
defer os.Clearenv()
t.Setenv("TEST", "xyz")
data := `include = ["$TEST/abc"]
store="xyz"
-strict = true
`
r := strings.NewReader(data)
if err := config.Load(r, func(p string) (io.Reader, error) {
if p == "xyz/abc" {
- return strings.NewReader("include = ['123']\nstrict = 1\nstore = 'abc'"), nil
+ return strings.NewReader("include = [{file = '123', required = 1}]\nstore = 'abc'"), nil
} else {
return nil, errors.New("invalid path")
}
}); err == nil || err.Error() != "non-bool found where bool expected: 1" {
t.Errorf("invalid error: %v", err)
}
- data = `include = ["$TEST/abc"]
+ data = `include = [{file = "$TEST/abc", required = true}]
store="xyz"
-strict = true
`
r = strings.NewReader(data)
if err := config.Load(r, func(_ string) (io.Reader, error) {
@@ -295,9 +293,8 @@ strict = true
}); err == nil || err.Error() != "failed to load the included file: xyz/abc" {
t.Errorf("invalid error: %v", err)
}
- data = `include = ["$TEST/abc"]
+ data = `include = [{file = "$TEST/abc", required = false}]
store="xyz"
-strict = false
`
r = strings.NewReader(data)
if err := config.Load(r, func(_ string) (io.Reader, error) {
@@ -305,4 +302,40 @@ strict = false
}); err != nil {
t.Errorf("invalid error: %v", err)
}
+ data = `include = [{file = "$TEST/abc", required = false, other = 1}]
+store="xyz"
+`
+ r = strings.NewReader(data)
+ if err := config.Load(r, func(_ string) (io.Reader, error) {
+ return nil, nil
+ }); err == nil || !strings.Contains(err.Error(), "invalid map array, too many keys:") {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = [{fsle = "$TEST/abc"}]
+store="xyz"
+`
+ r = strings.NewReader(data)
+ if err := config.Load(r, func(_ string) (io.Reader, error) {
+ return nil, nil
+ }); err == nil || !strings.Contains(err.Error(), "'file' is required, missing:") {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = [{file = "$TEST/abc", require = 1}]
+store="xyz"
+`
+ r = strings.NewReader(data)
+ if err := config.Load(r, func(_ string) (io.Reader, error) {
+ return nil, nil
+ }); err == nil || !strings.Contains(err.Error(), "only 'required' key is allowed here:") {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = [{file = "$TEST/abc", required = 1}]
+store="xyz"
+`
+ r = strings.NewReader(data)
+ if err := config.Load(r, func(_ string) (io.Reader, error) {
+ return nil, nil
+ }); err == nil || err.Error() != "non-bool found where bool expected: 1" {
+ t.Errorf("invalid error: %v", err)
+ }
}
diff --git a/internal/platform/os_test.go b/internal/platform/os_test.go
@@ -41,7 +41,7 @@ func TestLoadConfigFile(t *testing.T) {
}
}
-func TestLoadConfigFileNoFileStrict(t *testing.T) {
+func TestLoadConfigFileNoFile(t *testing.T) {
store.Clear()
os.Mkdir("testdata", 0o755)
defer os.RemoveAll("testdata")
@@ -57,7 +57,7 @@ func TestLoadConfigFileNoFileStrict(t *testing.T) {
}
}
-func TestLoadConfigFileNoFileNoStrict(t *testing.T) {
+func TestLoadConfigFileNoFileNotRequired(t *testing.T) {
store.Clear()
os.Mkdir("testdata", 0o755)
defer os.RemoveAll("testdata")
@@ -66,10 +66,25 @@ func TestLoadConfigFileNoFileNoStrict(t *testing.T) {
if err != nil {
t.Errorf("invalid error: %v", err)
}
- loaded = strings.Replace(loaded, "include = []", "include = ['invalid.toml']", 1)
- loaded = strings.Replace(loaded, "strict = true", "strict = false", 1)
+ loaded = strings.Replace(loaded, "include = []", "include = [{file = 'invalid.toml', required = false}]", 1)
os.WriteFile(file, []byte(loaded), 0o644)
if err := platform.LoadConfigFile(file); err != nil {
t.Errorf("invalid error: %v", err)
}
}
+
+func TestLoadConfigFileNoFileRequired(t *testing.T) {
+ store.Clear()
+ os.Mkdir("testdata", 0o755)
+ defer os.RemoveAll("testdata")
+ file := filepath.Join("testdata", "config.toml")
+ loaded, err := config.DefaultTOML()
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ loaded = strings.Replace(loaded, "include = []", "include = [{file = 'invalid.toml'}]", 1)
+ os.WriteFile(file, []byte(loaded), 0o644)
+ if err := platform.LoadConfigFile(file); err == nil || err.Error() != "failed to load the included file: invalid.toml" {
+ t.Errorf("invalid error: %v", err)
+ }
+}