Skip to content

Commit 1a9a6ec

Browse files
committed
json: Add ParseExpression function
1 parent 90676d4 commit 1a9a6ec

3 files changed

Lines changed: 164 additions & 0 deletions

File tree

json/parser.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ func parseFileContent(buf []byte, filename string, start hcl.Pos) (node, hcl.Dia
2323
return node, diags
2424
}
2525

26+
func parseExpression(buf []byte, filename string, start hcl.Pos) (node, hcl.Diagnostics) {
27+
tokens := scan(buf, pos{Filename: filename, Pos: start})
28+
p := newPeeker(tokens)
29+
node, diags := parseValue(p)
30+
if len(diags) == 0 && p.Peek().Type != tokenEOF {
31+
diags = diags.Append(&hcl.Diagnostic{
32+
Severity: hcl.DiagError,
33+
Summary: "Extraneous data after value",
34+
Detail: "Extra characters appear after the JSON value.",
35+
Subject: p.Peek().Range.Ptr(),
36+
})
37+
}
38+
return node, diags
39+
}
40+
2641
func parseValue(p *peeker) (node, hcl.Diagnostics) {
2742
tok := p.Peek()
2843

json/public.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,20 @@ func ParseWithStartPos(src []byte, filename string, start hcl.Pos) (*hcl.File, h
7171
return file, diags
7272
}
7373

74+
// ParseExpression parses the given buffer as a standalone JSON expression,
75+
// returning it as an instance of Expression.
76+
func ParseExpression(src []byte, filename string) (hcl.Expression, hcl.Diagnostics) {
77+
return ParseExpressionWithStartPos(src, filename, hcl.Pos{Byte: 0, Line: 1, Column: 1})
78+
}
79+
80+
// ParseExpressionWithStartPos parses like json.ParseExpression, but unlike
81+
// json.ParseExpression you can pass a start position of the given JSON
82+
// expression as a hcl.Pos.
83+
func ParseExpressionWithStartPos(src []byte, filename string, start hcl.Pos) (hcl.Expression, hcl.Diagnostics) {
84+
node, diags := parseExpression(src, filename, start)
85+
return &expression{src: node}, diags
86+
}
87+
7488
// ParseFile is a convenience wrapper around Parse that first attempts to load
7589
// data from the given filename, passing the result to Parse if successful.
7690
//

json/public_test.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package json
22

33
import (
4+
"fmt"
45
"strings"
56
"testing"
67

@@ -182,3 +183,137 @@ func TestParseWithStartPos(t *testing.T) {
182183
t.Errorf("The two ranges did not match: src=%s, part=%s", srcRange, partRange)
183184
}
184185
}
186+
187+
func TestParseExpression(t *testing.T) {
188+
tests := []struct {
189+
Input string
190+
Want string
191+
}{
192+
{
193+
`"hello"`,
194+
`cty.StringVal("hello")`,
195+
},
196+
{
197+
`"hello ${noun}"`,
198+
`cty.StringVal("hello world")`,
199+
},
200+
{
201+
"true",
202+
"cty.True",
203+
},
204+
{
205+
"false",
206+
"cty.False",
207+
},
208+
{
209+
"1",
210+
"cty.NumberIntVal(1)",
211+
},
212+
{
213+
"{}",
214+
"cty.EmptyObjectVal",
215+
},
216+
{
217+
`{"foo":"bar","baz":1}`,
218+
`cty.ObjectVal(map[string]cty.Value{"baz":cty.NumberIntVal(1), "foo":cty.StringVal("bar")})`,
219+
},
220+
{
221+
"[]",
222+
"cty.EmptyTupleVal",
223+
},
224+
{
225+
`["1",2,3]`,
226+
`cty.TupleVal([]cty.Value{cty.StringVal("1"), cty.NumberIntVal(2), cty.NumberIntVal(3)})`,
227+
},
228+
}
229+
230+
for _, test := range tests {
231+
t.Run(test.Input, func(t *testing.T) {
232+
expr, diags := ParseExpression([]byte(test.Input), "")
233+
if diags.HasErrors() {
234+
t.Errorf("got %d diagnostics; want 0", len(diags))
235+
for _, d := range diags {
236+
t.Logf(" - %s", d.Error())
237+
}
238+
}
239+
240+
value, diags := expr.Value(&hcl.EvalContext{
241+
Variables: map[string]cty.Value{
242+
"noun": cty.StringVal("world"),
243+
},
244+
})
245+
if diags.HasErrors() {
246+
t.Errorf("got %d diagnostics on decode value; want 0", len(diags))
247+
for _, d := range diags {
248+
t.Logf(" - %s", d.Error())
249+
}
250+
}
251+
got := fmt.Sprintf("%#v", value)
252+
253+
if got != test.Want {
254+
t.Errorf("got %s, but want %s", got, test.Want)
255+
}
256+
})
257+
}
258+
}
259+
260+
func TestParseExpression_malformed(t *testing.T) {
261+
src := `invalid`
262+
expr, diags := ParseExpression([]byte(src), "")
263+
if got, want := len(diags), 1; got != want {
264+
t.Errorf("got %d diagnostics; want %d", got, want)
265+
}
266+
if err, want := diags.Error(), `Invalid JSON keyword`; !strings.Contains(err, want) {
267+
t.Errorf("diags are %q, but should contain %q", err, want)
268+
}
269+
if expr == nil {
270+
t.Errorf("got nil Expression; want actual expression")
271+
}
272+
}
273+
274+
func TestParseExpressionWithStartPos(t *testing.T) {
275+
src := `{
276+
"foo": "bar"
277+
}`
278+
part := `"bar"`
279+
280+
file, diags := Parse([]byte(src), "")
281+
partExpr, partDiags := ParseExpressionWithStartPos([]byte(part), "", hcl.Pos{Byte: 0, Line: 2, Column: 10})
282+
if len(diags) != 0 {
283+
t.Errorf("got %d diagnostics on parse src; want 0", len(diags))
284+
for _, diag := range diags {
285+
t.Logf("- %s", diag.Error())
286+
}
287+
}
288+
if len(partDiags) != 0 {
289+
t.Errorf("got %d diagnostics on parse part src; want 0", len(partDiags))
290+
for _, diag := range partDiags {
291+
t.Logf("- %s", diag.Error())
292+
}
293+
}
294+
295+
if file == nil {
296+
t.Errorf("got nil File; want actual file")
297+
}
298+
if file.Body == nil {
299+
t.Errorf("got nil Body: want actual body")
300+
}
301+
if partExpr == nil {
302+
t.Errorf("got nil Expression; want actual expression")
303+
}
304+
305+
content, diags := file.Body.Content(&hcl.BodySchema{
306+
Attributes: []hcl.AttributeSchema{{Name: "foo"}},
307+
})
308+
if len(diags) != 0 {
309+
t.Errorf("got %d diagnostics on decode; want 0", len(diags))
310+
for _, diag := range diags {
311+
t.Logf("- %s", diag.Error())
312+
}
313+
}
314+
expr := content.Attributes["foo"].Expr
315+
316+
if expr.Range().String() != partExpr.Range().String() {
317+
t.Errorf("The two ranges did not match: src=%s, part=%s", expr.Range(), partExpr.Range())
318+
}
319+
}

0 commit comments

Comments
 (0)