summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorwata_mac <watassbass@gmail.com>2020-05-24 21:56:16 +0900
committerMartin Atkins <mart@degeneration.co.uk>2020-09-04 14:12:01 -0700
commit636e660fac2a9ee31de5346b96e314a9ef6500b0 (patch)
tree039e423e6d5ac093af82e884213b91832c33a96d
parent90676d47a0ce038f02596e2aa5894bf0c57fad56 (diff)
json: Add ParseExpression function
-rw-r--r--json/parser.go15
-rw-r--r--json/public.go14
-rw-r--r--json/public_test.go135
3 files changed, 164 insertions, 0 deletions
diff --git a/json/parser.go b/json/parser.go
index 4e40068..6b7420b 100644
--- a/json/parser.go
+++ b/json/parser.go
@@ -23,6 +23,21 @@ func parseFileContent(buf []byte, filename string, start hcl.Pos) (node, hcl.Dia
return node, diags
}
+func parseExpression(buf []byte, filename string, start hcl.Pos) (node, hcl.Diagnostics) {
+ tokens := scan(buf, pos{Filename: filename, Pos: start})
+ p := newPeeker(tokens)
+ node, diags := parseValue(p)
+ if len(diags) == 0 && p.Peek().Type != tokenEOF {
+ diags = diags.Append(&hcl.Diagnostic{
+ Severity: hcl.DiagError,
+ Summary: "Extraneous data after value",
+ Detail: "Extra characters appear after the JSON value.",
+ Subject: p.Peek().Range.Ptr(),
+ })
+ }
+ return node, diags
+}
+
func parseValue(p *peeker) (node, hcl.Diagnostics) {
tok := p.Peek()
diff --git a/json/public.go b/json/public.go
index d3bc9a0..d1e4faf 100644
--- a/json/public.go
+++ b/json/public.go
@@ -71,6 +71,20 @@ func ParseWithStartPos(src []byte, filename string, start hcl.Pos) (*hcl.File, h
return file, diags
}
+// ParseExpression parses the given buffer as a standalone JSON expression,
+// returning it as an instance of Expression.
+func ParseExpression(src []byte, filename string) (hcl.Expression, hcl.Diagnostics) {
+ return ParseExpressionWithStartPos(src, filename, hcl.Pos{Byte: 0, Line: 1, Column: 1})
+}
+
+// ParseExpressionWithStartPos parses like json.ParseExpression, but unlike
+// json.ParseExpression you can pass a start position of the given JSON
+// expression as a hcl.Pos.
+func ParseExpressionWithStartPos(src []byte, filename string, start hcl.Pos) (hcl.Expression, hcl.Diagnostics) {
+ node, diags := parseExpression(src, filename, start)
+ return &expression{src: node}, diags
+}
+
// ParseFile is a convenience wrapper around Parse that first attempts to load
// data from the given filename, passing the result to Parse if successful.
//
diff --git a/json/public_test.go b/json/public_test.go
index 70944d9..554b165 100644
--- a/json/public_test.go
+++ b/json/public_test.go
@@ -1,6 +1,7 @@
package json
import (
+ "fmt"
"strings"
"testing"
@@ -182,3 +183,137 @@ func TestParseWithStartPos(t *testing.T) {
t.Errorf("The two ranges did not match: src=%s, part=%s", srcRange, partRange)
}
}
+
+func TestParseExpression(t *testing.T) {
+ tests := []struct {
+ Input string
+ Want string
+ }{
+ {
+ `"hello"`,
+ `cty.StringVal("hello")`,
+ },
+ {
+ `"hello ${noun}"`,
+ `cty.StringVal("hello world")`,
+ },
+ {
+ "true",
+ "cty.True",
+ },
+ {
+ "false",
+ "cty.False",
+ },
+ {
+ "1",
+ "cty.NumberIntVal(1)",
+ },
+ {
+ "{}",
+ "cty.EmptyObjectVal",
+ },
+ {
+ `{"foo":"bar","baz":1}`,
+ `cty.ObjectVal(map[string]cty.Value{"baz":cty.NumberIntVal(1), "foo":cty.StringVal("bar")})`,
+ },
+ {
+ "[]",
+ "cty.EmptyTupleVal",
+ },
+ {
+ `["1",2,3]`,
+ `cty.TupleVal([]cty.Value{cty.StringVal("1"), cty.NumberIntVal(2), cty.NumberIntVal(3)})`,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.Input, func(t *testing.T) {
+ expr, diags := ParseExpression([]byte(test.Input), "")
+ if diags.HasErrors() {
+ t.Errorf("got %d diagnostics; want 0", len(diags))
+ for _, d := range diags {
+ t.Logf(" - %s", d.Error())
+ }
+ }
+
+ value, diags := expr.Value(&hcl.EvalContext{
+ Variables: map[string]cty.Value{
+ "noun": cty.StringVal("world"),
+ },
+ })
+ if diags.HasErrors() {
+ t.Errorf("got %d diagnostics on decode value; want 0", len(diags))
+ for _, d := range diags {
+ t.Logf(" - %s", d.Error())
+ }
+ }
+ got := fmt.Sprintf("%#v", value)
+
+ if got != test.Want {
+ t.Errorf("got %s, but want %s", got, test.Want)
+ }
+ })
+ }
+}
+
+func TestParseExpression_malformed(t *testing.T) {
+ src := `invalid`
+ expr, diags := ParseExpression([]byte(src), "")
+ if got, want := len(diags), 1; got != want {
+ t.Errorf("got %d diagnostics; want %d", got, want)
+ }
+ if err, want := diags.Error(), `Invalid JSON keyword`; !strings.Contains(err, want) {
+ t.Errorf("diags are %q, but should contain %q", err, want)
+ }
+ if expr == nil {
+ t.Errorf("got nil Expression; want actual expression")
+ }
+}
+
+func TestParseExpressionWithStartPos(t *testing.T) {
+ src := `{
+ "foo": "bar"
+}`
+ part := `"bar"`
+
+ file, diags := Parse([]byte(src), "")
+ partExpr, partDiags := ParseExpressionWithStartPos([]byte(part), "", hcl.Pos{Byte: 0, Line: 2, Column: 10})
+ if len(diags) != 0 {
+ t.Errorf("got %d diagnostics on parse src; want 0", len(diags))
+ for _, diag := range diags {
+ t.Logf("- %s", diag.Error())
+ }
+ }
+ if len(partDiags) != 0 {
+ t.Errorf("got %d diagnostics on parse part src; want 0", len(partDiags))
+ for _, diag := range partDiags {
+ t.Logf("- %s", diag.Error())
+ }
+ }
+
+ if file == nil {
+ t.Errorf("got nil File; want actual file")
+ }
+ if file.Body == nil {
+ t.Errorf("got nil Body: want actual body")
+ }
+ if partExpr == nil {
+ t.Errorf("got nil Expression; want actual expression")
+ }
+
+ content, diags := file.Body.Content(&hcl.BodySchema{
+ Attributes: []hcl.AttributeSchema{{Name: "foo"}},
+ })
+ if len(diags) != 0 {
+ t.Errorf("got %d diagnostics on decode; want 0", len(diags))
+ for _, diag := range diags {
+ t.Logf("- %s", diag.Error())
+ }
+ }
+ expr := content.Attributes["foo"].Expr
+
+ if expr.Range().String() != partExpr.Range().String() {
+ t.Errorf("The two ranges did not match: src=%s, part=%s", expr.Range(), partExpr.Range())
+ }
+}