Skip to content

Commit 919ba77

Browse files
authored
Merge pull request #387 from hashicorp/hcldec-add-validatefuncspec
hcldec: add ValidateSpec
2 parents 92a27b7 + 9c4784b commit 919ba77

2 files changed

Lines changed: 114 additions & 0 deletions

File tree

hcldec/spec.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,6 +1565,52 @@ func (s *TransformFuncSpec) sourceRange(content *hcl.BodyContent, blockLabels []
15651565
return s.Wrapped.sourceRange(content, blockLabels)
15661566
}
15671567

1568+
// ValidateFuncSpec is a spec that allows for extended
1569+
// developer-defined validation. The validation function receives the
1570+
// result of the wrapped spec.
1571+
//
1572+
// The Subject field of the returned Diagnostic is optional. If not
1573+
// specified, it is automatically populated with the range covered by
1574+
// the wrapped spec.
1575+
//
1576+
type ValidateSpec struct {
1577+
Wrapped Spec
1578+
Func func(value cty.Value) hcl.Diagnostics
1579+
}
1580+
1581+
func (s *ValidateSpec) visitSameBodyChildren(cb visitFunc) {
1582+
cb(s.Wrapped)
1583+
}
1584+
1585+
func (s *ValidateSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
1586+
wrappedVal, diags := s.Wrapped.decode(content, blockLabels, ctx)
1587+
if diags.HasErrors() {
1588+
// We won't try to run our function in this case, because it'll probably
1589+
// generate confusing additional errors that will distract from the
1590+
// root cause.
1591+
return cty.UnknownVal(s.impliedType()), diags
1592+
}
1593+
1594+
validateDiags := s.Func(wrappedVal)
1595+
// Auto-populate the Subject fields if they weren't set.
1596+
for i := range validateDiags {
1597+
if validateDiags[i].Subject == nil {
1598+
validateDiags[i].Subject = s.sourceRange(content, blockLabels).Ptr()
1599+
}
1600+
}
1601+
1602+
diags = append(diags, validateDiags...)
1603+
return wrappedVal, diags
1604+
}
1605+
1606+
func (s *ValidateSpec) impliedType() cty.Type {
1607+
return s.Wrapped.impliedType()
1608+
}
1609+
1610+
func (s *ValidateSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
1611+
return s.Wrapped.sourceRange(content, blockLabels)
1612+
}
1613+
15681614
// noopSpec is a placeholder spec that does nothing, used in situations where
15691615
// a non-nil placeholder spec is required. It is not exported because there is
15701616
// no reason to use it directly; it is always an implementation detail only.

hcldec/spec_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package hcldec
22

33
import (
4+
"fmt"
45
"reflect"
56
"testing"
67

@@ -26,6 +27,7 @@ var _ Spec = (*BlockLabelSpec)(nil)
2627
var _ Spec = (*DefaultSpec)(nil)
2728
var _ Spec = (*TransformExprSpec)(nil)
2829
var _ Spec = (*TransformFuncSpec)(nil)
30+
var _ Spec = (*ValidateSpec)(nil)
2931

3032
var _ attrSpec = (*AttrSpec)(nil)
3133
var _ attrSpec = (*DefaultSpec)(nil)
@@ -139,3 +141,69 @@ bar = barval
139141
}
140142
})
141143
}
144+
145+
func TestValidateFuncSpec(t *testing.T) {
146+
config := `
147+
foo = "invalid"
148+
`
149+
f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1})
150+
if diags.HasErrors() {
151+
t.Fatal(diags.Error())
152+
}
153+
154+
expectRange := map[string]*hcl.Range{
155+
"without_range": nil,
156+
"with_range": &hcl.Range{
157+
Filename: "foobar",
158+
Start: hcl.Pos{Line: 99, Column: 99},
159+
End: hcl.Pos{Line: 999, Column: 999},
160+
},
161+
}
162+
163+
for name := range expectRange {
164+
t.Run(name, func(t *testing.T) {
165+
spec := &ValidateSpec{
166+
Wrapped: &AttrSpec{
167+
Name: "foo",
168+
Type: cty.String,
169+
},
170+
Func: func(value cty.Value) hcl.Diagnostics {
171+
if value.AsString() != "invalid" {
172+
return hcl.Diagnostics{
173+
&hcl.Diagnostic{
174+
Severity: hcl.DiagError,
175+
Summary: "incorrect value",
176+
Detail: fmt.Sprintf("invalid value passed in: %s", value.GoString()),
177+
},
178+
}
179+
}
180+
181+
return hcl.Diagnostics{
182+
&hcl.Diagnostic{
183+
Severity: hcl.DiagWarning,
184+
Summary: "OK",
185+
Detail: "validation called correctly",
186+
Subject: expectRange[name],
187+
},
188+
}
189+
},
190+
}
191+
192+
_, diags = Decode(f.Body, spec, nil)
193+
if len(diags) != 1 ||
194+
diags[0].Severity != hcl.DiagWarning ||
195+
diags[0].Summary != "OK" ||
196+
diags[0].Detail != "validation called correctly" {
197+
t.Fatalf("unexpected diagnostics: %s", diags.Error())
198+
}
199+
200+
if expectRange[name] == nil && diags[0].Subject == nil {
201+
t.Fatal("returned diagnostic subject missing")
202+
}
203+
204+
if expectRange[name] != nil && !reflect.DeepEqual(expectRange[name], diags[0].Subject) {
205+
t.Fatalf("expected range %s, got range %s", expectRange[name], diags[0].Subject)
206+
}
207+
})
208+
}
209+
}

0 commit comments

Comments
 (0)