hclwrite: Allow constructing expressions from raw tokens
We currently have functions for constructing new expressions from either constant values or from traversals, but some surgical updates require producing a more complex expression. In the long run perhaps we'll have some mechanism for constructing valid expressions via a high-level AST-like API, similar to what we already have for structural constructs, but as a simpler first step here we add a mechanism to just write raw tokens directly into an expression, with the caller being responsible for making sure those tokens represent valid HCL expression syntax. Since this new API treats the given tokens as unstructured, the resulting expression can't fully support the whole of the expression API, but it's good enough for writing in complex expressions without disturbing existing content elsewhere in the input file.
This commit is contained in:
parent
56bc00a23c
commit
06985781df
@ -134,6 +134,26 @@ func (b *Body) RemoveBlock(block *Block) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// SetAttributeRaw either replaces the expression of an existing attribute
|
||||
// of the given name or adds a new attribute definition to the end of the block,
|
||||
// using the given tokens verbatim as the expression.
|
||||
//
|
||||
// The same caveats apply to this function as for NewExpressionRaw on which
|
||||
// it is based. If possible, prefer to use SetAttributeValue or
|
||||
// SetAttributeTraversal.
|
||||
func (b *Body) SetAttributeRaw(name string, tokens Tokens) *Attribute {
|
||||
attr := b.GetAttribute(name)
|
||||
expr := NewExpressionRaw(tokens)
|
||||
if attr != nil {
|
||||
attr.expr = attr.expr.ReplaceWith(expr)
|
||||
} else {
|
||||
attr := newAttribute()
|
||||
attr.init(name, expr)
|
||||
b.appendItem(attr)
|
||||
}
|
||||
return attr
|
||||
}
|
||||
|
||||
// SetAttributeValue either replaces the expression of an existing attribute
|
||||
// of the given name or adds a new attribute definition to the end of the block.
|
||||
//
|
||||
|
@ -766,6 +766,170 @@ func TestBodySetAttributeTraversal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodySetAttributeRaw(t *testing.T) {
|
||||
tests := []struct {
|
||||
src string
|
||||
name string
|
||||
tokens Tokens
|
||||
want Tokens
|
||||
}{
|
||||
{
|
||||
"",
|
||||
"a",
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`true`),
|
||||
SpacesBefore: 0,
|
||||
},
|
||||
},
|
||||
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 = 23\n",
|
||||
"a",
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`true`),
|
||||
SpacesBefore: 0,
|
||||
},
|
||||
},
|
||||
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 = 23\n",
|
||||
"a",
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte(`true`),
|
||||
SpacesBefore: 0,
|
||||
},
|
||||
},
|
||||
Tokens{
|
||||
{
|
||||
Type: hclsyntax.TokenIdent,
|
||||
Bytes: []byte{'b'},
|
||||
SpacesBefore: 0,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenEqual,
|
||||
Bytes: []byte{'='},
|
||||
SpacesBefore: 1,
|
||||
},
|
||||
{
|
||||
Type: hclsyntax.TokenNumberLit,
|
||||
Bytes: []byte("23"),
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s = %s in %s", test.name, test.tokens.Bytes(), 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().SetAttributeRaw(test.name, test.tokens)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodySetAttributeValueInBlock(t *testing.T) {
|
||||
src := `service "label1" {
|
||||
attr1 = "val1"
|
||||
|
@ -21,6 +21,29 @@ func newExpression() *Expression {
|
||||
}
|
||||
}
|
||||
|
||||
// NewExpressionRaw constructs an expression containing the given raw tokens.
|
||||
//
|
||||
// There is no automatic validation that the given tokens produce a valid
|
||||
// expression. Callers of thus function must take care to produce invalid
|
||||
// expression tokens. Where possible, use the higher-level functions
|
||||
// NewExpressionLiteral or NewExpressionAbsTraversal instead.
|
||||
//
|
||||
// Because NewExpressionRaw does not interpret the given tokens in any way,
|
||||
// an expression created by NewExpressionRaw will produce an empty result
|
||||
// for calls to its method Variables, even if the given token sequence
|
||||
// contains a subslice that would normally be interpreted as a traversal under
|
||||
// parsing.
|
||||
func NewExpressionRaw(tokens Tokens) *Expression {
|
||||
expr := newExpression()
|
||||
// We copy the tokens here in order to make sure that later mutations
|
||||
// by the caller don't inadvertently cause our expression to become
|
||||
// invalid.
|
||||
copyTokens := make(Tokens, len(tokens))
|
||||
copy(copyTokens, tokens)
|
||||
expr.children.AppendUnstructuredTokens(copyTokens)
|
||||
return expr
|
||||
}
|
||||
|
||||
// NewExpressionLiteral constructs an an expression that represents the given
|
||||
// literal value.
|
||||
//
|
||||
|
Loading…
Reference in New Issue
Block a user