diff --git a/hcldec/spec.go b/hcldec/spec.go index a70818e..b3cb1f8 100644 --- a/hcldec/spec.go +++ b/hcldec/spec.go @@ -1565,6 +1565,52 @@ func (s *TransformFuncSpec) sourceRange(content *hcl.BodyContent, blockLabels [] return s.Wrapped.sourceRange(content, blockLabels) } +// ValidateFuncSpec is a spec that allows for extended +// developer-defined validation. The validation function receives the +// result of the wrapped spec. +// +// The Subject field of the returned Diagnostic is optional. If not +// specified, it is automatically populated with the range covered by +// the wrapped spec. +// +type ValidateSpec struct { + Wrapped Spec + Func func(value cty.Value) hcl.Diagnostics +} + +func (s *ValidateSpec) visitSameBodyChildren(cb visitFunc) { + cb(s.Wrapped) +} + +func (s *ValidateSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { + wrappedVal, diags := s.Wrapped.decode(content, blockLabels, ctx) + if diags.HasErrors() { + // We won't try to run our function in this case, because it'll probably + // generate confusing additional errors that will distract from the + // root cause. + return cty.UnknownVal(s.impliedType()), diags + } + + validateDiags := s.Func(wrappedVal) + // Auto-populate the Subject fields if they weren't set. + for i := range validateDiags { + if validateDiags[i].Subject == nil { + validateDiags[i].Subject = s.sourceRange(content, blockLabels).Ptr() + } + } + + diags = append(diags, validateDiags...) + return wrappedVal, diags +} + +func (s *ValidateSpec) impliedType() cty.Type { + return s.Wrapped.impliedType() +} + +func (s *ValidateSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { + return s.Wrapped.sourceRange(content, blockLabels) +} + // noopSpec is a placeholder spec that does nothing, used in situations where // a non-nil placeholder spec is required. It is not exported because there is // no reason to use it directly; it is always an implementation detail only. diff --git a/hcldec/spec_test.go b/hcldec/spec_test.go index 64ea771..61d60c9 100644 --- a/hcldec/spec_test.go +++ b/hcldec/spec_test.go @@ -1,6 +1,7 @@ package hcldec import ( + "fmt" "reflect" "testing" @@ -26,6 +27,7 @@ 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) @@ -139,3 +141,69 @@ bar = barval } }) } + +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) + } + }) + } +}