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