json: Allow strings to be treated as HIL templates

Eventually zcl will have its own native template format that we'll use
by default, but for applications migrating from HCL/HIL we can instead
parse strings as HIL templates, for compatibility with how JSON configs
would've been processed in a HCL/HIL app.

When this mode is not enabled, we still just treat strings as literals,
pending the implementation of the zcl template parser.
This commit is contained in:
Martin Atkins 2017-05-21 22:34:23 -07:00
parent e4fdbb6b15
commit 2cfc08c632
4 changed files with 122 additions and 9 deletions

View File

@ -88,3 +88,28 @@ func ParseFile(filename string) (*zcl.File, zcl.Diagnostics) {
return Parse(src, filename)
}
// ParseWithHIL is like Parse except the returned file will use the HIL
// template syntax for expressions in strings, rather than the native zcl
// template syntax.
//
// This is intended for providing backward compatibility for applications that
// used to use HCL/HIL and thus had a JSON-based format with HIL
// interpolations.
func ParseWithHIL(src []byte, filename string) (*zcl.File, zcl.Diagnostics) {
file, diags := Parse(src, filename)
if file != nil && file.Body != nil {
file.Body.(*body).useHIL = true
}
return file, diags
}
// ParseFileWithHIL is like ParseWithHIL but it reads data from a file before
// parsing it.
func ParseFileWithHIL(filename string) (*zcl.File, zcl.Diagnostics) {
file, diags := ParseFile(filename)
if file != nil && file.Body != nil {
file.Body.(*body).useHIL = true
}
return file, diags
}

View File

@ -2,6 +2,9 @@ package json
import (
"testing"
"github.com/apparentlymart/go-cty/cty"
"github.com/apparentlymart/go-zcl/zcl"
)
func TestParse_nonObject(t *testing.T) {
@ -20,3 +23,30 @@ func TestParse_nonObject(t *testing.T) {
t.Errorf("got nil Body object; want placeholder object")
}
}
func TestParseTemplateWithHIL(t *testing.T) {
src := `{"greeting": "hello ${\"world\"}"}`
file, diags := ParseWithHIL([]byte(src), "")
if len(diags) != 0 {
t.Errorf("got %d diagnostics on parse; want 0", len(diags))
}
if file == nil {
t.Errorf("got nil File; want actual file")
}
if file.Body == nil {
t.Fatalf("got nil Body; want actual body")
}
attrs, diags := file.Body.JustAttributes()
if len(diags) != 0 {
t.Errorf("got %d diagnostics on decode; want 0", len(diags))
}
val, diags := attrs["greeting"].Expr.Value(&zcl.EvalContext{})
if len(diags) != 0 {
t.Errorf("got %d diagnostics on eval; want 0", len(diags))
}
if !val.RawEquals(cty.StringVal("hello world")) {
t.Errorf("wrong result %#v; want %#v", val, cty.StringVal("hello world"))
}
}

View File

