cmd/hcldec: "transform" spec type

This new spec type allows evaluating an arbitrary expression on the
result of a nested spec, for situations where the a value must be
transformed in some way.
This commit is contained in:
Martin Atkins 2018-02-04 09:59:20 -08:00
parent f65a097d17
commit 1ba92ee170
4 changed files with 193 additions and 0 deletions

View File

@ -305,6 +305,31 @@ their usual behavior but are not able to impose validation constraints on the
current body since they are not evaluated unless all prior specs produce
`null` as their result.
## `transform` spec blocks
The `transform` spec type evaluates one nested spec and then evaluates a given
expression with that nested spec result to produce a final value.
It creates no validation constraints of its own, but passes on the validation
constraints from its nested block.
```hcl
transform {
attr {
name = "size_in_mb"
type = number
}
# Convert result to a size in bytes
result = nested * 1024 * 1024
}
```
`transform` spec blocks accept the following argument:
* `result` (required) - The expression to evaluate on the result of the nested
spec. The variable `nested` is defined when evaluating this expression, with
the result value of the nested spec.
## Type Expressions
Type expressions are used to describe the expected type of an attribute, as

View File

@ -85,6 +85,9 @@ func decodeSpecBlock(block *hcl.Block) (hcldec.Spec, hcl.Diagnostics) {
case "default":
return decodeDefaultSpec(block.Body)
case "transform":
return decodeTransformSpec(block.Body)
case "literal":
return decodeLiteralSpec(block.Body)
@ -439,6 +442,50 @@ func decodeDefaultSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
return spec, diags
}
func decodeTransformSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
type content struct {
Result hcl.Expression `hcl:"result"`
Nested hcl.Body `hcl:",remain"`
}
var args content
diags := gohcl.DecodeBody(body, nil, &args)
if diags.HasErrors() {
return errSpec, diags
}
spec := &hcldec.TransformExprSpec{
Expr: args.Result,
VarName: "nested",
}
nestedContent, nestedDiags := args.Nested.Content(specSchemaUnlabelled)
diags = append(diags, nestedDiags...)
if len(nestedContent.Blocks) != 1 {
if nestedDiags.HasErrors() {
// If we already have errors then they probably explain
// why we have the wrong number of blocks, so we'll skip our
// additional error message added below.
return errSpec, diags
}
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid transform spec",
Detail: "A transform spec block must have exactly one nested spec block.",
Subject: body.MissingItemRange().Ptr(),
})
return errSpec, diags
}
nestedSpec, nestedDiags := decodeSpecBlock(nestedContent.Blocks[0])
diags = append(diags, nestedDiags...)
spec.Wrapped = nestedSpec
return spec, diags
}
var errSpec = &hcldec.LiteralSpec{
Value: cty.NullVal(cty.DynamicPseudoType),
}
@ -457,6 +504,7 @@ var specBlockTypes = []string{
"block_set",
"default",
"transform",
}
var specSchemaUnlabelled *hcl.BodySchema

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
)
// A Spec is a description of how to decode a hcl.Body to a cty.Value.
@ -878,3 +879,120 @@ func (s *DefaultSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockL
// reasonable source range to return anyway.
return s.Primary.sourceRange(content, blockLabels)
}
// TransformExprSpec is a spec that wraps another and then evaluates a given
// hcl.Expression on the result.
//
// The implied type of this spec is determined by evaluating the expression
// with an unknown value of the nested spec's implied type, which may cause
// the result to be imprecise. This spec should not be used in situations where
// precise result type information is needed.
type TransformExprSpec struct {
Wrapped Spec
Expr hcl.Expression
TransformCtx *hcl.EvalContext
VarName string
}
func (s *TransformExprSpec) visitSameBodyChildren(cb visitFunc) {
cb(s.Wrapped)
}
func (s *TransformExprSpec) 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
}
chiCtx := s.TransformCtx.NewChild()
chiCtx.Variables = map[string]cty.Value{
s.VarName: wrappedVal,
}
resultVal, resultDiags := s.Expr.Value(chiCtx)
diags = append(diags, resultDiags...)
return resultVal, diags
}
func (s *TransformExprSpec) impliedType() cty.Type {
wrappedTy := s.Wrapped.impliedType()
chiCtx := s.TransformCtx.NewChild()
chiCtx.Variables = map[string]cty.Value{
s.VarName: cty.UnknownVal(wrappedTy),
}
resultVal, _ := s.Expr.Value(chiCtx)
return resultVal.Type()
}
func (s *TransformExprSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// We'll just pass through our wrapped range here, even though that's
// not super-accurate, because there's nothing better to return.
return s.Wrapped.sourceRange(content, blockLabels)
}
// TransformFuncSpec is a spec that wraps another and then evaluates a given
// cty function with the result. The given function must expect exactly one
// argument, where the result of the wrapped spec will be passed.
//
// The implied type of this spec is determined by type-checking the function
// with an unknown value of the nested spec's implied type, which may cause
// the result to be imprecise. This spec should not be used in situations where
// precise result type information is needed.
//
// If the given function produces an error when run, this spec will produce
// a non-user-actionable diagnostic message. It's the caller's responsibility
// to ensure that the given function cannot fail for any non-error result
// of the wrapped spec.
type TransformFuncSpec struct {
Wrapped Spec
Func function.Function
}
func (s *TransformFuncSpec) visitSameBodyChildren(cb visitFunc) {
cb(s.Wrapped)
}
func (s *TransformFuncSpec) 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
}
resultVal, err := s.Func.Call([]cty.Value{wrappedVal})
if err != nil {
// This is not a good example of a diagnostic because it is reporting
// a programming error in the calling application, rather than something
// an end-user could act on.
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Transform function failed",
Detail: fmt.Sprintf("Decoder transform returned an error: %s", err),
Subject: s.sourceRange(content, blockLabels).Ptr(),
})
return cty.UnknownVal(s.impliedType()), diags
}
return resultVal, diags
}
func (s *TransformFuncSpec) impliedType() cty.Type {
wrappedTy := s.Wrapped.impliedType()
resultTy, err := s.Func.ReturnType([]cty.Type{wrappedTy})
if err != nil {
// Should never happen with a correctly-configured spec
return cty.DynamicPseudoType
}
return resultTy
}
func (s *TransformFuncSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// We'll just pass through our wrapped range here, even though that's
// not super-accurate, because there's nothing better to return.
return s.Wrapped.sourceRange(content, blockLabels)
}

View File

@ -12,6 +12,8 @@ var _ Spec = (*BlockSetSpec)(nil)
var _ Spec = (*BlockMapSpec)(nil)
var _ Spec = (*BlockLabelSpec)(nil)
var _ Spec = (*DefaultSpec)(nil)
var _ Spec = (*TransformExprSpec)(nil)
var _ Spec = (*TransformFuncSpec)(nil)
var _ attrSpec = (*AttrSpec)(nil)