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:
parent
e4fdbb6b15
commit
2cfc08c632
@ -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
|
||||
}
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user