diff --git a/adapters.go b/adapters.go index 4a28b0b..368b5ef 100644 --- a/adapters.go +++ b/adapters.go @@ -1,16 +1,20 @@ package conf import ( + "bytes" "encoding/json" "errors" "fmt" + "reflect" "strings" + "unicode" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsimple" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/pelletier/go-toml/v2" + "gopkg.in/ini.v1" "gopkg.in/yaml.v3" ) @@ -26,6 +30,7 @@ const ( typeTOML typeHCL typeYAML + typeINI ) // getType returns the type of the config file. @@ -40,6 +45,9 @@ func getType(filename string) filetype { case strings.HasSuffix(filename, ".yml") || strings.HasSuffix(filename, ".yaml"): return typeYAML + case strings.HasSuffix(filename, ".ini") || + strings.HasSuffix(filename, ".cfg"): + return typeINI default: return typeInvalid } @@ -58,6 +66,8 @@ func unmarshal(filepath string, data []byte, v any) error { return unmarshalHCL(filepath, data, v) case typeYAML: return unmarshalYAML(data, v) + case typeINI: + return unmarshalINI(data, v) default: return ErrUnsupportedFileType } @@ -74,6 +84,8 @@ func marshal(ft filetype, v any) ([]byte, error) { return marshalHCL(v) case typeYAML: return marshalYAML(v) + case typeINI: + return marshalINI(v) default: return nil, ErrUnsupportedFileType } @@ -178,3 +190,55 @@ func marshalYAML(v any) ([]byte, error) { return data, nil } + +// unmarshalINI unmarshals the given data to the given struct. +func unmarshalINI(data []byte, v any) error { + opts := ini.LoadOptions{} + cfg, err := ini.LoadSources(opts, data) + if err != nil { + return parseError(err) + } + + cfg.NameMapper = iniNameMapper + + err = cfg.MapTo(v) + if err != nil { + return parseError(err) + } + + return nil +} + +// marshalYAML marshals the given struct to bytes. +func marshalINI(v any) ([]byte, error) { + cfg := ini.Empty() + + val := reflect.ValueOf(v) + if val.Kind() != reflect.Ptr { + v = &v + } + + err := ini.ReflectFromWithMapper(cfg, v, iniNameMapper) + if err != nil { + return nil, fmt.Errorf("cannot generate config content: %w", err) + } + + var buf bytes.Buffer + cfg.WriteTo(&buf) + data := buf.Bytes() + + return data, nil +} + +func iniNameMapper(raw string) string { + newstr := make([]rune, 0, len(raw)) + for i, chr := range raw { + if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { + if i > 0 { + newstr = append(newstr, '_') + } + } + newstr = append(newstr, unicode.ToLower(chr)) + } + return string(newstr) +} diff --git a/config_test.go b/config_test.go index d302d3b..9e55316 100644 --- a/config_test.go +++ b/config_test.go @@ -35,6 +35,12 @@ func TestYAMLFiles(t *testing.T) { runTestSuite(t, "yaml") } +func TestINIFiles(t *testing.T) { + t.Parallel() + + runTestSuite(t, "ini") +} + func TestUnknownFiles(t *testing.T) { t.Parallel() diff --git a/go.mod b/go.mod index 6eeb6a3..31ee2dd 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,12 @@ module code.bcarlin.net/go/conf go 1.22 -toolchain go1.23.4 - require ( + github.com/hashicorp/hcl/v2 v2.23.0 github.com/pelletier/go-toml/v2 v2.2.3 github.com/stretchr/testify v1.10.0 + gopkg.in/ini.v1 v1.67.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -15,7 +16,6 @@ require ( github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/hashicorp/hcl/v2 v2.23.0 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/zclconf/go-cty v1.13.0 // indirect @@ -23,5 +23,4 @@ require ( golang.org/x/sys v0.5.0 // indirect golang.org/x/text v0.11.0 // indirect golang.org/x/tools v0.6.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b356020..45fb1af 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 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/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 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/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos= @@ -20,8 +22,12 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= @@ -30,5 +36,7 @@ golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test_data/full.ini b/test_data/full.ini new file mode 100644 index 0000000..91197e4 --- /dev/null +++ b/test_data/full.ini @@ -0,0 +1,3 @@ +string = default string +invariant = should not change +int = 1 diff --git a/test_data/invalid.ini b/test_data/invalid.ini new file mode 100644 index 0000000..ce94f17 --- /dev/null +++ b/test_data/invalid.ini @@ -0,0 +1 @@ +String not ini diff --git a/test_data/part1.ini b/test_data/part1.ini new file mode 100644 index 0000000..0f0d3b9 --- /dev/null +++ b/test_data/part1.ini @@ -0,0 +1 @@ +string = foo diff --git a/test_data/part2.ini b/test_data/part2.ini new file mode 100644 index 0000000..f09a38c --- /dev/null +++ b/test_data/part2.ini @@ -0,0 +1 @@ +int = 42 diff --git a/test_data/same1.ini b/test_data/same1.ini new file mode 100644 index 0000000..0f0d3b9 --- /dev/null +++ b/test_data/same1.ini @@ -0,0 +1 @@ +string = foo diff --git a/test_data/same2.ini b/test_data/same2.ini new file mode 100644 index 0000000..07e0ced --- /dev/null +++ b/test_data/same2.ini @@ -0,0 +1 @@ +string = bar diff --git a/test_data/unknown.ini b/test_data/unknown.ini new file mode 100644 index 0000000..5b2dcf1 --- /dev/null +++ b/test_data/unknown.ini @@ -0,0 +1,3 @@ +string = config string +int = 42 +unknown = foo diff --git a/test_data/valid.ini b/test_data/valid.ini new file mode 100644 index 0000000..4e6f923 --- /dev/null +++ b/test_data/valid.ini @@ -0,0 +1,2 @@ +string = config string +int = 42