Skip to content

Commit

Permalink
add HCL (#43)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristaloleg authored Jan 15, 2021
1 parent cb9b214 commit 621bdbd
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ There are many solutions regarding configuration loading in Go. I was looking fo
* Automatic fields mapping.
* Supports different sources:
* defaults in the code
* files (JSON, YAML, TOML)
* files (JSON, YAML, TOML, DotENV, HCL)
* environment variables
* command-line flags
* Dependency-free (file parsers are optional).
Expand Down
1 change: 1 addition & 0 deletions aconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const (
jsonNameTag = "json"
yamlNameTag = "yaml"
tomlNameTag = "toml"
hclNameTag = "hcl"
envNameTag = "env"
flagNameTag = "flag"
)
Expand Down
8 changes: 8 additions & 0 deletions aconfighcl/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/cristalhq/aconfig/aconfighcl

go 1.15

require (
github.com/cristalhq/aconfig v0.10.0
github.com/hashicorp/hcl v1.0.0
)
6 changes: 6 additions & 0 deletions aconfighcl/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/cristalhq/aconfig v0.10.0 h1:5dyXjRkUBDuueSHFYxlLdxpZrnzGHwlffhL16NiT+XQ=
github.com/cristalhq/aconfig v0.10.0/go.mod h1:0ZBp7dUf0F2Jr7YbLjw8OVlAD0eeV2bU3NwmVgeUReo=
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/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
86 changes: 86 additions & 0 deletions aconfighcl/hcl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package aconfighcl

import (
"fmt"
"io/ioutil"
"strings"

"github.com/hashicorp/hcl"
)

// Decoder of HCL files for aconfig.
type Decoder struct{}

// New HCL decoder for aconfig.
func New() *Decoder { return &Decoder{} }

// DecodeFile implements aconfig.FileDecoder.
func (d *Decoder) DecodeFile(filename string) (map[string]interface{}, error) {
b, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}

f, err := hcl.ParseBytes(b)
if err != nil {
return nil, err
}

var raw map[string]interface{}
if err := hcl.DecodeObject(&raw, f); err != nil {
return nil, err
}

res := map[string]interface{}{}

for key, value := range raw {
flatten("", key, value, res)
}
return res, nil
}

// copied and adapted from aconfig/utils.go
//
func flatten(prefix, key string, curr interface{}, res map[string]interface{}) {
switch curr := curr.(type) {
case []map[string]interface{}:
for _, v := range curr {
flatten(prefix+key, "", v, res)
}
case []map[interface{}]interface{}:
for k, v := range curr {
flatten(prefix+key+".", fmt.Sprint(k), v, res)
}

case map[string]interface{}:
for k, v := range curr {
flatten(prefix+key+".", k, v, res)
}

case map[interface{}]interface{}:
for k, v := range curr {
if k, ok := k.(string); ok {
flatten(prefix+key+".", k, v, res)
}
}
case []interface{}:
b := &strings.Builder{}
for i, v := range curr {
if i > 0 {
b.WriteByte(',')
}
b.WriteString(fmt.Sprint(v))
}
res[prefix+key] = b.String()
case bool:
res[prefix+key] = fmt.Sprint(curr)
case string:
res[prefix+key] = curr
case float64:
res[prefix+key] = fmt.Sprint(curr)
case int, int8, int16, int32:
res[prefix+key] = fmt.Sprint(curr)
default:
panic(fmt.Sprintf("%s::%s got %T %v", prefix, key, curr, curr))
}
}
155 changes: 155 additions & 0 deletions aconfighcl/hcl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package aconfighcl_test

import (
"os"
"reflect"
"testing"

"github.com/cristalhq/aconfig"
"github.com/cristalhq/aconfig/aconfighcl"
)

