feat: add support for hcl files

This commit is contained in:
Bruno Carlin 2025-01-15 01:49:44 +01:00
parent 14e0b29372
commit 8509f98d69
Signed by: bcarlin
GPG key ID: 8E254EA0FFEB9B6D
14 changed files with 113 additions and 5 deletions

View file

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

View file

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

View file

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

View file

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

11
go.mod
View file

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

22
go.sum
View file

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

3
test_data/full.hcl Normal file
View file

@ -0,0 +1,3 @@
String = "default string"
Invariant = "should not change"
Int = 1

1
test_data/invalid.hcl Normal file
View file

@ -0,0 +1 @@
String: not hcl

1
test_data/part1.hcl Normal file
View file

@ -0,0 +1 @@
String = "foo"

1
test_data/part2.hcl Normal file
View file

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

1
test_data/same1.hcl Normal file
View file

@ -0,0 +1 @@
String = "foo"

1
test_data/same2.hcl Normal file
View file

@ -0,0 +1 @@
String = "bar"

3
test_data/unknown.hcl Normal file
View file

@ -0,0 +1,3 @@
String = "config string"
Int = 42
Unknown = "foo"

2
test_data/valid.hcl Normal file
View file

@ -0,0 +1,2 @@
String = "config string"
Int = 42