@ -5,6 +5,7 @@ import (
"github.com/apparentlymart/go-cty/cty"
"github.com/apparentlymart/go-zcl/zcl"
"github.com/apparentlymart/go-zcl/zcl/hclhil"
)
// body is the implementation of "Body" used for files processed with the JSON
@ -16,12 +17,19 @@ type body struct {
// be treated as non-existing. This is used when Body.PartialContent is
// called, to produce the "remaining content" Body.
hiddenAttrs map[string]struct{}
// If set, string values are turned into expressions using HIL's template
// language, rather than the native zcl language. This is intended to
// allow applications moving from HCL to zcl to continue to parse the
// JSON variant of their config that HCL handled previously.
useHIL bool
}
// expression is the implementation of "Expression" used for files processed
// with the JSON parser.
type expression struct {
src node
src node
useHIL bool
}
func (b *body) Content(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Diagnostics) {
@ -96,7 +104,7 @@ func (b *body) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Bod
}
content.Attributes[attrS.Name] = &zcl.Attribute{
Name: attrS.Name,
Expr: &expression{src: jsonAttr.Value},
Expr: &expression{src: jsonAttr.Value, useHIL: b.useHIL},
Range: zcl.RangeBetween(jsonAttr.NameRange, jsonAttr.Value.Range()),
NameRange: jsonAttr.NameRange,
}
@ -118,6 +126,7 @@ func (b *body) PartialContent(schema *zcl.BodySchema) (*zcl.BodyContent, zcl.Bod
unusedBody := &body{
obj: b.obj,
hiddenAttrs: usedNames,
useHIL: b.useHIL,
}
return content, unusedBody, diags
@ -133,7 +142,7 @@ func (b *body) JustAttributes() (zcl.Attributes, zcl.Diagnostics) {
}
attrs[name] = &zcl.Attribute{
Name: name,
Expr: &expression{src: jsonAttr.Value},
Expr: &expression{src: jsonAttr.Value, useHIL: b.useHIL},
Range: zcl.RangeBetween(jsonAttr.NameRange, jsonAttr.Value.Range()),
NameRange: jsonAttr.NameRange,
}
@ -197,7 +206,8 @@ func (b *body) unpackBlock(v node, typeName string, typeRange *zcl.Range, labels
Type: typeName,
Labels: labels,
Body: &body{
obj: tv,
obj: tv,
useHIL: b.useHIL,
},
DefRange: tv.OpenRange,
@ -222,7 +232,8 @@ func (b *body) unpackBlock(v node, typeName string, typeRange *zcl.Range, labels
Type: typeName,
Labels: labels,
Body: &body{
obj: ov,
obj: ov,
useHIL: b.useHIL,
},
DefRange: tv.OpenRange,
@ -244,11 +255,32 @@ func (b *body) unpackBlock(v node, typeName string, typeRange *zcl.Range, labels
func (e *expression) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
// TEMP: Since we've not yet implemented the zcl native template language
// parser, for the moment we'll support only literal values here.
// FIXME: Once the template language parser is implemented, parse string
// values as templates and evaluate them.
switch v := e.src.(type) {
case *stringVal:
if e.useHIL && ctx != nil {
// Legacy interface to parse HCL-style JSON with HIL expressions.
templateSrc := v.Value
hilExpr, diags := hclhil.ParseTemplateEmbedded(
[]byte(templateSrc),
v.SrcRange.Filename,
zcl.Pos{
// skip over the opening quote mark
Byte: v.SrcRange.Start.Byte + 1,
Line: v.SrcRange.Start.Line,
Column: v.SrcRange.Start.Column,
},
)
if hilExpr == nil {
return cty.DynamicVal, diags
}
val, evalDiags := hilExpr.Value(ctx)
diags = append(diags, evalDiags...)
return val, diags
}
// FIXME: Once the native zcl template language parser is implemented,
// parse string values as templates and evaluate them.
return cty.StringVal(v.Value), nil
case *numberVal:
return cty.NumberVal(v.Value), nil
@ -257,14 +289,14 @@ func (e *expression) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
case *arrayVal:
vals := []cty.Value{}
for _, jsonVal := range v.Values {
val, _ := (&expression{src: jsonVal}).Value(ctx)
val, _ := (&expression{src: jsonVal, useHIL: e.useHIL}).Value(ctx)
vals = append(vals, val)
}
return cty.TupleVal(vals), nil
case *objectVal:
attrs := map[string]cty.Value{}
for name, jsonAttr := range v.Attrs {
val, _ := (&expression{src: jsonAttr.Value}).Value(ctx)
val, _ := (&expression{src: jsonAttr.Value, useHIL: e.useHIL}).Value(ctx)
attrs[name] = val
}
return cty.ObjectVal(attrs), nil

View File

@ -36,6 +36,20 @@ func (p *Parser) ParseJSON(src []byte, filename string) (*zcl.File, zcl.Diagnost
return file, diags
}
// ParseJSONWithHIL parses the given JSON buffer (which is assumed to have been
// loaded from the given filename) and returns the zcl.File object representing
// it. Unlike ParseJSON, the strings within the file will be interpreted as
// HIL templates rather than native zcl templates.
func (p *Parser) ParseJSONWithHIL(src []byte, filename string) (*zcl.File, zcl.Diagnostics) {
if existing := p.files[filename]; existing != nil {
return existing, nil
}
file, diags := json.ParseWithHIL(src, filename)
p.files[filename] = file
return file, diags
}
// ParseJSONFile reads the given filename and parses it as JSON, similarly to
// ParseJSON. An error diagnostic is returned if the given file cannot be read.
func (p *Parser) ParseJSONFile(filename string) (*zcl.File, zcl.Diagnostics) {
@ -48,6 +62,18 @@ func (p *Parser) ParseJSONFile(filename string) (*zcl.File, zcl.Diagnostics) {
return file, diags
}
// ParseJSONFileWithHIL reads the given filename and parses it as JSON, similarly to
// ParseJSONWithHIL. An error diagnostic is returned if the given file cannot be read.
func (p *Parser) ParseJSONFileWithHIL(filename string) (*zcl.File, zcl.Diagnostics) {
if existing := p.files[filename]; existing != nil {
return existing, nil
}
file, diags := json.ParseFileWithHIL(filename)
p.files[filename] = file
return file, diags
}
// ParseHCLHIL parses the given buffer (which is assumed to have been loaded
// from the given filename) using the HCL and HIL parsers, and returns the
// zcl.File object representing it.