hclwrite: Body.SetAttributeValue

For now, this is the only way to set an attribute, and so attributes can
only be set to literal values.

Later this will be generalized so that this is just a helper wrapper
around a "SetAttribute" method that just uses a given expression, which
then helps by constructing the expression from the value first.
This commit is contained in:
Martin Atkins 2018-08-09 08:44:48 -07:00
parent 77c0b55a59
commit c8c208e083
6 changed files with 309 additions and 6 deletions

View File

@ -2,6 +2,7 @@ package hclwrite
import ( import (
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
) )
@ -15,9 +16,17 @@ type Body struct {
indentLevel int indentLevel int
} }
func (b *Body) appendItem(n *node) { func (b *Body) appendItem(c nodeContent) *node {
b.inTree.children.AppendNode(n) nn := b.children.Append(c)
b.items.Add(n) b.items.Add(nn)
return nn
}
func (b *Body) appendItemNode(nn *node) *node {
nn.assertUnattached()
b.children.AppendNode(nn)
b.items.Add(nn)
return nn
} }
func (b *Body) AppendUnstructuredTokens(ts Tokens) { func (b *Body) AppendUnstructuredTokens(ts Tokens) {
@ -49,7 +58,16 @@ func (b *Body) GetAttribute(name string) *Attribute {
// The return value is the attribute that was either modified in-place or // The return value is the attribute that was either modified in-place or
// created. // created.
func (b *Body) SetAttributeValue(name string, val cty.Value) *Attribute { func (b *Body) SetAttributeValue(name string, val cty.Value) *Attribute {
panic("Body.SetAttributeValue not yet implemented") attr := b.GetAttribute(name)
expr := NewExpressionLiteral(val)
if attr != nil {
attr.expr = attr.expr.ReplaceWith(expr)
} else {
attr := newAttribute()
attr.init(name, expr)
b.appendItem(attr)
}
return attr
} }
// SetAttributeTraversal either replaces the expression of an existing attribute // SetAttributeTraversal either replaces the expression of an existing attribute
@ -73,6 +91,40 @@ type Attribute struct {
lineComments *node lineComments *node
} }
func newAttribute() *Attribute {
return &Attribute{
inTree: newInTree(),
}
}
func (a *Attribute) init(name string, expr *Expression) {
expr.assertUnattached()
nameTok := newIdentToken(name)
nameObj := newIdentifier(nameTok)
a.leadComments = a.children.Append(newComments(nil))
a.name = a.children.Append(nameObj)
a.children.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
},
})
a.expr = a.children.Append(expr)
a.expr.list = a.children
a.lineComments = a.children.Append(newComments(nil))
a.children.AppendUnstructuredTokens(Tokens{
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
},
})
}
func (a *Attribute) Expr() *Expression {
return a.expr.content.(*Expression)
}
type Block struct { type Block struct {
inTree inTree

View File

@ -6,8 +6,10 @@ import (
"testing" "testing"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/hashicorp/hcl2/hcl/hclsyntax" "github.com/hashicorp/hcl2/hcl/hclsyntax"
"github.com/zclconf/go-cty/cty"
) )
func TestBodyGetAttribute(t *testing.T) { func TestBodyGetAttribute(t *testing.T) {
@ -213,3 +215,200 @@ func TestBodyGetAttribute(t *testing.T) {
}) })
} }
} }
func TestBodySetAttributeValue(t *testing.T) {
tests := []struct {
src string
name string
val cty.Value
want Tokens
}{
{
"",
"a",
cty.True,
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte{'a'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte("true"),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
{
"b = false\n",
"a",
cty.True,
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte{'b'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte("false"),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte{'a'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte("true"),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
{
"a = false\n",
"a",
cty.True,
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte{'a'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte("true"),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEOF,
Bytes: []byte{},
SpacesBefore: 0,
},
},
},
{
"a = 1\nb = false\n",
"a",
cty.True,
Tokens{
{
Type: hclsyntax.TokenIdent,
Bytes: []byte{'a'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte("true"),
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenNewline,
Bytes: []byte{'\n'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte{'b'},
SpacesBefore: 0,
},
{
Type: hclsyntax.TokenEqual,
Bytes: []byte{'='},
SpacesBefore: 1,
},
{
Type: hclsyntax.TokenIdent,
Bytes: []byte("false"),
SpacesBefore: 1,
},
{
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.name, test.val, 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().SetAttributeValue(test.name, test.val)
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)
}
})
}
}

View File

@ -20,8 +20,22 @@ func newExpression() *Expression {
// NewExpressionLiteral constructs an an expression that represents the given // NewExpressionLiteral constructs an an expression that represents the given
// literal value. // literal value.
//
// Since an unknown value cannot be represented in source code, this function
// will panic if the given value is unknown or contains a nested unknown value.
// Use val.IsWhollyKnown before calling to be sure.
//
// HCL native syntax does not directly represent lists, maps, and sets, and
// instead relies on the automatic conversions to those collection types from
// either list or tuple constructor syntax. Therefore converting collection
// values to source code and re-reading them will lose type information, and
// the reader must provide a suitable type at decode time to recover the
// original value.
func NewExpressionLiteral(val cty.Value) *Expression { func NewExpressionLiteral(val cty.Value) *Expression {
panic("NewExpressionLiteral not yet implemented") toks := TokensForValue(val)
expr := newExpression()
expr.children.AppendUnstructuredTokens(toks)
return expr
} }
// NewExpressionAbsTraversal constructs an expression that represents the // NewExpressionAbsTraversal constructs an expression that represents the

View File

@ -51,6 +51,35 @@ func (n *node) Detach() {
n.after = nil n.after = nil
} }
// ReplaceWith removes the receiver from the list it currently belongs to and
// inserts a new node with the given content in its place. If the node is not
// currently in a list, this function will panic.
//
// The return value is the newly-constructed node, containing the given content.
// After this function returns, the reciever is no longer attached to a list.
func (n *node) ReplaceWith(c nodeContent) *node {
if n.list == nil {
panic("can't replace node that is not in a list")
}
before := n.before
after := n.after
list := n.list
n.before, n.after, n.list = nil, nil, nil
nn := newNode(c)
nn.before = before
nn.after = after
nn.list = list
if before != nil {
before.after = nn
}
if after != nil {
after.before = nn
}
return nn
}
func (n *node) assertUnattached() { func (n *node) assertUnattached() {
if n.list != nil { if n.list != nil {
panic(fmt.Sprintf("attempt to attach already-attached node %#v", n)) panic(fmt.Sprintf("attempt to attach already-attached node %#v", n))
@ -80,6 +109,7 @@ func (ns *nodes) Append(c nodeContent) *node {
content: c, content: c,
} }
ns.AppendNode(n) ns.AppendNode(n)
n.list = ns
return n return n
} }
@ -101,6 +131,7 @@ func (ns *nodes) AppendUnstructuredTokens(tokens Tokens) *node {
} }
n := newNode(tokens) n := newNode(tokens)
ns.AppendNode(n) ns.AppendNode(n)
n.list = ns
return n return n
} }

View File

@ -197,7 +197,7 @@ func parseBody(nativeBody *hclsyntax.Body, from inputTokens) (inputTokens, *node
if beforeItem.Len() > 0 { if beforeItem.Len() > 0 {
body.AppendUnstructuredTokens(beforeItem.Tokens()) body.AppendUnstructuredTokens(beforeItem.Tokens())
} }
body.appendItem(item) body.appendItemNode(item)
remain = afterItem remain = afterItem
} }

View File

@ -95,3 +95,10 @@ func (ts Tokens) walkChildNodes(w internalWalkFunc) {
func (ts Tokens) BuildTokens(to Tokens) Tokens { func (ts Tokens) BuildTokens(to Tokens) Tokens {
return append(to, ts...) return append(to, ts...)
} }
func newIdentToken(name string) *Token {
return &Token{
Type: hclsyntax.TokenIdent,
Bytes: []byte(name),
}
}