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:
parent
0b8f6498ff
commit
fde586e193
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user