398 lines
10 KiB
Go
398 lines
10 KiB
Go
|
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
|
||
|
}
|