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:
parent
f65a097d17
commit
1ba92ee170
@ -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
|
||||
|
@ -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
|
||||
|
118
hcldec/spec.go
118
hcldec/spec.go
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user