Skip to content

Commit dca91cc

Browse files
committed
hclwrite: add ObjectConsExpr.SetItemRaw() and ObjectConsExpr.ItemFor()
1 parent 3d0eee9 commit dca91cc

2 files changed

Lines changed: 270 additions & 5 deletions

File tree

hclwrite/ast_object_cons_expr.go

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,78 @@
33

44
package hclwrite
55

6+
import (
7+
"github.com/hashicorp/hcl/v2"
8+
"github.com/hashicorp/hcl/v2/hclsyntax"
9+
"github.com/zclconf/go-cty/cty"
10+
)
11+
612
type ObjectConsExpr struct {
713
inTree
14+
15+
items nodeSet
816
}
917

1018
func newObjectConsExpr() *ObjectConsExpr {
1119
return &ObjectConsExpr{
1220
inTree: newInTree(),
21+
items: newNodeSet(),
22+
}
23+
}
24+
25+
func (object *ObjectConsExpr) firstItemNode() *node {
26+
return object.items.List()[0]
27+
}
28+
29+
// SetItemRaw either replaces the expression of an existing item of the given
30+
// name or adds a new item definition to the end of the object, using the given
31+
// tokens verbatim as the expression.
32+
//
33+
// The same caveats apply to this function as for NewExpressionRaw on which it
34+
// is based. If possible, prefer to use SetItemValue or SetItemTraversal.
35+
func (object *ObjectConsExpr) SetItemRaw(key string, tokens Tokens) (*ObjectConsKey, *ObjectConsValue) {
36+
item := object.ItemFor(key)
37+
expr := NewExpressionRaw(tokens)
38+
if item != nil {
39+
item.valueObj().expr.Detach()
40+
item.valueObj().expr = item.valueObj().children.Append(expr)
41+
} else {
42+
item = newObjectConsItem()
43+
item.init(key, expr)
44+
if firstItemNode := object.firstItemNode(); firstItemNode == nil {
45+
return nil, nil
46+
} else {
47+
object.items.Add(object.children.Insert(firstItemNode, item))
48+
}
1349
}
50+
return item.kv()
51+
}
52+
53+
// SetItemValue either replaces the expression of an existing item of the given
54+
// name or adds a new item definition to the end of the object.
55+
//
56+
// The value is given as a cty.Value, and must therefore be a literal. To set a
57+
// variable reference or other traversal, use SetItemTraversal.
58+
//
59+
// The return value is the item that was either modified in-place or created.
60+
func (object *ObjectConsExpr) SetItemValue(name string, val cty.Value) (*ObjectConsKey, *ObjectConsValue) {
61+
return nil, nil
1462
}
1563

