hclhil: interface to treat HIL templates as expressions
This commit is contained in:
parent
fde586e193
commit
e4fdbb6b15
@ -49,3 +49,21 @@ func ParseFile(filename string) (*zcl.File, zcl.Diagnostics) {
|
||||
|
||||
return Parse(src, filename)
|
||||
}
|
||||
|
||||
// ParseTemplate attempts to parse the given buffer as a HIL template and,
|
||||
// if successful, returns a zcl.Expression for the value represented by it.
|
||||
//
|
||||
// The returned file is valid only if the returned diagnostics returns false
|
||||
// from its HasErrors method. If HasErrors returns true, the file represents
|
||||
// the subset of data that was able to be parsed, which may be none.
|
||||
func ParseTemplate(src []byte, filename string) (zcl.Expression, zcl.Diagnostics) {
|
||||
return parseTemplate(src, filename, zcl.Pos{Line: 1, Column: 1})
|
||||
}
|
||||
|
||||
// ParseTemplateEmbedded is like ParseTemplate but is for templates that are
|
||||
// embedded in a file in another language. Practically-speaking this just
|
||||
// offsets the source positions returned in diagnostics, etc to be relative
|
||||
// to the given position.
|
||||
func ParseTemplateEmbedded(src []byte, filename string, startPos zcl.Pos) (zcl.Expression, zcl.Diagnostics) {
|
||||
return parseTemplate(src, filename, startPos)
|
||||
}
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"github.com/apparentlymart/go-zcl/zcl"
|
||||
hclparser "github.com/hashicorp/hcl/hcl/parser"
|
||||
hcltoken "github.com/hashicorp/hcl/hcl/token"
|
||||
hilast "github.com/hashicorp/hil/ast"
|
||||
hilparser "github.com/hashicorp/hil/parser"
|
||||
)
|
||||
|
||||
// errorRange attempts to extract a source range from the given error,
|
||||
@ -12,12 +14,16 @@ import (
|
||||
// errorRange understands HCL's "PosError" type, which wraps an error
|
||||
// with a source position.
|
||||
func errorRange(err error) *zcl.Range {
|
||||
if perr, ok := err.(*hclparser.PosError); ok {
|
||||
rng := rangeFromHCLPos(perr.Pos)
|
||||
switch terr := err.(type) {
|
||||
case *hclparser.PosError:
|
||||
rng := rangeFromHCLPos(terr.Pos)
|
||||
return &rng
|
||||
case *hilparser.ParseError:
|
||||
rng := rangeFromHILPos(terr.Pos)
|
||||
return &rng
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func rangeFromHCLPos(pos hcltoken.Pos) zcl.Range {
|
||||
@ -37,3 +43,25 @@ func rangeFromHCLPos(pos hcltoken.Pos) zcl.Range {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func rangeFromHILPos(pos hilast.Pos) zcl.Range {
|
||||
// HIL only marks single positions rather than ranges, so we adapt this
|
||||
// by creating a single-character range at the given position.
|
||||
// HIL also doesn't track byte offsets, so we will hard-code these to
|
||||
// zero so that no position can be considered to be "inside" these
|
||||
// from a byte offset perspective.
|
||||
return zcl.Range{
|
||||
Filename: pos.Filename,
|
||||
Start: zcl.Pos{
|
||||
Byte: 0,
|
||||
Line: pos.Line,
|
||||
Column: pos.Column,
|
||||
},
|
||||
End: zcl.Pos{
|
||||
Byte: 0,
|
||||
Line: pos.Line,
|
||||
Column: pos.Column + 1,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
397
zcl/hclhil/template.go
Normal file
397
zcl/hclhil/template.go
Normal file
@ -0,0 +1,397 @@
|
||||
package hclhil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/apparentlymart/go-cty/cty"
|
||||
"github.com/apparentlymart/go-cty/cty/function"
|
||||
"github.com/apparentlymart/go-zcl/zcl"
|
||||
"github.com/hashicorp/hil"
|
||||
hilast "github.com/hashicorp/hil/ast"
|
||||
)
|
||||
|
||||
func parseTemplate(src []byte, filename string, startPos zcl.Pos) (zcl.Expression, zcl.Diagnostics) {
|
||||
hilStartPos := hilast.Pos{
|
||||
Filename: filename,
|
||||
Line: startPos.Line,
|
||||
Column: startPos.Column,
|
||||
// HIL positions don't have byte offsets, so we ignore startPos.Byte here
|
||||
}
|
||||
rootNode, err := hil.ParseWithPosition(string(src), hilStartPos)
|
||||
|
||||
if err != nil {
|
||||
return nil, zcl.Diagnostics{
|
||||
{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Syntax error in template",
|
||||
Detail: fmt.Sprintf("The template could not be parsed: %s", err),
|
||||
Subject: errorRange(err),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &templateExpression{
|
||||
node: rootNode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type templateExpression struct {
|
||||
node hilast.Node
|
||||
}
|
||||
|
||||
func (e *templateExpression) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
|
||||
cfg := hilEvalConfig(ctx)
|
||||
return ctyValueFromHILNode(e.node, cfg)
|
||||
}
|
||||
|
||||
func (e *templateExpression) Range() zcl.Range {
|
||||
return rangeFromHILPos(e.node.Pos())
|
||||
}
|
||||
func (e *templateExpression) StartRange() zcl.Range {
|
||||
return rangeFromHILPos(e.node.Pos())
|
||||
}
|
||||
|
||||
func hilEvalConfig(ctx *zcl.EvalContext) *hil.EvalConfig {
|
||||
cfg := &hil.EvalConfig{
|
||||
GlobalScope: &hilast.BasicScope{
|
||||
VarMap: map[string]hilast.Variable{},
|
||||
FuncMap: map[string]hilast.Function{},
|
||||
},
|
||||
}
|
||||
|
||||
if ctx == nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
if ctx.Variables != nil {
|
||||
for name, val := range ctx.Variables {
|
||||
cfg.GlobalScope.VarMap[name] = hilVariableForInput(hilVariableFromCtyValue(val))
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Functions != nil {
|
||||
for name, hf := range ctx.Functions {
|
||||
cfg.GlobalScope.FuncMap[name] = hilFunctionFromCtyFunction(hf)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func ctyValueFromHILNode(node hilast.Node, cfg *hil.EvalConfig) (cty.Value, zcl.Diagnostics) {
|
||||
result, err := hil.Eval(node, cfg)
|
||||
if err != nil {
|
||||
return cty.DynamicVal, zcl.Diagnostics{
|
||||
{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Template evaluation failed",
|
||||
Detail: fmt.Sprintf("Error while evaluating template: %s", err),
|
||||
Subject: rangeFromHILPos(node.Pos()).Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return ctyValueFromHILResult(result), nil
|
||||
}
|
||||
|
||||
func ctyValueFromHILResult(result hil.EvaluationResult) cty.Value {
|
||||
switch result.Type {
|
||||
case hil.TypeString:
|
||||
return cty.StringVal(result.Value.(string))
|
||||
case hil.TypeBool:
|
||||
return cty.BoolVal(result.Value.(bool))
|
||||
case hil.TypeList:
|
||||
varsI := result.Value.([]interface{})
|
||||
if len(varsI) == 0 {
|
||||
return cty.ListValEmpty(cty.String)
|
||||
}
|
||||
vals := make([]cty.Value, len(varsI))
|
||||
for i, varI := range varsI {
|
||||
hv, err := hil.InterfaceToVariable(varI)
|
||||
if err != nil {
|
||||
panic("HIL returned type that can't be converted back to variable")
|
||||
}
|
||||
vals[i] = ctyValueFromHILVariable(hv)
|
||||
}
|
||||
return cty.TupleVal(vals)
|
||||
case hil.TypeMap:
|
||||
varsI := result.Value.(map[string]interface{})
|
||||
if len(varsI) == 0 {
|
||||
return cty.MapValEmpty(cty.String)
|
||||
}
|
||||
vals := make(map[string]cty.Value)
|
||||
for key, varI := range varsI {
|
||||
hv, err := hil.InterfaceToVariable(varI)
|
||||
if err != nil {
|
||||
panic("HIL returned type that can't be converted back to variable")
|
||||
}
|
||||
vals[key] = ctyValueFromHILVariable(hv)
|
||||
}
|
||||
return cty.ObjectVal(vals)
|
||||
case hil.TypeUnknown:
|
||||
// HIL doesn't have typed unknowns, so we have to return dynamic
|
||||
return cty.DynamicVal
|
||||
default:
|
||||
// should never happen
|
||||
panic(fmt.Sprintf("unsupported EvaluationResult type %s", result.Type))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func ctyValueFromHILVariable(vr hilast.Variable) cty.Value {
|
||||
switch vr.Type {
|
||||
case hilast.TypeBool:
|
||||
return cty.BoolVal(vr.Value.(bool))
|
||||
case hilast.TypeString:
|
||||
return cty.StringVal(vr.Value.(string))
|
||||
case hilast.TypeInt:
|
||||
return cty.NumberIntVal(vr.Value.(int64))
|
||||
case hilast.TypeFloat:
|
||||
return cty.NumberFloatVal(vr.Value.(float64))
|
||||
case hilast.TypeList:
|
||||
vars := vr.Value.([]hilast.Variable)
|
||||
if len(vars) == 0 {
|
||||
return cty.ListValEmpty(cty.String)
|
||||
}
|
||||
vals := make([]cty.Value, len(vars))
|
||||
for i, v := range vars {
|
||||
vals[i] = ctyValueFromHILVariable(v)
|
||||
}
|
||||
return cty.TupleVal(vals)
|
||||
case hilast.TypeMap:
|
||||
vars := vr.Value.(map[string]hilast.Variable)
|
||||
if len(vars) == 0 {
|
||||
return cty.MapValEmpty(cty.String)
|
||||
}
|
||||
vals := make(map[string]cty.Value)
|
||||
for key, v := range vars {
|
||||
vals[key] = ctyValueFromHILVariable(v)
|
||||
}
|
||||
return cty.ObjectVal(vals)
|
||||
case hilast.TypeAny, hilast.TypeUnknown:
|
||||
return cty.DynamicVal
|
||||
default:
|
||||
// should never happen
|
||||
panic(fmt.Sprintf("unsupported HIL Variable type %s", vr.Type))
|
||||
}
|
||||
}
|
||||
|
||||
func hilVariableFromCtyValue(val cty.Value) hilast.Variable {
|
||||
if !val.IsKnown() {
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeUnknown,
|
||||
Value: hil.UnknownValue,
|
||||
}
|
||||
}
|
||||
if val.IsNull() {
|
||||
// HIL doesn't actually support nulls, so we'll cheat a bit and
|
||||
// use an unknown. This is not quite right since nulls are supposed
|
||||
// to fail when evaluated, but it should suffice as a compatibility
|
||||
// shim since HIL-using applications probably won't be generating
|
||||
// nulls anyway.
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeUnknown,
|
||||
Value: hil.UnknownValue,
|
||||
}
|
||||
}
|
||||
|
||||
ty := val.Type()
|
||||
switch ty {
|
||||
case cty.String:
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeString,
|
||||
Value: val.AsString(),
|
||||
}
|
||||
case cty.Number:
|
||||
// cty doesn't distinguish between floats and ints, so we'll
|
||||
// just always use floats here and depend on automatic conversions
|
||||
// to produce ints where needed.
|
||||
bf := val.AsBigFloat()
|
||||
f, _ := bf.Float64()
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeFloat,
|
||||
Value: f,
|
||||
}
|
||||
case cty.Bool:
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeBool,
|
||||
Value: val.True(),
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
|
||||
// HIL doesn't have sets, so we'll just turn them into lists
|
||||
// HIL doesn't support tuples either, so any tuples without consistent
|
||||
// element types will fail HIL's check for consistent types, but that's
|
||||
// okay since we don't intend to change HIL semantics here.
|
||||
vars := []hilast.Variable{}
|
||||
it := val.ElementIterator()
|
||||
for it.Next() {
|
||||
_, ev := it.Element()
|
||||
vars = append(vars, hilVariableFromCtyValue(ev))
|
||||
}
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeList,
|
||||
Value: vars,
|
||||
}
|
||||
case ty.IsMapType():
|
||||
vars := map[string]hilast.Variable{}
|
||||
it := val.ElementIterator()
|
||||
for it.Next() {
|
||||
kv, ev := it.Element()
|
||||
k := kv.AsString()
|
||||
vars[k] = hilVariableFromCtyValue(ev)
|
||||
}
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeMap,
|
||||
Value: vars,
|
||||
}
|
||||
case ty.IsObjectType():
|
||||
// HIL doesn't support objects, so objects that don't have consistent
|
||||
// attribute types will fail HIL's check for consistent types. That's
|
||||
// okay since we don't intend to change HIL semantics here.
|
||||
vars := map[string]interface{}{}
|
||||
atys := ty.AttributeTypes()
|
||||
for k := range atys {
|
||||
vars[k] = hilVariableFromCtyValue(val.GetAttr(k))
|
||||
}
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeMap,
|
||||
Value: vars,
|
||||
}
|
||||
case ty.IsCapsuleType():
|
||||
// Can't do anything reasonable with capsule types, so we'll just
|
||||
// treat them as unknown and let the caller deal with it as an error.
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeUnknown,
|
||||
Value: hil.UnknownValue,
|
||||
}
|
||||
default:
|
||||
// Should never happen if we've done our job right here
|
||||
panic(fmt.Sprintf("don't know how to convert %#v into a HIL variable", ty))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// hilVariableForInput constrains the given variable to be of the types HIL
|
||||
// accepts as input, which entails converting all primitive types to string.
|
||||
func hilVariableForInput(v hilast.Variable) hilast.Variable {
|
||||
switch v.Type {
|
||||
case hilast.TypeFloat:
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeString,
|
||||
Value: strconv.FormatFloat(v.Value.(float64), 'f', -1, 64),
|
||||
}
|
||||
case hilast.TypeBool:
|
||||
if v.Value.(bool) {
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeString,
|
||||
Value: "true",
|
||||
}
|
||||
} else {
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeString,
|
||||
Value: "false",
|
||||
}
|
||||
}
|
||||
case hilast.TypeList:
|
||||
inVars := v.Value.([]hilast.Variable)
|
||||
outVars := make([]hilast.Variable, len(inVars))
|
||||
for i, inVar := range inVars {
|
||||
outVars[i] = hilVariableForInput(inVar)
|
||||
}
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeList,
|
||||
Value: outVars,
|
||||
}
|
||||
case hilast.TypeMap:
|
||||
inVars := v.Value.(map[string]hilast.Variable)
|
||||
outVars := make(map[string]hilast.Variable)
|
||||
for k, inVar := range inVars {
|
||||
outVars[k] = hilVariableForInput(inVar)
|
||||
}
|
||||
return hilast.Variable{
|
||||
Type: hilast.TypeMap,
|
||||
Value: outVars,
|
||||
}
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func hilTypeFromCtyType(ty cty.Type) hilast.Type {
|
||||
switch ty {
|
||||
case cty.String:
|
||||
return hilast.TypeString
|
||||
case cty.Number:
|
||||
return hilast.TypeFloat
|
||||
case cty.Bool:
|
||||
return hilast.TypeBool
|
||||
case cty.DynamicPseudoType:
|
||||
// Assume we're using this as a type specification, so we'd rather
|
||||
// have TypeAny than TypeUnknown.
|
||||
return hilast.TypeAny
|
||||
}
|
||||
|
||||
switch {
|
||||
case ty.IsListType() || ty.IsSetType() || ty.IsTupleType():
|
||||
return hilast.TypeList
|
||||
case ty.IsMapType(), ty.IsObjectType():
|
||||
return hilast.TypeMap
|
||||
default:
|
||||
return hilast.TypeUnknown
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func hilFunctionFromCtyFunction(f function.Function) hilast.Function {
|
||||
hf := hilast.Function{}
|
||||
params := f.Params()
|
||||
varParam := f.VarParam()
|
||||
|
||||
hf.ArgTypes = make([]hilast.Type, len(params))
|
||||
staticTypes := make([]cty.Type, len(params))
|
||||
for i, param := range params {
|
||||
hf.ArgTypes[i] = hilTypeFromCtyType(param.Type)
|
||||
staticTypes[i] = param.Type
|
||||
}
|
||||
if varParam != nil {
|
||||
hf.Variadic = true
|
||||
hf.VariadicType = hilTypeFromCtyType(varParam.Type)
|
||||
}
|
||||
|
||||
retType, err := f.ReturnType(staticTypes)
|
||||
if err == nil {
|
||||
hf.ReturnType = hilTypeFromCtyType(retType)
|
||||
} else {
|
||||
hf.ReturnType = hilTypeFromCtyType(cty.DynamicPseudoType)
|
||||
}
|
||||
|
||||
hf.Callback = func(hilArgs []interface{}) (interface{}, error) {
|
||||
args := make([]cty.Value, len(hilArgs))
|
||||
for i, hilArg := range hilArgs {
|
||||
var hilType hilast.Type
|
||||
if i < len(hf.ArgTypes) {
|
||||
hilType = hf.ArgTypes[i]
|
||||
} else {
|
||||
hilType = hf.VariadicType
|
||||
}
|
||||
args[i] = ctyValueFromHILVariable(hilast.Variable{
|
||||
Type: hilType,
|
||||
Value: hilArg,
|
||||
})
|
||||
}
|
||||
|
||||
result, err := f.Call(args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hilResult := hilVariableFromCtyValue(result)
|
||||
return hilResult.Value, nil
|
||||
}
|
||||
|
||||
return hf
|
||||
}
|
187
zcl/hclhil/template_test.go
Normal file
187
zcl/hclhil/template_test.go
Normal file
@ -0,0 +1,187 @@
|
||||
package hclhil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/apparentlymart/go-cty/cty"
|
||||
"github.com/apparentlymart/go-cty/cty/function"
|
||||
"github.com/apparentlymart/go-cty/cty/function/stdlib"
|
||||
"github.com/apparentlymart/go-zcl/zcl"
|
||||
)
|
||||
|
||||
func TestTemplateExpression(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
ctx *zcl.EvalContext
|
||||
want cty.Value
|
||||
diagCount int
|
||||
}{
|
||||
{
|
||||
``,
|
||||
nil,
|
||||
cty.StringVal(""),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`hello`,
|
||||
nil,
|
||||
cty.StringVal("hello"),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`hello ${"world"}`,
|
||||
nil,
|
||||
cty.StringVal("hello world"),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`${"hello"}`,
|
||||
nil,
|
||||
cty.StringVal("hello"),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`Hello ${planet}!`,
|
||||
&zcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"planet": cty.StringVal("Earth"),
|
||||
},
|
||||
},
|
||||
cty.StringVal("Hello Earth!"),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`${names}`,
|
||||
&zcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("Ermintrude"),
|
||||
cty.StringVal("Tom"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.StringVal("Ermintrude"),
|
||||
cty.StringVal("Tom"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`${doodads}`,
|
||||
&zcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"doodads": cty.MapVal(map[string]cty.Value{
|
||||
"Captain": cty.StringVal("Ermintrude"),
|
||||
"First Officer": cty.StringVal("Tom"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"Captain": cty.StringVal("Ermintrude"),
|
||||
"First Officer": cty.StringVal("Tom"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`${names}`,
|
||||
&zcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"names": cty.TupleVal([]cty.Value{
|
||||
cty.StringVal("Ermintrude"),
|
||||
cty.NumberIntVal(5),
|
||||
}),
|
||||
},
|
||||
},
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.StringVal("Ermintrude"),
|
||||
cty.StringVal("5"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`${messytuple}`,
|
||||
&zcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"messytuple": cty.TupleVal([]cty.Value{
|
||||
cty.StringVal("Ermintrude"),
|
||||
cty.ListValEmpty(cty.String),
|
||||
}),
|
||||
},
|
||||
},
|
||||
cty.TupleVal([]cty.Value{
|
||||
cty.StringVal("Ermintrude"),
|
||||
cty.ListValEmpty(cty.String), // HIL's sloppy type checker actually lets us get away with this
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`number ${num}`,
|
||||
&zcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"num": cty.NumberIntVal(5),
|
||||
},
|
||||
},
|
||||
cty.StringVal("number 5"),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`${length("hello")}`,
|
||||
&zcl.EvalContext{
|
||||
Functions: map[string]function.Function{
|
||||
"length": stdlib.StrlenFunc,
|
||||
},
|
||||
},
|
||||
cty.StringVal("5"), // HIL always stringifies numbers on output
|
||||
0,
|
||||
},
|
||||
{
|
||||
`${true}`,
|
||||
nil,
|
||||
cty.StringVal("true"), // HIL always stringifies bools on output
|
||||
0,
|
||||
},
|
||||
{
|
||||
`cannot ${names}`,
|
||||
&zcl.EvalContext{
|
||||
Variables: map[string]cty.Value{
|
||||
"names": cty.ListVal([]cty.Value{
|
||||
cty.StringVal("Ermintrude"),
|
||||
cty.StringVal("Tom"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
cty.DynamicVal,
|
||||
1, // can't concatenate a list
|
||||
},
|
||||
{
|
||||
`${syntax error`,
|
||||
nil,
|
||||
cty.NilVal,
|
||||
1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
expr, diags := ParseTemplate([]byte(test.input), "test.hil")
|
||||
if expr != nil {
|
||||
val, valDiags := expr.Value(test.ctx)
|
||||
diags = append(diags, valDiags...)
|
||||
if !val.RawEquals(test.want) {
|
||||
t.Errorf("wrong result\ngot: %#v\nwant: %#v", val, test.want)
|
||||
}
|
||||
} else {
|
||||
if test.want != cty.NilVal {
|
||||
t.Errorf("Unexpected diagnostics during parse: %s", diags.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(diags) != test.diagCount {
|
||||
t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.diagCount)
|
||||
for _, diag := range diags {
|
||||
t.Logf(" - %s", diag.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user