Repository: avenga/couper
Pull request #391 (2021-11-23 19:33:10)

Fix missing type checks while using index expressions
This PR catches missing handling for index expression handling with different index key types.

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b50920f..2089c6ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,8 @@ Unreleased changes are available as `avenga/couper:edge` container.
   * Reduced memory usage for backend response bodies which just get piped to the client and are not required to be read by Couper due to a variable references ([#375](https://github.com/avenga/couper/pull/375))
     * However, if a huge message body is passed and additionally referenced via e.g. `json_body`, Couper may require a lot of memory for storing the data structure.
   * For each SAML attribute listed in [`array_attributes`](./docs/REFERENCE.md#saml-block) at least an empty array is created in `request.context.<label>.attributes.<name>` ([#369](https://github.com/avenga/couper/pull/369))
-  * Missing support for RelativeTraversalExpr, IndexExpr, UnaryOpExpr ([#389](https://github.com/avenga/couper/pull/389))
+  * HCL: Missing support for RelativeTraversalExpr, IndexExpr, UnaryOpExpr ([#389](https://github.com/avenga/couper/pull/389))
+  * HCL: Missing support for different variable index key types ([#391](https://github.com/avenga/couper/pull/391))
 
 ---
 
diff --git a/eval/value.go b/eval/value.go
index 29a5f1eb..ea439cb9 100644
--- a/eval/value.go
+++ b/eval/value.go
@@ -110,14 +110,15 @@ func newLiteralValueExpr(ctx *hcl.EvalContext, exp hcl.Expression) hclsyntax.Exp
 		return expr
 	case *hclsyntax.TemplateExpr:
 		for p, part := range expr.Parts {
-			for _, v := range part.Variables() {
-				if traversalValue(ctx.Variables, v) == cty.NilVal {
-					expr.Parts[p] = &hclsyntax.LiteralValueExpr{Val: emptyStringVal, SrcRange: v.SourceRange()}
-					break
-				}
-			}
+			expr.Parts[p] = newLiteralValueExpr(ctx, part)
 		}
-		return expr
+
+		// "pre"-evaluate to be able to combine string expressions with empty strings on NilVal result.
+		c, _ := expr.Value(ctx)
+		if c.IsNull() {
+			return &hclsyntax.LiteralValueExpr{Val: emptyStringVal, SrcRange: expr.Range()}
+		}
+		return &hclsyntax.LiteralValueExpr{Val: c, SrcRange: expr.Range()}
 	case *hclsyntax.TemplateWrapExpr:
 		if val := newLiteralValueExpr(ctx, expr.Wrapped); val != nil {
 			expr.Wrapped = val
@@ -319,13 +320,30 @@ func walk(variables, fallback cty.Value, traversal hcl.Traversal) cty.Value {
 		}
 		return walk(current, fallback, traversal[1:])
 	case hcl.TraverseIndex:
-		if variables.HasIndex(t.Key).True() {
+		if !variables.CanIterateElements() {
+			return fallback
+		}
+
+		switch t.Key.Type() {
+		case cty.Number:
+			if variables.HasIndex(t.Key).True() {
+				if hasNext {
+					fidx := t.Key.AsBigFloat()
+					idx, _ := fidx.Int64()
+					return walk(variables.AsValueSlice()[idx], fallback, traversal[1:])
+				}
+				return variables
+			}
+		case cty.String:
+			current, exist := currentFn(t.Key.AsString())
+			if !exist {
+				return fallback
+			}
 			if hasNext {
-				return walk(variables, fallback, traversal[1:])
+				return walk(current, fallback, traversal[1:])
 			}
 			return variables
 		}
-
 		return fallback
 	default:
 		panic("eval: unsupported traversal: " + reflect.TypeOf(t).String())


Test patch:
diff --git a/eval/value_test.go b/eval/value_test.go
index 1347e829..e7b05594 100644
--- a/eval/value_test.go
+++ b/eval/value_test.go
@@ -4,19 +4,20 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/avenga/couper/errors"
-
+	"github.com/hashicorp/hcl/v2"
 	"github.com/hashicorp/hcl/v2/hclsyntax"
+	"github.com/zclconf/go-cty/cty"
 
 	"github.com/avenga/couper/config"
-	"github.com/hashicorp/hcl/v2"
-	"github.com/zclconf/go-cty/cty"
+	"github.com/avenga/couper/errors"
+	"github.com/avenga/couper/internal/seetie"
 )
 
 func TestValue(t *testing.T) {
 	evalCtx := NewContext(nil, &config.Defaults{}).HCLContext()
 	rootObj := cty.ObjectVal(map[string]cty.Value{
 		"exist": cty.StringVal("here"),
+		"slice": seetie.GoToValue([]string{"1", "2"}),
 	})
 	evalCtx.Variables["rootvar"] = rootObj
 
@@ -28,6 +29,12 @@ func TestValue(t *testing.T) {
 	}{
 		{"root non nil", "key = rootvar", rootObj, false},
 		{"child non nil", "key = rootvar.exist", cty.StringVal("here"), false},
+		{"child non nil, string key idx expr", `key = rootvar["exist"]`, cty.StringVal("here"), false},
+		{"child nil, string key idx expr", `key = rootvar["not"]`, cty.NilVal, false},
+		{"child non nil, string key idx expr iterate", `key = rootvar["exist"][1]`, cty.NilVal, false},
+		{"child non nil, number key idx expr", `key = rootvar.slice[1]`, cty.StringVal("2"), false},
+		{"child non nil, number key idx expr iterate", `key = rootvar.slice[1]["not"]`, cty.NilVal, false},
+		{"child non nil, idx nil", `key = rootvar.slice[5]`, cty.NilVal, false},
 		{"child nil", "key = rootvar.child", cty.NilVal, false},
 		{"child idx nil", "key = rootvar.child[2].sub", cty.NilVal, false},
 		{"template attr value exp empty string", `key = "prefix${rootvar.child}"`, cty.StringVal("prefix"), false},
@@ -48,12 +55,12 @@ func TestValue(t *testing.T) {
 			for _, attr := range attrs {
 				got, err := Value(evalCtx, attr.Expr)
 				if (err != nil) != tt.wantErr {
-					t.Errorf("Value() error = %v, wantErr %v", err, tt.wantErr)
-					t.Error(err.(errors.GoError).LogError())
+					st.Errorf("Value() error = %v, wantErr %v", err, tt.wantErr)
+					st.Error(err.(errors.GoError).LogError())
 					return
 				}
 				if !reflect.DeepEqual(got, tt.want) {
-					t.Errorf("Value() got = %v, want %v", got.GoString(), tt.want.GoString())
+					st.Errorf("Value() got = %v, want %v", got.GoString(), tt.want.GoString())
 				}
 			}
 		})
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 7421152c..baf80f2a 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -3584,7 +3584,7 @@ func TestFunctions(t *testing.T) {
 	for _, tc := range []testCase{
 		{"merge", "/v1/merge", map[string]string{"X-Merged-1": "{\"foo\":[1,2]}", "X-Merged-2": "{\"bar\":[3,4]}", "X-Merged-3": "[\"a\",\"b\"]"}, http.StatusOK},
 		{"coalesce", "/v1/coalesce?q=a", map[string]string{"X-Coalesce-1": "/v1/coalesce", "X-Coalesce-2": "default", "X-Coalesce-3": "default", "X-Coalesce-4": "default"}, http.StatusOK},
-		{"default", "/v1/default?q=a", map[string]string{"X-Default-1": "/v1/default", "X-Default-2": "default", "X-Default-3": "default", "X-Default-4": "default"}, http.StatusOK},
+		{"default", "/v1/default?q=a", map[string]string{"X-Default-1": "/v1/default", "X-Default-2": "default", "X-Default-3": "default", "X-Default-4": "default", "X-Default-5": "prefix-default", "X-Default-6": "default"}, http.StatusOK},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
 			helper := test.New(subT)
diff --git a/server/testdata/integration/functions/01_couper.hcl b/server/testdata/integration/functions/01_couper.hcl
index b2c4e29f..4fd568d4 100644
--- a/server/testdata/integration/functions/01_couper.hcl
+++ b/server/testdata/integration/functions/01_couper.hcl
@@ -30,6 +30,8 @@ server "api" {
           x-default-2 = default(request.cookies.undef, "default")
           x-default-3 = default(request.query.q[1], "default")
           x-default-4 = default(request.cookies.undef, request.query.q[1], "default", request.path)
+          x-default-5 = "prefix-${default(request.cookies.undef, "default")}" # template expr
+          x-default-6 = "${default(request.cookies.undef, "default")}" # template wrap expr
         }
       }
     }