hclhil: Expression.Value implementation

Currently only deals with the literal HCL structure. Later we will also
support HIL parsing and evaluation within strings, to achieve parity with
existing uses of HCL/HIL together. For now, this has parity with uses of
HCL alone, with the exception that float and int values are not
distinguished, because cty does not make this distinction.
This commit is contained in:
Martin Atkins 2017-05-21 18:42:39 -07:00
parent 0b8f6498ff
commit fde586e193
2 changed files with 190 additions and 2 deletions

View File

@ -7,6 +7,7 @@ import (
"github.com/apparentlymart/go-cty/cty"
"github.com/apparentlymart/go-zcl/zcl"
hclast "github.com/hashicorp/hcl/hcl/ast"
hcltoken "github.com/hashicorp/hcl/hcl/token"
)
// body is our implementation of zcl.Body in terms of an HCL ObjectList
@ -306,8 +307,7 @@ type expression struct {
}
func (e *expression) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
// TODO: Implement
return cty.NilVal, nil
return ctyValueFromHCLNode(e.src, ctx)
}
func (e *expression) Range() zcl.Range {
@ -316,3 +316,71 @@ func (e *expression) Range() zcl.Range {
func (e *expression) StartRange() zcl.Range {
return rangeFromHCLPos(e.src.Pos())
}
func ctyValueFromHCLNode(node hclast.Node, ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
switch tn := node.(type) {
case *hclast.LiteralType:
tok := tn.Token
switch tok.Type {
case hcltoken.NUMBER: // means integer, in HCL land
val := tok.Value().(int64)
return cty.NumberIntVal(val), nil
case hcltoken.FLOAT:
val := tok.Value().(float64)
return cty.NumberFloatVal(val), nil
case hcltoken.STRING, hcltoken.HEREDOC:
val := tok.Value().(string)
// TODO: HIL parsing and evaluation, if ctx is non-nil.
return cty.StringVal(val), nil
case hcltoken.BOOL:
val := tok.Value().(bool)
return cty.BoolVal(val), nil
default:
// should never happen
panic(fmt.Sprintf("unsupported HCL literal type %s", tok.Type))
}
case *hclast.ObjectType:
list := tn.List
attrs, diags := (&body{oli: list}).JustAttributes()
if attrs == nil {
return cty.DynamicVal, diags
}
vals := map[string]cty.Value{}
for name, attr := range attrs {
val, valDiags := attr.Expr.Value(ctx)
if len(valDiags) > 0 {
diags = append(diags, valDiags...)
}
if val == cty.NilVal {
// If we skip one attribute then our return type will be
// inconsistent, so we'll prefer to return dynamic to prevent
// any weird downstream type errors.
return cty.DynamicVal, diags
}
vals[name] = val
}
return cty.ObjectVal(vals), diags
case *hclast.ListType:
nodes := tn.List
vals := make([]cty.Value, len(nodes))
var diags zcl.Diagnostics
for i, node := range nodes {
val, valDiags := ctyValueFromHCLNode(node, ctx)
if len(valDiags) > 0 {
diags = append(diags, valDiags...)
}
if val == cty.NilVal {
// If we skip one element then our return type will be
// inconsistent, so we'll prefer to return dynamic to prevent
// any weird downstream type errors.
return cty.DynamicVal, diags
}
vals[i] = val
}
return cty.TupleVal(vals), diags
default:
panic(fmt.Sprintf("unsupported HCL value type %T", tn))
}
}

View File

@ -5,6 +5,7 @@ import (
"reflect"
"testing"
"github.com/apparentlymart/go-cty/cty"
"github.com/apparentlymart/go-zcl/zcl"
"github.com/davecgh/go-spew/spew"
hclast "github.com/hashicorp/hcl/hcl/ast"
@ -377,3 +378,122 @@ func TestBodyJustAttributes(t *testing.T) {
})
}
}
func TestExpressionValue(t *testing.T) {
tests := []struct {
Source string // HCL source assigning a value to attribute "v"
Want cty.Value
DiagCount int
}{
{
`v = 1`,
cty.NumberIntVal(1),
0,
},
{
`v = 1.5`,
cty.NumberFloatVal(1.5),
0,
},
{
`v = "hello"`,
cty.StringVal("hello"),
0,
},
{
`v = <<EOT
heredoc
EOT
`,
cty.StringVal("heredoc\n"),
0,
},
{
`v = true`,
cty.True,
0,
},
{
`v = false`,
cty.False,
0,
},
{
`v = []`,
cty.EmptyTupleVal,
0,
},
{
`v = ["hello", 5, true, 3.4]`,
cty.TupleVal([]cty.Value{
cty.StringVal("hello"),
cty.NumberIntVal(5),
cty.True,
cty.NumberFloatVal(3.4),
}),
0,
},
{
`v = {}`,
cty.EmptyObjectVal,
0,
},
{
`v = {
string = "hello"
int = 5
bool = true
float = 3.4
list = []
object = {}
}`,
cty.ObjectVal(map[string]cty.Value{
"string": cty.StringVal("hello"),
"int": cty.NumberIntVal(5),
"bool": cty.True,
"float": cty.NumberFloatVal(3.4),
"list": cty.EmptyTupleVal,
"object": cty.EmptyObjectVal,
}),
0,
},
{
`v {}`,
cty.EmptyObjectVal,
0, // warns about using block syntax during content extraction, but we ignore that here
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
file, diags := Parse([]byte(test.Source), "test.hcl")
if len(diags) != 0 {
t.Fatalf("diagnostics from parse: %s", diags.Error())
}
content, diags := file.Body.Content(&zcl.BodySchema{
Attributes: []zcl.AttributeSchema{
{
Name: "v",
Required: true,
},
},
})
expr := content.Attributes["v"].Expr
got, diags := expr.Value(nil)
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())
}
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}