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  }