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 current body since they are not evaluated unless all prior specs produce
`null` as their result. `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
Type expressions are used to describe the expected type of an attribute, as 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": case "default":
return decodeDefaultSpec(block.Body) return decodeDefaultSpec(block.Body)
case "transform":
return decodeTransformSpec(block.Body)
case "literal": case "literal":
return decodeLiteralSpec(block.Body) return decodeLiteralSpec(block.Body)
@ -439,6 +442,50 @@ func decodeDefaultSpec(body hcl.Body) (hcldec.Spec, hcl.Diagnostics) {
return spec, diags 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{ var errSpec = &hcldec.LiteralSpec{
Value: cty.NullVal(cty.DynamicPseudoType), Value: cty.NullVal(cty.DynamicPseudoType),
} }
@ -457,6 +504,7 @@ var specBlockTypes = []string{
"block_set", "block_set",
"default", "default",
"transform",
} }
var specSchemaUnlabelled *hcl.BodySchema var specSchemaUnlabelled *hcl.BodySchema

View File

@ -7,6 +7,7 @@ import (
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert" "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. // 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. // reasonable source range to return anyway.
return s.Primary.sourceRange(content, blockLabels) 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 = (*BlockMapSpec)(nil)
var _ Spec = (*BlockLabelSpec)(nil) var _ Spec = (*BlockLabelSpec)(nil)
var _ Spec = (*DefaultSpec)(nil) var _ Spec = (*DefaultSpec)(nil)
var _ Spec = (*TransformExprSpec)(nil)
var _ Spec = (*TransformFuncSpec)(nil)
var _ attrSpec = (*AttrSpec)(nil) var _ attrSpec = (*AttrSpec)(nil)