func TestHCL(t *testing.T) {
filepath := createTestFile(t)

var cfg structConfig
loader := aconfig.LoaderFor(&cfg, aconfig.Config{
SkipDefaults: true,
SkipEnv: true,
SkipFlags: true,
FileDecoders: map[string]aconfig.FileDecoder{
".hcl": aconfighcl.New(),
},
Files: []string{filepath},
})

if err := loader.Load(); err != nil {
t.Fatal(err)
}

i := int32(42)
j := int64(420)
want := structConfig{
A: "b",
C: 10,
E: 123.456,
B: []byte("abc"),
I: &i,
J: &j,
Y: structY{
X: "y",
Z: []string{"1", "2", "3"},
A: structD{
I: true,
},
},
AA: structA{
X: "y",
BB: structB{
CC: structC{
MM: "n",
BB: []byte("boo"),
},
DD: []string{"x", "y", "z"},
},
},
StructM: StructM{
M: "n",
},
}

if got := cfg; !reflect.DeepEqual(want, got) {
t.Fatalf("want %v, got %v", want, got)
}
}

func createTestFile(t *testing.T) string {
t.Helper()
dir := t.TempDir()
t.Cleanup(func() {
os.RemoveAll(dir)
})

filepath := dir + "/testfile.hcl"

f, err := os.Create(filepath)
if err != nil {
t.Fatal(err)
}
_, err = f.WriteString(testfileContent)
if err != nil {
t.Fatal(err)
}
return filepath
}

type structConfig struct {
A string `hcl:"a"`
C int `hcl:"c"`
E float64 `hcl:"e"`
B []byte `hcl:"b"`
I *int32 `hcl:"i"`
J *int64 `hcl:"j"`
Y structY `hcl:"y"`

AA structA `hcl:"A"`
StructM
}

type structY struct {
X string `hcl:"x"`
Z []string `hcl:"z"`
A structD `hcl:"A"`
}

type structA struct {
X string `hcl:"x"`
BB structB `hcl:"B"`
}

type structB struct {
CC structC `hcl:"C"`
DD []string `hcl:"D"`
}

type structC struct {
MM string `hcl:"m"`
BB []byte `hcl:"b"`
}

type structD struct {
I bool `hcl:"i"`
}

type StructM struct {
M string `hcl:"M"`
}

const testfileContent = `
"a" = "b"
"c" = 10
"e" = 123.456
"b" = "abc"
"i" = 42
"j" = 420
"y" = {
"x" = "y"
"z" = ["1", "2", "3"]
"A" = {
"i" = true
}
}
"A" = {
"x" = "y"
"B" = {
"C" = {
"m" = "n"
"b" = "boo"
}
"D" = ["x", "y", "z"]
}
}
"M" = "n"
`
4 changes: 4 additions & 0 deletions reflection.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type fieldData struct {
jsonName string
yamlName string
tomlName string
hclName string
envName string
flagName string
}
Expand All @@ -40,6 +41,7 @@ func (l *Loader) newFieldData(field reflect.StructField, value reflect.Value, pa
jsonName: l.makeTagValue(field, jsonNameTag, words),
yamlName: l.makeTagValue(field, yamlNameTag, words),
tomlName: l.makeTagValue(field, tomlNameTag, words),
hclName: l.makeTagValue(field, hclNameTag, words),
envName: l.makeEnvName(field, words),
flagName: l.makeTagValue(field, flagNameTag, words),
}
Expand Down Expand Up @@ -74,6 +76,8 @@ func (f *fieldData) Tag(tag string) string {
return f.yamlName
case tomlNameTag:
return f.tomlName
case hclNameTag:
return f.hclName
case envNameTag:
return f.envName
case flagNameTag:
Expand Down
2 changes: 1 addition & 1 deletion utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (l *Loader) makeTagValue(field reflect.StructField, tag string, words []str
return v
}
switch tag {
case jsonNameTag, yamlNameTag, tomlNameTag:
case jsonNameTag, yamlNameTag, tomlNameTag, hclNameTag:
if l.config.DontGenerateTags {
return field.Name
}
Expand Down

0 comments on commit 621bdbd

Please sign in to comment.