hcl/zcl/hclhil/template.go

451 lines
11 KiB
Go

package hclhil
import (
"fmt"
"strconv"
"strings"
"github.com/hashicorp/hcl2/zcl"
"github.com/hashicorp/hil"
hilast "github.com/hashicorp/hil/ast"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
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) Variables() []zcl.Traversal {
var vars []zcl.Traversal
e.node.Accept(func(n hilast.Node) hilast.Node {
vn, ok := n.(*hilast.VariableAccess)
if !ok {
return n
}
rawName := vn.Name
parts := strings.Split(rawName, ".")
if len(parts) == 0 {
return n
}
tr := make(zcl.Traversal, 0, len(parts))
tr = append(tr, zcl.TraverseRoot{
Name: parts[0],
SrcRange: rangeFromHILPos(n.Pos()),
})
for _, name := range parts {
if nv, err := strconv.Atoi(name); err == nil {
// Turn this into a sequence index in zcl land, to save
// callers from having to understand both HIL-style numeric
// attributes and zcl-style indices.
tr = append(tr, zcl.TraverseIndex{
Key: cty.NumberIntVal(int64(nv)),
SrcRange: rangeFromHILPos(n.Pos()),
})
continue
}
if name == "*" {
// TODO: support splat traversals, but that requires some
// more work here because we need to then accumulate the
// rest of the parts into the splat's own "Each" traversal.
continue
}
tr = append(tr, zcl.TraverseAttr{
Name: name,
SrcRange: rangeFromHILPos(n.Pos()),
})
}
vars = append(vars, tr)
return n
})
return vars
}
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
}