github.com/hashicorp/hcl/v2@v2.20.0/integrationtest/hcldec_into_expr_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package integrationtest 5 6 import ( 7 "testing" 8 9 "github.com/google/go-cmp/cmp" 10 "github.com/google/go-cmp/cmp/cmpopts" 11 "github.com/hashicorp/hcl/v2" 12 "github.com/hashicorp/hcl/v2/ext/customdecode" 13 "github.com/hashicorp/hcl/v2/hcldec" 14 "github.com/hashicorp/hcl/v2/hclsyntax" 15 "github.com/zclconf/go-cty/cty" 16 ) 17 18 // TestHCLDecDecodeToExpr tests both hcldec's support for types with custom 19 // expression decoding rules and the two expression capsule types implemented 20 // in ext/customdecode. This mechanism requires cooperation between those 21 // two components and cty in order to work, so it's helpful to exercise it in 22 // an integration test. 23 func TestHCLDecDecodeToExpr(t *testing.T) { 24 // Here we're going to capture the structure of two simple expressions 25 // without immediately evaluating them. 26 const input = ` 27 a = foo 28 b = foo 29 c = "hello" 30 ` 31 // We'll capture "a" directly as an expression, losing its evaluation 32 // context but retaining its structure. We'll capture "b" as a 33 // customdecode.ExpressionClosure, which gives us both the expression 34 // itself and the evaluation context it was originally evaluated in. 35 // We also have "c" here just to make sure we can still decode into a 36 // "normal" type via standard expression evaluation. 37 38 f, diags := hclsyntax.ParseConfig([]byte(input), "", hcl.Pos{Line: 1, Column: 1}) 39 if diags.HasErrors() { 40 t.Fatalf("unexpected problems: %s", diags.Error()) 41 } 42 43 spec := hcldec.ObjectSpec{ 44 "a": &hcldec.AttrSpec{ 45 Name: "a", 46 Type: customdecode.ExpressionType, 47 Required: true, 48 }, 49 "b": &hcldec.AttrSpec{ 50 Name: "b", 51 Type: customdecode.ExpressionClosureType, 52 Required: true, 53 }, 54 "c": &hcldec.AttrSpec{ 55 Name: "c", 56 Type: cty.String, 57 Required: true, 58 }, 59 } 60 ctx := &hcl.EvalContext{ 61 Variables: map[string]cty.Value{ 62 "foo": cty.StringVal("foo value"), 63 }, 64 } 65 objVal, diags := hcldec.Decode(f.Body, spec, ctx) 66 if diags.HasErrors() { 67 t.Fatalf("unexpected problems: %s", diags.Error()) 68 } 69 70 aVal := objVal.GetAttr("a") 71 bVal := objVal.GetAttr("b") 72 cVal := objVal.GetAttr("c") 73 74 if got, want := aVal.Type(), customdecode.ExpressionType; !got.Equals(want) { 75 t.Fatalf("wrong type for 'a'\ngot: %#v\nwant: %#v", got, want) 76 } 77 if got, want := bVal.Type(), customdecode.ExpressionClosureType; !got.Equals(want) { 78 t.Fatalf("wrong type for 'b'\ngot: %#v\nwant: %#v", got, want) 79 } 80 if got, want := cVal.Type(), cty.String; !got.Equals(want) { 81 t.Fatalf("wrong type for 'c'\ngot: %#v\nwant: %#v", got, want) 82 } 83 84 gotAExpr := customdecode.ExpressionFromVal(aVal) 85 wantAExpr := &hclsyntax.ScopeTraversalExpr{ 86 Traversal: hcl.Traversal{ 87 hcl.TraverseRoot{ 88 Name: "foo", 89 SrcRange: hcl.Range{ 90 Start: hcl.Pos{Line: 2, Column: 5, Byte: 5}, 91 End: hcl.Pos{Line: 2, Column: 8, Byte: 8}, 92 }, 93 }, 94 }, 95 SrcRange: hcl.Range{ 96 Start: hcl.Pos{Line: 2, Column: 5, Byte: 5}, 97 End: hcl.Pos{Line: 2, Column: 8, Byte: 8}, 98 }, 99 } 100 if diff := cmp.Diff(wantAExpr, gotAExpr, cmpopts.IgnoreUnexported(hcl.TraverseRoot{})); diff != "" { 101 t.Errorf("wrong expression for a\n%s", diff) 102 } 103 104 bClosure := customdecode.ExpressionClosureFromVal(bVal) 105 gotBVal, diags := bClosure.Value() 106 wantBVal := cty.StringVal("foo value") 107 if diags.HasErrors() { 108 t.Fatalf("unexpected problems: %s", diags.Error()) 109 } 110 if got, want := gotBVal, wantBVal; !want.RawEquals(got) { 111 t.Errorf("wrong 'b' result\ngot: %#v\nwant: %#v", got, want) 112 } 113 114 if got, want := cVal, cty.StringVal("hello"); !want.RawEquals(got) { 115 t.Errorf("wrong 'c'\ngot: %#v\nwant: %#v", got, want) 116 } 117 118 // One additional "trick" we can do with the expression closure is to 119 // evaluate the expression in a _derived_ EvalContext, rather than the 120 // captured one. This could be useful for introducing additional local 121 // variables/functions in a particular context, for example. 122 deriveCtx := bClosure.EvalContext.NewChild() 123 deriveCtx.Variables = map[string]cty.Value{ 124 "foo": cty.StringVal("overridden foo value"), 125 } 126 gotBVal2, diags := bClosure.Expression.Value(deriveCtx) 127 wantBVal2 := cty.StringVal("overridden foo value") 128 if diags.HasErrors() { 129 t.Fatalf("unexpected problems: %s", diags.Error()) 130 } 131 if got, want := gotBVal2, wantBVal2; !want.RawEquals(got) { 132 t.Errorf("wrong 'b' result with derived EvalContext\ngot: %#v\nwant: %#v", got, want) 133 } 134 }