diff --git a/globals/globals_test.go b/globals/globals_test.go index 0d9f136b5b..b3e9377382 100644 --- a/globals/globals_test.go +++ b/globals/globals_test.go @@ -2870,6 +2870,135 @@ func TestLoadGlobals(t *testing.T) { ), }, }, + { + name: "tm_alltrue with literal list of bool", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("val", `tm_alltrue([true, 1==1, false==false, !false])`), + ), + }, + }, + want: map[string]*hclwrite.Block{ + "/stack": Globals( + EvalExpr(t, "val", `true`), + ), + }, + }, + { + name: "tm_alltrue with literal tuple containing non-boolean", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("val", `tm_alltrue([true, 1==1, "string", {}])`), + ), + }, + }, + wantErr: errors.E(globals.ErrEval), + }, + { + name: "tm_alltrue with literal for-loop", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("val", `tm_alltrue([for i in [true, 1==1, !false] : i])`), + ), + }, + }, + want: map[string]*hclwrite.Block{ + "/stack": Globals( + EvalExpr(t, "val", `true`), + ), + }, + }, + { + name: "tm_alltrue with funcall", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("val", `tm_alltrue(tm_distinct([true, !false, 1==1]))`), + ), + }, + }, + want: map[string]*hclwrite.Block{ + "/stack": Globals( + EvalExpr(t, "val", `true`), + ), + }, + }, + + { + name: "tm_anytrue with literal list of bool", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("val", `tm_anytrue([false, 1!=1, false==false, !false])`), + ), + }, + }, + want: map[string]*hclwrite.Block{ + "/stack": Globals( + EvalExpr(t, "val", `true`), + ), + }, + }, + { + name: "tm_anytrue with literal tuple containing non-boolean", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("val", `tm_anytrue([false, 1!=1, "string", {}])`), + ), + }, + }, + wantErr: errors.E(globals.ErrEval), + }, + { + name: "tm_anytrue with literal for-loop", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("val", `tm_anytrue([for i in [false, 1!=1, !false] : i])`), + ), + }, + }, + want: map[string]*hclwrite.Block{ + "/stack": Globals( + EvalExpr(t, "val", `true`), + ), + }, + }, + { + name: "tm_anytrue with funcall", + layout: []string{"s:stack"}, + configs: []hclconfig{ + { + path: "/stack", + add: Globals( + Expr("val", `tm_anytrue(tm_distinct([false, false, 1==1]))`), + ), + }, + }, + want: map[string]*hclwrite.Block{ + "/stack": Globals( + EvalExpr(t, "val", `true`), + ), + }, + }, { name: "globals.map label conflicts with global name", layout: []string{"s:stack"}, diff --git a/stdlib/collection.go b/stdlib/collection.go new file mode 100644 index 0000000000..80fcde3e00 --- /dev/null +++ b/stdlib/collection.go @@ -0,0 +1,145 @@ +// Copyright 2023 Terramate GmbH +// SPDX-License-Identifier: MPL-2.0 + +package stdlib + +import ( + "github.com/hashicorp/hcl/v2/ext/customdecode" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/terramate-io/terramate/errors" + "github.com/terramate-io/terramate/hcl/eval" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// AllTrueFunc implements the `tm_alltrue()` function. +func AllTrueFunc() function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: customdecode.ExpressionClosureType, + }, + }, + Type: function.StaticReturnType(cty.Bool), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + argClosure := customdecode.ExpressionClosureFromVal(args[0]) + evalctx := eval.NewContextFrom(argClosure.EvalContext) + listExpression, ok := argClosure.Expression.(*hclsyntax.TupleConsExpr) + if !ok { + v, err := evalctx.Eval(argClosure.Expression) + if err != nil { + return cty.False, err + } + if !v.Type().IsListType() && !v.Type().IsTupleType() { + return cty.False, errors.E(`Invalid value for "list" parameter: %s`, v.Type().FriendlyName()) + } + result := true + i := 0 + for it := v.ElementIterator(); it.Next(); { + _, v := it.Element() + if !v.IsKnown() { + return cty.UnknownVal(cty.Bool), nil + } + if v.IsNull() { + return cty.False, nil + } + if !v.Type().Equals(cty.Bool) { + return cty.False, errors.E(`Invalid value for "list" parameter: element %d: bool required`, i+1) + } + result = result && v.True() + if !result { + return cty.False, nil + } + i++ + } + return cty.True, nil + } + + result := true + for i, expr := range listExpression.Exprs { + v, err := evalctx.Eval(expr) + if err != nil { + return cty.False, err + } + if v.IsNull() { + return cty.False, nil + } + if !v.Type().Equals(cty.Bool) { + return cty.False, errors.E(`Invalid value for "list" parameter: element %d: bool required`, i+1) + } + result = result && v.True() + if !result { + return cty.False, nil + } + } + return cty.True, nil + }, + }) +} + +// AnyTrueFunc implements the `tm_anytrue()` function. +func AnyTrueFunc() function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: customdecode.ExpressionClosureType, + }, + }, + Type: function.StaticReturnType(cty.Bool), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + argClosure := customdecode.ExpressionClosureFromVal(args[0]) + evalctx := eval.NewContextFrom(argClosure.EvalContext) + listExpression, ok := argClosure.Expression.(*hclsyntax.TupleConsExpr) + if !ok { + v, err := evalctx.Eval(argClosure.Expression) + if err != nil { + return cty.False, err + } + if !v.Type().IsListType() && !v.Type().IsTupleType() { + return cty.False, errors.E(`Invalid value for "list" parameter: %s`, v.Type().FriendlyName()) + } + result := false + i := 0 + for it := v.ElementIterator(); it.Next(); { + _, v := it.Element() + if !v.IsKnown() { + continue + } + if v.IsNull() { + continue + } + if !v.Type().Equals(cty.Bool) { + return cty.False, errors.E(`Invalid value for "list" parameter: element %d: bool required`, i+1) + } + result = result || v.True() + if result { + return cty.True, nil + } + i++ + } + return cty.False, nil + } + + result := false + for i, expr := range listExpression.Exprs { + v, err := evalctx.Eval(expr) + if err != nil { + return cty.False, err + } + if v.IsNull() { + continue + } + if !v.Type().Equals(cty.Bool) { + return cty.False, errors.E(`Invalid value for "list" parameter: element %d: bool required`, i+1) + } + result = result || v.True() + if result { + return cty.True, nil + } + } + return cty.False, nil + }, + }) +} diff --git a/stdlib/funcs.go b/stdlib/funcs.go index d1d65e265c..be71dfab7d 100644 --- a/stdlib/funcs.go +++ b/stdlib/funcs.go @@ -67,6 +67,10 @@ func Functions(basedir string) map[string]function.Function { // sane ternary tmfuncs["tm_ternary"] = TernaryFunc() + // optimized collection functions + tmfuncs["tm_alltrue"] = AllTrueFunc() + tmfuncs["tm_anytrue"] = AnyTrueFunc() + tmfuncs["tm_version_match"] = VersionMatch() return tmfuncs }