From 636e660fac2a9ee31de5346b96e314a9ef6500b0 Mon Sep 17 00:00:00 2001 From: wata_mac Date: Sun, 24 May 2020 21:56:16 +0900 Subject: [PATCH] json: Add ParseExpression function --- json/parser.go | 15 +++++ json/public.go | 14 +++++ json/public_test.go | 135 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+) 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()) + } +}