feat: add support for ini files

This commit is contained in:
Bruno Carlin 2025-06-09 15:15:21 +02:00
parent 89e6dbfb98
commit fdf99daa9c
12 changed files with 94 additions and 4 deletions

View file

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

View file

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

7
go.mod
View file

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

8
go.sum
View file

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

3
test_data/full.ini Normal file
View file

@ -0,0 +1,3 @@
string = default string
invariant = should not change
int = 1

1
test_data/invalid.ini Normal file
View file

@ -0,0 +1 @@
String not ini

1
test_data/part1.ini Normal file
View file

@ -0,0 +1 @@
string = foo

1
test_data/part2.ini Normal file
View file

@ -0,0 +1 @@
int = 42

1
test_data/same1.ini Normal file
View file

@ -0,0 +1 @@
string = foo

1
test_data/same2.ini Normal file
View file

@ -0,0 +1 @@
string = bar

3
test_data/unknown.ini Normal file
View file

@ -0,0 +1,3 @@
string = config string
int = 42
unknown = foo

2
test_data/valid.ini Normal file
View file

@ -0,0 +1,2 @@
string = config string
int = 42