9c4784b144
This adds ValidateSpec, a new decoder Spec that allows one to add custom validations to work with values at decode-time. The validation is run on the value after the wrapped spec is applied to the expression in question. Diagnostics are expected to be returned, with the author having flexibility over whether or not they want to specify a range; if one is not supplied, the range of the wrapped expression is used.
210 lines
5.0 KiB
Go
210 lines
5.0 KiB
Go
package hcldec
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/apparentlymart/go-dump/dump"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/hashicorp/hcl/v2/hclsyntax"
|
|
)
|
|
|
|
// Verify that all of our spec types implement the necessary interfaces
|
|
var _ Spec = ObjectSpec(nil)
|
|
var _ Spec = TupleSpec(nil)
|
|
var _ Spec = (*AttrSpec)(nil)
|
|
var _ Spec = (*LiteralSpec)(nil)
|
|
var _ Spec = (*ExprSpec)(nil)
|
|
var _ Spec = (*BlockSpec)(nil)
|
|
var _ Spec = (*BlockListSpec)(nil)
|
|
var _ Spec = (*BlockSetSpec)(nil)
|
|
var _ Spec = (*BlockMapSpec)(nil)
|
|
var _ Spec = (*BlockAttrsSpec)(nil)
|
|
var _ Spec = (*BlockLabelSpec)(nil)
|
|
var _ Spec = (*DefaultSpec)(nil)
|
|
var _ Spec = (*TransformExprSpec)(nil)
|
|
var _ Spec = (*TransformFuncSpec)(nil)
|
|
var _ Spec = (*ValidateSpec)(nil)
|
|
|
|
var _ attrSpec = (*AttrSpec)(nil)
|
|
var _ attrSpec = (*DefaultSpec)(nil)
|
|
|
|
var _ blockSpec = (*BlockSpec)(nil)
|
|
var _ blockSpec = (*BlockListSpec)(nil)
|
|
var _ blockSpec = (*BlockSetSpec)(nil)
|
|
var _ blockSpec = (*BlockMapSpec)(nil)
|
|
var _ blockSpec = (*BlockAttrsSpec)(nil)
|
|
var _ blockSpec = (*DefaultSpec)(nil)
|
|
|
|
var _ specNeedingVariables = (*AttrSpec)(nil)
|
|
var _ specNeedingVariables = (*BlockSpec)(nil)
|
|
var _ specNeedingVariables = (*BlockListSpec)(nil)
|
|
var _ specNeedingVariables = (*BlockSetSpec)(nil)
|
|
var _ specNeedingVariables = (*BlockMapSpec)(nil)
|
|
var _ specNeedingVariables = (*BlockAttrsSpec)(nil)
|
|
|
|
func TestDefaultSpec(t *testing.T) {
|
|
config := `
|
|
foo = fooval
|
|
bar = barval
|
|
`
|
|
f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1})
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Error())
|
|
}
|
|
|
|
t.Run("primary set", func(t *testing.T) {
|
|
spec := &DefaultSpec{
|
|
Primary: &AttrSpec{
|
|
Name: "foo",
|
|
Type: cty.String,
|
|
},
|
|
Default: &AttrSpec{
|
|
Name: "bar",
|
|
Type: cty.String,
|
|
},
|
|
}
|
|
|
|
gotVars := Variables(f.Body, spec)
|
|
wantVars := []hcl.Traversal{
|
|
{
|
|
hcl.TraverseRoot{
|
|
Name: "fooval",
|
|
SrcRange: hcl.Range{
|
|
Filename: "",
|
|
Start: hcl.Pos{Line: 2, Column: 7, Byte: 7},
|
|
End: hcl.Pos{Line: 2, Column: 13, Byte: 13},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
hcl.TraverseRoot{
|
|
Name: "barval",
|
|
SrcRange: hcl.Range{
|
|
Filename: "",
|
|
Start: hcl.Pos{Line: 3, Column: 7, Byte: 20},
|
|
End: hcl.Pos{Line: 3, Column: 13, Byte: 26},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if !reflect.DeepEqual(gotVars, wantVars) {
|
|
t.Errorf("wrong Variables result\ngot: %s\nwant: %s", dump.Value(gotVars), dump.Value(wantVars))
|
|
}
|
|
|
|
ctx := &hcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"fooval": cty.StringVal("foo value"),
|
|
"barval": cty.StringVal("bar value"),
|
|
},
|
|
}
|
|
|
|
got, err := Decode(f.Body, spec, ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := cty.StringVal("foo value")
|
|
if !got.RawEquals(want) {
|
|
t.Errorf("wrong Decode result\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("primary not set", func(t *testing.T) {
|
|
spec := &DefaultSpec{
|
|
Primary: &AttrSpec{
|
|
Name: "foo",
|
|
Type: cty.String,
|
|
},
|
|
Default: &AttrSpec{
|
|
Name: "bar",
|
|
Type: cty.String,
|
|
},
|
|
}
|
|
|
|
ctx := &hcl.EvalContext{
|
|
Variables: map[string]cty.Value{
|
|
"fooval": cty.NullVal(cty.String),
|
|
"barval": cty.StringVal("bar value"),
|
|
},
|
|
}
|
|
|
|
got, err := Decode(f.Body, spec, ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := cty.StringVal("bar value")
|
|
if !got.RawEquals(want) {
|
|
t.Errorf("wrong Decode result\ngot: %#v\nwant: %#v", got, want)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestValidateFuncSpec(t *testing.T) {
|
|
config := `
|
|
foo = "invalid"
|
|
`
|
|
f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1})
|
|
if diags.HasErrors() {
|
|
t.Fatal(diags.Error())
|
|
}
|
|
|
|
expectRange := map[string]*hcl.Range{
|
|
"without_range": nil,
|
|
"with_range": &hcl.Range{
|
|
Filename: "foobar",
|
|
Start: hcl.Pos{Line: 99, Column: 99},
|
|
End: hcl.Pos{Line: 999, Column: 999},
|
|
},
|
|
}
|
|
|
|
for name := range expectRange {
|
|
t.Run(name, func(t *testing.T) {
|
|
spec := &ValidateSpec{
|
|
Wrapped: &AttrSpec{
|
|
Name: "foo",
|
|
Type: cty.String,
|
|
},
|
|
Func: func(value cty.Value) hcl.Diagnostics {
|
|
if value.AsString() != "invalid" {
|
|
return hcl.Diagnostics{
|
|
&hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "incorrect value",
|
|
Detail: fmt.Sprintf("invalid value passed in: %s", value.GoString()),
|
|
},
|
|
}
|
|
}
|
|
|
|
return hcl.Diagnostics{
|
|
&hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "OK",
|
|
Detail: "validation called correctly",
|
|
Subject: expectRange[name],
|
|
},
|
|
}
|
|
},
|
|
}
|
|
|
|
_, diags = Decode(f.Body, spec, nil)
|
|
if len(diags) != 1 ||
|
|
diags[0].Severity != hcl.DiagWarning ||
|
|
diags[0].Summary != "OK" ||
|
|
diags[0].Detail != "validation called correctly" {
|
|
t.Fatalf("unexpected diagnostics: %s", diags.Error())
|
|
}
|
|
|
|
if expectRange[name] == nil && diags[0].Subject == nil {
|
|
t.Fatal("returned diagnostic subject missing")
|
|
}
|
|
|
|
if expectRange[name] != nil && !reflect.DeepEqual(expectRange[name], diags[0].Subject) {
|
|
t.Fatalf("expected range %s, got range %s", expectRange[name], diags[0].Subject)
|
|
}
|
|
})
|
|
}
|
|
}
|