Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,18 @@ var (
// Unmarshal accepts a byte slice as input and writes the
// data to the value pointed to by v.
func Unmarshal(bs []byte, v interface{}) error {
root, err := parse(bs)
root, err := parse(bs, false)
if err != nil {
return err
}

return DecodeObject(v, root)
}

// UnmarshalErrorOnDuplicates accepts a byte slice as input and writes the
// data to the value pointed to by v but errors on duplicate attribute key.
func UnmarshalErrorOnDuplicates(bs []byte, v interface{}) error {
root, err := parse(bs, true)
if err != nil {
return err
}
Expand All @@ -35,7 +46,19 @@ func Unmarshal(bs []byte, v interface{}) error {
// Decode reads the given input and decodes it into the structure
// given by `out`.
func Decode(out interface{}, in string) error {
obj, err := Parse(in)
return decode(out, in, false)
}

// DecodeErrorOnDuplicates reads the given input and decodes it into the structure but errrors on duplicate attribute key
// given by `out`.
func DecodeErrorOnDuplicates(out interface{}, in string) error {
return decode(out, in, true)
}

// decode reads the given input and decodes it into the structure given by `out`.
// takes in a boolean to determine if it should error on duplicate attribute
func decode(out interface{}, in string, errorOnDuplicateAtributes bool) error {
obj, err := parse([]byte(in), errorOnDuplicateAtributes)
if err != nil {
return err
}
Expand Down Expand Up @@ -393,14 +416,15 @@ func (d *decoder) decodeMap(name string, node ast.Node, result reflect.Value) er

// Set the final map if we can
set.Set(resultMap)

return nil
}

func (d *decoder) decodePtr(name string, node ast.Node, result reflect.Value) error {
// if pointer is not nil, decode into existing value
if !result.IsNil() {
return d.decode(name, node, result.Elem())
}
// if pointer is not nil, decode into existing value
if !result.IsNil() {
return d.decode(name, node, result.Elem())
}

// Create an element of the concrete (non pointer) type and decode
// into that. Then set the value of the pointer to this type.
Expand Down
50 changes: 50 additions & 0 deletions decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ func TestDecode_interface(t *testing.T) {
"foo-bar": "baz",
},
},
{
"basic_duplicates.hcl",
false,
map[string]interface{}{
"foo": "${file(\"bing/bong.txt\")}",
},
},
{
"empty.hcl",
false,
Expand Down Expand Up @@ -450,6 +457,49 @@ func TestDecode_interface(t *testing.T) {
}
}

func TestDecodeErrorOnDuplicates_interface(t *testing.T) {
cases := []struct {
File string
Err bool
Out interface{}
}{
{
"basic_duplicates.hcl",
true,
nil,
},
}

for _, tc := range cases {
t.Run(tc.File, func(t *testing.T) {
d, err := ioutil.ReadFile(filepath.Join(fixtureDir, tc.File))
if err != nil {
t.Fatalf("err: %s", err)
}

var out interface{}
err = DecodeErrorOnDuplicates(&out, string(d))
if (err != nil) != tc.Err {
t.Fatalf("Input: %s\n\nError: %s", tc.File, err)
}

if !reflect.DeepEqual(out, tc.Out) {
t.Fatalf("Input: %s. Actual, Expected.\n\n%#v\n\n%#v", tc.File, out, tc.Out)
}

var v interface{}
err = UnmarshalErrorOnDuplicates(d, &v)
if (err != nil) != tc.Err {
t.Fatalf("Input: %s\n\nError: %s", tc.File, err)
}

if !reflect.DeepEqual(v, tc.Out) {
t.Fatalf("Input: %s. Actual, Expected.\n\n%#v\n\n%#v", tc.File, out, tc.Out)
}
})
}
}

func TestDecode_interfaceInline(t *testing.T) {
cases := []struct {
Value string
Expand Down
30 changes: 20 additions & 10 deletions hcl/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,35 @@ type Parser struct {
enableTrace bool
indent int
n int // buffer size (max = 1)

errorOnDuplicateKeys bool
}

func newParser(src []byte) *Parser {
func newParser(src []byte, errorOnDuplicateKeys bool) *Parser {
return &Parser{
sc: scanner.New(src),
sc: scanner.New(src),
errorOnDuplicateKeys: errorOnDuplicateKeys,
}
}

// Parse returns the fully parsed source and returns the abstract syntax tree.
func Parse(src []byte) (*ast.File, error) {
return parse(src, true)
}

// Parse returns the fully parsed source and returns the abstract syntax tree.
func ParseDontErrorOnDuplicateKeys(src []byte) (*ast.File, error) {
return parse(src, false)
}

// Parse returns the fully parsed source and returns the abstract syntax tree.
func parse(src []byte, errorOnDuplicateKeys bool) (*ast.File, error) {
// normalize all line endings
// since the scanner and output only work with "\n" line endings, we may
// end up with dangling "\r" characters in the parsed data.
src = bytes.Replace(src, []byte("\r\n"), []byte("\n"), -1)

p := newParser(src)
p := newParser(src, errorOnDuplicateKeys)
return p.Parse()
}

Expand All @@ -65,6 +78,7 @@ func (p *Parser) Parse() (*ast.File, error) {
}

f.Comments = p.comments

return f, nil
}

Expand Down Expand Up @@ -97,6 +111,9 @@ func (p *Parser) objectList(obj bool) (*ast.ObjectList, error) {

if n.Assign.String() != "-" {
for _, key := range n.Keys {
if !p.errorOnDuplicateKeys {
break
}
_, ok := seenKeys[key.Token.Text]
if ok {
return nil, errors.New(fmt.Sprintf("The argument %q at %s was already set. Each argument can only be defined once", key.Token.Text, key.Token.Pos.String()))
Expand Down Expand Up @@ -241,7 +258,6 @@ func (p *Parser) objectItem() (*ast.ObjectItem, error) {
func (p *Parser) objectKey() ([]*ast.ObjectKey, error) {
keyCount := 0
keys := make([]*ast.ObjectKey, 0)
seenKeys := map[string]struct{}{}

for {
tok := p.scan()
Expand Down Expand Up @@ -285,12 +301,6 @@ func (p *Parser) objectKey() ([]*ast.ObjectKey, error) {
// object
return keys, err
case token.IDENT, token.STRING:
_, ok := seenKeys[p.tok.Text]
if ok {
return nil, errors.New(fmt.Sprintf("The argument %q at %s was already set. Each argument can only be defined once", p.tok.Text, p.tok.Pos.String()))
}
seenKeys[p.tok.Text] = struct{}{}

keyCount++
keys = append(keys, &ast.ObjectKey{Token: p.tok})
case token.ILLEGAL:
Expand Down
41 changes: 26 additions & 15 deletions hcl/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestType(t *testing.T) {
}

for _, l := range literals {
p := newParser([]byte(l.src))
p := newParser([]byte(l.src), true)
item, err := p.objectItem()
if err != nil {
t.Error(err)
Expand Down Expand Up @@ -78,7 +78,7 @@ EOF
}

for _, l := range literals {
p := newParser([]byte(l.src))
p := newParser([]byte(l.src), true)
item, err := p.objectItem()
if err != nil {
t.Error(err)
Expand All @@ -105,7 +105,7 @@ func TestListOfMaps(t *testing.T) {
{key = "bar"},
{key = "baz", key2 = "qux"},
]`
p := newParser([]byte(src))
p := newParser([]byte(src), true)

file, err := p.Parse()
if err != nil {
Expand Down Expand Up @@ -138,7 +138,7 @@ func TestDuplicateKeys_NotAllowed(t *testing.T) {
src := `key = "value1"
key = "value2"
`
p := newParser([]byte(src))
p := newParser([]byte(src), true)

_, err := p.Parse()
if err == nil {
Expand All @@ -151,11 +151,23 @@ func TestDuplicateKeys_NotAllowed(t *testing.T) {
}
}

func TestDuplicateKeys_DontErrorWhenErrorOnDuplicateKeysIsFalse(t *testing.T) {
src := `key = "value1"
key = "value2"
`
p := newParser([]byte(src), false)

_, err := p.Parse()
if err != nil {
t.Fatalf("Unexpected error " + err.Error())
}
}

func TestDuplicateKeys_NotAllowedInBlock(t *testing.T) {
src := `key = "value1"
structkey "structname" { name = "test" name = "test2"}
`
p := newParser([]byte(src))
p := newParser([]byte(src), true)

_, err := p.Parse()
if err == nil {
Expand All @@ -170,9 +182,8 @@ func TestDuplicateKeys_NotAllowedInBlock(t *testing.T) {

func TestDuplicateBlocks_allowed(t *testing.T) {
src := `structkey { name = "test" }
structkey { name = "test" }
`
p := newParser([]byte(src))
structkey { name = "test" }`
p := newParser([]byte(src), true)

_, err := p.Parse()
if err != nil {
Expand All @@ -186,7 +197,7 @@ func TestListOfMaps_requiresComma(t *testing.T) {
{key = "bar"}
{key = "baz"}
]`
p := newParser([]byte(src))
p := newParser([]byte(src), true)

_, err := p.Parse()
if err == nil {
Expand Down Expand Up @@ -216,7 +227,7 @@ func TestListType_leadComment(t *testing.T) {
}

for _, l := range literals {
p := newParser([]byte(l.src))
p := newParser([]byte(l.src), true)
item, err := p.objectItem()
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -267,7 +278,7 @@ func TestListType_lineComment(t *testing.T) {
}

for _, l := range literals {
p := newParser([]byte(l.src))
p := newParser([]byte(l.src), true)
item, err := p.objectItem()
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -356,7 +367,7 @@ func TestObjectType(t *testing.T) {
for _, l := range literals {
t.Logf("Source: %s", l.src)

p := newParser([]byte(l.src))
p := newParser([]byte(l.src), true)
// p.enableTrace = true
item, err := p.objectItem()
if err != nil {
Expand Down Expand Up @@ -402,7 +413,7 @@ func TestObjectKey(t *testing.T) {
}

for _, k := range keys {
p := newParser([]byte(k.src))
p := newParser([]byte(k.src), true)
keys, err := p.objectKey()
if err != nil {
t.Fatal(err)
Expand All @@ -426,7 +437,7 @@ func TestObjectKey(t *testing.T) {
}

for _, k := range errKeys {
p := newParser([]byte(k.src))
p := newParser([]byte(k.src), true)
_, err := p.objectKey()
if err == nil {
t.Errorf("case '%s' should give an error", k.src)
Expand All @@ -445,7 +456,7 @@ func TestCommentGroup(t *testing.T) {

for _, tc := range cases {
t.Run(tc.src, func(t *testing.T) {
p := newParser([]byte(tc.src))
p := newParser([]byte(tc.src), true)
file, err := p.Parse()
if err != nil {
t.Fatalf("parse error: %s", err)
Expand Down
11 changes: 7 additions & 4 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@ import (
//
// Input can be either JSON or HCL
func ParseBytes(in []byte) (*ast.File, error) {
return parse(in)
return parse(in, true)
}

// ParseString accepts input as a string and returns ast tree.
func ParseString(input string) (*ast.File, error) {
return parse([]byte(input))
return parse([]byte(input), true)
}

func parse(in []byte) (*ast.File, error) {
func parse(in []byte, errorOnDuplicateKeys bool) (*ast.File, error) {
switch lexMode(in) {
case lexModeHcl:
if !errorOnDuplicateKeys {
return hclParser.ParseDontErrorOnDuplicateKeys(in)
}
return hclParser.Parse(in)
case lexModeJson:
return jsonParser.Parse(in)
Expand All @@ -35,5 +38,5 @@ func parse(in []byte) (*ast.File, error) {
//
// The input format can be either HCL or JSON.
func Parse(input string) (*ast.File, error) {
return parse([]byte(input))
return parse([]byte(input), true)
}
2 changes: 2 additions & 0 deletions test-fixtures/basic_duplicates.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
foo = "bar"
foo = "${file("bing/bong.txt")}"