diff --git a/CHANGELOG.md b/CHANGELOG.md index 137ab2e..35454da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Add support for TOML configuration files +- Add support for HCL configuration files - Use stdlib for tests instead of convey - Update golangci-lint configuration diff --git a/adapters.go b/adapters.go index 5e156b3..439b7c9 100644 --- a/adapters.go +++ b/adapters.go @@ -2,9 +2,14 @@ package conf import ( "encoding/json" + "errors" "fmt" "strings" + "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" ) @@ -14,6 +19,7 @@ const ( typeInvalid filetype = iota typeJSON typeTOML + typeHCL ) // getType returns the type of the config file. @@ -23,18 +29,24 @@ func getType(filename string) filetype { return typeJSON case strings.HasSuffix(filename, ".toml"): return typeTOML + case strings.HasSuffix(filename, ".hcl"): + return typeHCL default: return typeInvalid } } // unmarshal unmarshals the given data to the given struct. -func unmarshal(ft filetype, data []byte, v any) error { +func unmarshal(filepath string, data []byte, v any) error { + ft := getType(filepath) + switch ft { case typeJSON: return unmarshalJSON(data, v) case typeTOML: return unmarshalTOML(data, v) + case typeHCL: + return unmarshalHCL(filepath, data, v) default: return ErrUnsupportedFileType } @@ -47,6 +59,8 @@ func marshal(ft filetype, v any) ([]byte, error) { return marshalJSON(v) case typeTOML: return marshalTOML(v) + case typeHCL: + return marshalHCL(v) default: return nil, ErrUnsupportedFileType } @@ -91,3 +105,43 @@ func marshalTOML(v any) ([]byte, error) { return data, nil } + +// unmarshalHCL unmarshals the given data to the given struct. +func unmarshalHCL(filepath string, data []byte, v any) error { + err := hclsimple.Decode(filepath, data, nil, v) + + var diags hcl.Diagnostics + + errors.As(err, &diags) + newDiags := hclFilterDiagnostics(diags) + + if len(newDiags) > 0 { + return fmt.Errorf("cannot parse config file: %w", newDiags) + } + + return nil +} + +// marshalHCL marshals the given struct to bytes. +func marshalHCL(v any) (b []byte, err error) { //nolint:nonamedreturns // need named return to convert a panic to error + f := hclwrite.NewEmptyFile() + gohcl.EncodeIntoBody(v, f.Body()) + + return f.Bytes(), nil +} + +func hclFilterDiagnostics(diags hcl.Diagnostics) hcl.Diagnostics { + var newDiags hcl.Diagnostics + + for _, diag := range diags { + if diag.Summary != "Unsupported argument" { + newDiags = append(newDiags, diag) + } + } + + if len(newDiags) > 0 { + return newDiags + } + + return nil +} diff --git a/config.go b/config.go index 933598b..bb3dde4 100644 --- a/config.go +++ b/config.go @@ -7,6 +7,7 @@ // // - JSON: ".json" // - TOML: ".toml" +// - HCL: ".hcl" package conf import ( @@ -89,7 +90,7 @@ func read(path string, data any) error { return fmt.Errorf("cannot read config file: %w", err) } - return unmarshal(getType(path), content, data) + return unmarshal(path, content, data) } func write(path string, data any) error { diff --git a/config_test.go b/config_test.go index ca425d2..930788e 100644 --- a/config_test.go +++ b/config_test.go @@ -23,6 +23,12 @@ func TestTOMLFiles(t *testing.T) { runTestSuite(t, "toml") } +func TestHCLFiles(t *testing.T) { + t.Parallel() + + runTestSuite(t, "hcl") +} + func TestUnknownFiles(t *testing.T) { t.Parallel() @@ -75,9 +81,9 @@ func runTestSuite(t *testing.T, ext string) { type testconf struct { inUpdate func() - String string - Invariant string - Int int + String string `hcl:"String,optional"` + Invariant string `hcl:"Invariant,optional"` + Int int `hcl:"Int,optional"` } func (t testconf) Update() { diff --git a/go.mod b/go.mod index 6734b11..6eeb6a3 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,18 @@ require ( ) require ( + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + 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 + golang.org/x/mod v0.8.0 // indirect + 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 111e37e..b356020 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,33 @@ +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +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/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= +github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 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= +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/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= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/test_data/full.hcl b/test_data/full.hcl new file mode 100644 index 0000000..cbf6906 --- /dev/null +++ b/test_data/full.hcl @@ -0,0 +1,3 @@ +String = "default string" +Invariant = "should not change" +Int = 1 diff --git a/test_data/invalid.hcl b/test_data/invalid.hcl new file mode 100644 index 0000000..6df0bfc --- /dev/null +++ b/test_data/invalid.hcl @@ -0,0 +1 @@ +String: not hcl diff --git a/test_data/part1.hcl b/test_data/part1.hcl new file mode 100644 index 0000000..acec3a1 --- /dev/null +++ b/test_data/part1.hcl @@ -0,0 +1 @@ +String = "foo" diff --git a/test_data/part2.hcl b/test_data/part2.hcl new file mode 100644 index 0000000..a002354 --- /dev/null +++ b/test_data/part2.hcl @@ -0,0 +1 @@ +Int = 42 diff --git a/test_data/same1.hcl b/test_data/same1.hcl new file mode 100644 index 0000000..acec3a1 --- /dev/null +++ b/test_data/same1.hcl @@ -0,0 +1 @@ +String = "foo" diff --git a/test_data/same2.hcl b/test_data/same2.hcl new file mode 100644 index 0000000..570e876 --- /dev/null +++ b/test_data/same2.hcl @@ -0,0 +1 @@ +String = "bar" diff --git a/test_data/unknown.hcl b/test_data/unknown.hcl new file mode 100644 index 0000000..cbc6ca3 --- /dev/null +++ b/test_data/unknown.hcl @@ -0,0 +1,3 @@ +String = "config string" +Int = 42 +Unknown = "foo" diff --git a/test_data/valid.hcl b/test_data/valid.hcl new file mode 100644 index 0000000..3bc0f57 --- /dev/null +++ b/test_data/valid.hcl @@ -0,0 +1,2 @@ +String = "config string" +Int = 42