package customdecode import ( "fmt" "reflect" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" ) // ExpressionType is a cty capsule type that carries hcl.Expression values. // // This type implements custom decoding in the most general way possible: it // just captures whatever expression is given to it, with no further processing // whatsoever. It could therefore be useful in situations where an application // must defer processing of the expression content until a later step. // // ExpressionType only captures the expression, not the evaluation context it // was destined to be evaluated in. That means this type can be fine for // situations where the recipient of the value only intends to do static // analysis, but ExpressionClosureType is more appropriate in situations where // the recipient will eventually evaluate the given expression. var ExpressionType cty.Type // ExpressionVal returns a new cty value of type ExpressionType, wrapping the // given expression. func ExpressionVal(expr hcl.Expression) cty.Value { return cty.CapsuleVal(ExpressionType, &expr) } // ExpressionFromVal returns the expression encapsulated in the given value, or // panics if the value is not a known value of ExpressionType. func ExpressionFromVal(v cty.Value) hcl.Expression { if !v.Type().Equals(ExpressionType) { panic("value is not of ExpressionType") } ptr := v.EncapsulatedValue().(*hcl.Expression) return *ptr } // ExpressionClosureType is a cty capsule type that carries hcl.Expression // values along with their original evaluation contexts. // // This is similar to ExpressionType except that during custom decoding it // also captures the hcl.EvalContext that was provided, allowing callers to // evaluate the expression later in the same context where it would originally // have been evaluated, or a context derived from that one. var ExpressionClosureType cty.Type // ExpressionClosure is the type encapsulated in ExpressionClosureType type ExpressionClosure struct { Expression hcl.Expression EvalContext *hcl.EvalContext } // ExpressionClosureVal returns a new cty value of type ExpressionClosureType, // wrapping the given expression closure. func ExpressionClosureVal(closure *ExpressionClosure) cty.Value { return cty.CapsuleVal(ExpressionClosureType, closure) } // Value evaluates the closure's expression using the closure's EvalContext, // returning the result. func (c *ExpressionClosure) Value() (cty.Value, hcl.Diagnostics) { return c.Expression.Value(c.EvalContext) } // ExpressionClosureFromVal returns the expression closure encapsulated in the // given value, or panics if the value is not a known value of // ExpressionClosureType. // // The caller MUST NOT modify the returned closure or the EvalContext inside // it. To derive a new EvalContext, either create a child context or make // a copy. func ExpressionClosureFromVal(v cty.Value) *ExpressionClosure { if !v.Type().Equals(ExpressionClosureType) { panic("value is not of ExpressionClosureType") } return v.EncapsulatedValue().(*ExpressionClosure) } func init() { // Getting hold of a reflect.Type for hcl.Expression is a bit tricky because // it's an interface type, but we can do it with some indirection. goExpressionType := reflect.TypeOf((*hcl.Expression)(nil)).Elem() ExpressionType = cty.CapsuleWithOps("expression", goExpressionType, &cty.CapsuleOps{ ExtensionData: func(key interface{}) interface{} { switch key { case CustomExpressionDecoder: return CustomExpressionDecoderFunc( func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { return ExpressionVal(expr), nil }, ) default: return nil } }, TypeGoString: func(_ reflect.Type) string { return "customdecode.ExpressionType" }, GoString: func(raw interface{}) string { exprPtr := raw.(*hcl.Expression) return fmt.Sprintf("customdecode.ExpressionVal(%#v)", *exprPtr) }, RawEquals: func(a, b interface{}) bool { aPtr := a.(*hcl.Expression) bPtr := b.(*hcl.Expression) return reflect.DeepEqual(*aPtr, *bPtr) }, }) ExpressionClosureType = cty.CapsuleWithOps("expression closure", reflect.TypeOf(ExpressionClosure{}), &cty.CapsuleOps{ ExtensionData: func(key interface{}) interface{} { switch key { case CustomExpressionDecoder: return CustomExpressionDecoderFunc( func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { return ExpressionClosureVal(&ExpressionClosure{ Expression: expr, EvalContext: ctx, }), nil }, ) default: return nil } }, TypeGoString: func(_ reflect.Type) string { return "customdecode.ExpressionClosureType" }, GoString: func(raw interface{}) string { closure := raw.(*ExpressionClosure) return fmt.Sprintf("customdecode.ExpressionClosureVal(%#v)", closure) }, RawEquals: func(a, b interface{}) bool { closureA := a.(*ExpressionClosure) closureB := b.(*ExpressionClosure) // The expression itself compares by deep equality, but EvalContexts // conventionally compare by pointer identity, so we'll comply // with both conventions here by testing them separately. return closureA.EvalContext == closureB.EvalContext && reflect.DeepEqual(closureA.Expression, closureB.Expression) }, }) }