From 57c6d75eb8cfbed1806eb1ff60e66beffb0a2c4b Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Sat, 3 Nov 2018 09:21:31 -0700 Subject: [PATCH] hclwrite: Body.AppendBlock This method allows a caller to generate a nested block within a body. Since a nested block has its own content body, this now allows for deep structures to be generated. --- hclwrite/ast_block.go | 55 +++++++ hclwrite/ast_body.go | 34 ++++- hclwrite/ast_body_test.go | 311 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 399 insertions(+), 1 deletion(-) diff --git a/hclwrite/ast_block.go b/hclwrite/ast_block.go index ba6fa17..e12d20b 100644 --- a/hclwrite/ast_block.go +++ b/hclwrite/ast_block.go @@ -1,5 +1,10 @@ package hclwrite +import ( + "github.com/hashicorp/hcl2/hcl/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + type Block struct { inTree @@ -10,3 +15,53 @@ type Block struct { body *node close *node } + +func newBlock() *Block { + return &Block{ + inTree: newInTree(), + labels: newNodeSet(), + } +} + +func (b *Block) init(typeName string, labels []string) { + nameTok := newIdentToken(typeName) + nameObj := newIdentifier(nameTok) + b.leadComments = b.children.Append(newComments(nil)) + b.typeName = b.children.Append(nameObj) + for _, label := range labels { + labelToks := TokensForValue(cty.StringVal(label)) + labelObj := newQuoted(labelToks) + labelNode := b.children.Append(labelObj) + b.labels.Add(labelNode) + } + b.open = b.children.AppendUnstructuredTokens(Tokens{ + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + }, + }) + body := newBody() // initially totally empty; caller can append to it subsequently + b.body = b.children.Append(body) + b.close = b.children.AppendUnstructuredTokens(Tokens{ + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + }, + }) +} + +// Body returns the body that represents the content of the receiving block. +// +// Appending to or otherwise modifying this body will make changes to the +// tokens that are generated between the blocks open and close braces. +func (b *Block) Body() *Body { + return b.body.content.(*Body) +} diff --git a/hclwrite/ast_body.go b/hclwrite/ast_body.go index a3cacd7..ded3761 100644 --- a/hclwrite/ast_body.go +++ b/hclwrite/ast_body.go @@ -2,6 +2,7 @@ package hclwrite import ( "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcl/hclsyntax" "github.com/zclconf/go-cty/cty" ) @@ -11,6 +12,13 @@ type Body struct { items nodeSet } +func newBody() *Body { + return &Body{ + inTree: newInTree(), + items: newNodeSet(), + } +} + func (b *Body) appendItem(c nodeContent) *node { nn := b.children.Append(c) b.items.Add(nn) @@ -66,7 +74,7 @@ func (b *Body) SetAttributeValue(name string, val cty.Value) *Attribute { } // SetAttributeTraversal either replaces the expression of an existing attribute -// of the given name or adds a new attribute definition to the end of the block. +// of the given name or adds a new attribute definition to the end of the body. // // The new expression is given as a hcl.Traversal, which must be an absolute // traversal. To set a literal value, use SetAttributeValue. @@ -76,3 +84,27 @@ func (b *Body) SetAttributeValue(name string, val cty.Value) *Attribute { func (b *Body) SetAttributeTraversal(name string, traversal hcl.Traversal) *Attribute { panic("Body.SetAttributeTraversal not yet implemented") } + +// AppendBlock appends a new nested block to the end of the receiving body. +// +// If blankLine is set, an additional empty line is added before the block +// for separation. Usual HCL style suggests that we group together blocks of +// the same type without intervening blank lines and then put blank lines +// between blocks of different types. In some languages, some different block +// types may be conceptually related and so may still be grouped together. +// It is the caller's responsibility to respect the usual conventions of the +// language being generated. +func (b *Body) AppendBlock(typeName string, labels []string, blankLine bool) *Block { + block := newBlock() + block.init(typeName, labels) + if blankLine { + b.AppendUnstructuredTokens(Tokens{ + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + }, + }) + } + b.appendItem(block) + return block +} diff --git a/hclwrite/ast_body_test.go b/hclwrite/ast_body_test.go index c47d203..bc0aabd 100644 --- a/hclwrite/ast_body_test.go +++ b/hclwrite/ast_body_test.go @@ -413,3 +413,314 @@ func TestBodySetAttributeValue(t *testing.T) { }) } } + +func TestBodyAppendBlock(t *testing.T) { + tests := []struct { + src string + blockType string + labels []string + blank bool + want Tokens + }{ + { + "", + "foo", + nil, + false, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`foo`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + { + "", + "foo", + []string{"bar"}, + false, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`foo`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOQuote, + Bytes: []byte(`"`), + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenQuotedLit, + Bytes: []byte(`bar`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenCQuote, + Bytes: []byte(`"`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + { + "", + "foo", + []string{"bar", "baz"}, + false, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`foo`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOQuote, + Bytes: []byte(`"`), + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenQuotedLit, + Bytes: []byte(`bar`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenCQuote, + Bytes: []byte(`"`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOQuote, + Bytes: []byte(`"`), + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenQuotedLit, + Bytes: []byte(`baz`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenCQuote, + Bytes: []byte(`"`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + { + "bar {}\n", + "foo", + nil, + false, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`bar`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`foo`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + { + "bar_blank_after {}\n", + "foo", + nil, + true, + Tokens{ + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`bar_blank_after`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenIdent, + Bytes: []byte(`foo`), + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenOBrace, + Bytes: []byte{'{'}, + SpacesBefore: 1, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenCBrace, + Bytes: []byte{'}'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenNewline, + Bytes: []byte{'\n'}, + SpacesBefore: 0, + }, + { + Type: hclsyntax.TokenEOF, + Bytes: []byte{}, + SpacesBefore: 0, + }, + }, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s %#v in %s", test.blockType, test.blockType, test.src), func(t *testing.T) { + f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1}) + if len(diags) != 0 { + for _, diag := range diags { + t.Logf("- %s", diag.Error()) + } + t.Fatalf("unexpected diagnostics") + } + + f.Body().AppendBlock(test.blockType, test.labels, test.blank) + got := f.BuildTokens(nil) + format(got) + if !reflect.DeepEqual(got, test.want) { + diff := cmp.Diff(test.want, got) + t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff) + } + }) + } +}