16-
func (o *ObjectConsExpr) ValueFor(key string) *ObjectConsValue {
17-
var found *ObjectConsValue
18-
o.walkChildNodes(func(n *node) {
64+
// SetItemTraversal either replaces the expression of an existing item of the
65+
// given name or adds a new item definition to the end of the object.
66+
//
67+
// The new expression is given as a hcl.Traversal, which must be an absolute
68+
// traversal. To set a literal value, use SetItemValue.
69+
//
70+
// The return value is the item that was either modified in-place or created.
71+
func (object *ObjectConsExpr) SetItemTraversal(name string, traversal hcl.Traversal) (*ObjectConsKey, *ObjectConsValue) {
72+
return nil, nil
73+
}
74+
75+
func (object *ObjectConsExpr) ItemFor(key string) *ObjectConsItem {
76+
var found *ObjectConsItem
77+
object.walkChildNodes(func(n *node) {
1978
if item, ok := n.content.(*ObjectConsItem); ok {
2079
k := item.key.content.(*ObjectConsKey)
2180
name := k.name.content.(*Expression)
@@ -28,14 +87,22 @@ func (o *ObjectConsExpr) ValueFor(key string) *ObjectConsValue {
2887
}
2988

3089
if k.literal && (maybeKey == key || maybeKey == `"`+key+`"`) {
31-
found = item.value.content.(*ObjectConsValue)
90+
found = item
3291
return
3392
}
3493
}
3594
})
3695
return found
3796
}
3897

98+
func (object *ObjectConsExpr) ValueFor(key string) *ObjectConsValue {
99+
if item := object.ItemFor(key); item == nil {
100+
return nil
101+
} else {
102+
return item.value.content.(*ObjectConsValue)
103+
}
104+
}
105+
39106
type ObjectConsItem struct {
40107
inTree
41108
key *node
@@ -48,8 +115,42 @@ func newObjectConsItem() *ObjectConsItem {
48115
}
49116
}
50117

118+
func (item *ObjectConsItem) keyObj() *ObjectConsKey {
119+
return item.key.content.(*ObjectConsKey)
120+
}
121+
122+
func (item *ObjectConsItem) valueObj() *ObjectConsValue {
123+
return item.value.content.(*ObjectConsValue)
124+
}
125+
126+
func (item *ObjectConsItem) init(key string, value *Expression) {
127+
value.assertUnattached()
128+
129+
item.children.AppendUnstructuredTokens(Tokens{
130+
{
131+
Type: hclsyntax.TokenNewline,
132+
Bytes: []byte{'\n'},
133+
},
134+
})
135+
item.key = item.children.Append(newObjectConsKey())
136+
item.keyObj().children.Append(newIdentifier(newIdentToken(key)))
137+
138+
item.children.AppendUnstructuredTokens(Tokens{
139+
{
140+
Type: hclsyntax.TokenEqual,
141+
Bytes: []byte{'='},
142+
},
143+
})
144+
145+
item.value = item.children.Append(newObjectConsValue())
146+
item.valueObj().children.Append(value)
147+
}
148+
51149
func (item *ObjectConsItem) kv() (*ObjectConsKey, *ObjectConsValue) {
52-
return item.key.content.(*ObjectConsKey), item.value.content.(*ObjectConsValue)
150+
key := item.key.content.(*ObjectConsKey)
151+
value := item.value.content.(*ObjectConsValue)
152+
153+
return key, value
53154
}
54155

55156
type ObjectConsKey struct {
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright IBM Corp. 2014, 2026
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package hclwrite
5+
6+
import (
7+
"fmt"
8+
"reflect"
9+
"testing"
10+
11+
"github.com/davecgh/go-spew/spew"
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/hashicorp/hcl/v2"
14+
"github.com/hashicorp/hcl/v2/hclsyntax"
15+
)
16+
17+
func TestObjectConsExprSetItemRaw(t *testing.T) {
18+
tests := []struct {
19+
src string
20+
attrName string
21+
key string
22+
tokens Tokens
23+
want Tokens
24+
}{
25+
{
26+
`a = {
27+
hat = "derby", (cat) = "calico" }` + "\n",
28+
"a",
29+
"hat",
30+
Tokens{
31+
{
32+
Type: hclsyntax.TokenOQuote,
33+
Bytes: []byte(`"`),
34+
SpacesBefore: 1,
35+
},
36+
{
37+
Type: hclsyntax.TokenQuotedLit,
38+
Bytes: []byte(`bowler`),
39+
},
40+
{
41+
Type: hclsyntax.TokenCQuote,
42+
Bytes: []byte(`"`),
43+
},
44+
},
45+
Tokens{
46+
{
47+
Type: hclsyntax.TokenIdent,
48+
Bytes: []byte{'a'},
49+
},
50+
{
51+
Type: hclsyntax.TokenEqual,
52+
Bytes: []byte{'='},
53+
SpacesBefore: 1,
54+
},
55+
{
56+
Type: hclsyntax.TokenOBrace,
57+
Bytes: []byte{'{'},
58+
SpacesBefore: 1,
59+
},
60+
{
61+
Type: hclsyntax.TokenNewline,
62+
Bytes: []byte("\n"),
63+
},
64+
{
65+
Type: hclsyntax.TokenIdent,
66+
Bytes: []byte("hat"),
67+
},
68+
{
69+
Type: hclsyntax.TokenEqual,
70+
Bytes: []byte{'='},
71+
SpacesBefore: 1,
72+
},
73+
{
74+
Type: hclsyntax.TokenOQuote,
75+
Bytes: []byte{'"'},
76+
SpacesBefore: 1,
77+
},
78+
{
79+
Type: hclsyntax.TokenQuotedLit,
80+
Bytes: []byte(`bowler`),
81+
},
82+
{
83+
Type: hclsyntax.TokenCQuote,
84+
Bytes: []byte{'"'},
85+
},
86+
{
87+
Type: hclsyntax.TokenComma,
88+
Bytes: []byte{','},
89+
},
90+
{
91+
Type: hclsyntax.TokenOParen,
92+
Bytes: []byte{'('},
93+
SpacesBefore: 1,
94+
},
95+
{
96+
Type: hclsyntax.TokenIdent,
97+
Bytes: []byte("cat"),
98+
},
99+
{
100+
Type: hclsyntax.TokenCParen,
101+
Bytes: []byte{')'},
102+
},
103+
{
104+
Type: hclsyntax.TokenEqual,
105+
Bytes: []byte{'='},
106+
SpacesBefore: 1,
107+
},
108+
{
109+
Type: hclsyntax.TokenOQuote,
110+
Bytes: []byte{'"'},
111+
SpacesBefore: 1,
112+
},
113+
{
114+
Type: hclsyntax.TokenQuotedLit,
115+
Bytes: []byte("calico"),
116+
},
117+
{
118+
Type: hclsyntax.TokenCQuote,
119+
Bytes: []byte{'"'},
120+
},
121+
{
122+
Type: hclsyntax.TokenCBrace,
123+
Bytes: []byte{'}'},
124+
SpacesBefore: 1,
125+
},
126+
{
127+
Type: hclsyntax.TokenNewline,
128+
Bytes: []byte("\n"),
129+
},
130+
{
131+
Type: hclsyntax.TokenEOF,
132+
Bytes: []byte{},
133+
SpacesBefore: 0,
134+
},
135+
},
136+
},
137+
}
138+
139+
for _, test := range tests {
140+
t.Run(fmt.Sprintf("%s = %s in %s", test.attrName, test.tokens.Bytes(), test.src), func(t *testing.T) {
141+
f, diags := ParseConfig([]byte(test.src), "", hcl.Pos{Line: 1, Column: 1})
142+
if len(diags) != 0 {
143+
for _, diag := range diags {
144+
t.Logf("- %s", diag.Error())
145+
}
146+
t.Fatalf("unexpected diagnostics")
147+
}
148+
149+
attr := f.Body().GetAttribute(test.attrName)
150+
if attr == nil {
151+
t.Fatal("attr nil")
152+
}
153+
expr := attr.Expr().AsObjectConsExpr()
154+
expr.SetItemRaw(test.key, test.tokens)
155+
156+
got := f.BuildTokens(nil)
157+
format(got)
158+
if !reflect.DeepEqual(got, test.want) {
159+
diff := cmp.Diff(test.want, got)
160+
t.Errorf("wrong result\ngot: %s\nwant: %s\ndiff:\n%s", spew.Sdump(got), spew.Sdump(test.want), diff)
161+
}
162+
})
163+
}
164+
}

0 commit comments

Comments
 (0)