feat: add sum, startswith and endswith functions

This commit adds 3 new HCL2 functions:

* `sum`: computes the sum of a collection of numerical values
* `startswith`: checks if a string has another as prefix
* `endswith`: checks if a string has another as suffix
pull/13355/head
Karthik P 10 months ago committed by GitHub
parent 1d02f14871
commit 51eeadba3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,31 @@
package function
import (
"strings"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
// EndsWithFunc constructs a function that checks if a string ends with
// a specific suffix using strings.HasSuffix
var EndsWithFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
},
{
Name: "suffix",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str := args[0].AsString()
suffix := args[1].AsString()
return cty.BoolVal(strings.HasSuffix(str, suffix)), nil
},
})

@ -0,0 +1,83 @@
package function
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestEndsWith(t *testing.T) {
tests := []struct {
String, Suffix cty.Value
Want cty.Value
}{
{
cty.StringVal("hello world"),
cty.StringVal("world"),
cty.True,
},
{
cty.StringVal("hey world"),
cty.StringVal("worldss"),
cty.False,
},
{
cty.StringVal(""),
cty.StringVal(""),
cty.True,
},
{
cty.StringVal("a"),
cty.StringVal(""),
cty.True,
},
{
cty.StringVal("hello world"),
cty.StringVal(" "),
cty.False,
},
{
cty.StringVal(" "),
cty.StringVal(""),
cty.True,
},
{
cty.StringVal(" "),
cty.StringVal("hello"),
cty.False,
},
{
cty.StringVal(""),
cty.StringVal("a"),
cty.False,
},
{
cty.UnknownVal(cty.String),
cty.StringVal("a"),
cty.UnknownVal(cty.Bool).RefineNotNull(),
},
{
cty.UnknownVal(cty.String),
cty.StringVal(""),
cty.UnknownVal(cty.Bool).RefineNotNull(),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("EndsWith(%#v, %#v)", test.String, test.Suffix), func(t *testing.T) {
got, err := EndsWithFunc.Call([]cty.Value{test.String, test.Suffix})
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf(
"wrong result\nstring: %#v\nsuffix: %#v\ngot: %#v\nwant: %#v",
test.String, test.Suffix, got, test.Want,
)
}
})
}
}

@ -0,0 +1,32 @@
package function
import (
"strings"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
// StartsWithFunc constructs a function that checks if a string starts with
// a specific prefix using strings.HasPrefix
var StartsWithFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "str",
Type: cty.String,
AllowUnknown: false,
},
{
Name: "prefix",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.Bool),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
str := args[0].AsString()
prefix := args[1].AsString()
return cty.BoolVal(strings.HasPrefix(str, prefix)), nil
},
})

@ -0,0 +1,98 @@
package function
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestStartsWith(t *testing.T) {
tests := []struct {
String, Prefix cty.Value
Want cty.Value
}{
{
cty.StringVal("hello world"),
cty.StringVal("hello"),
cty.True,
},
{
cty.StringVal("hey world"),
cty.StringVal("hello"),
cty.False,
},
{
cty.StringVal(""),
cty.StringVal(""),
cty.True,
},
{
cty.StringVal(""),
cty.StringVal(" "),
cty.False,
},
{
cty.StringVal("a"),
cty.StringVal(""),
cty.True,
},
{
cty.StringVal(""),
cty.StringVal("a"),
cty.False,
},
{
// Unicode combining characters edge-case: we match the prefix
// in terms of unicode code units rather than grapheme clusters,
// which is inconsistent with our string processing elsewhere but
// would be a breaking change to fix that bug now.
cty.StringVal("\U0001f937\u200d\u2642"), // "Man Shrugging" is encoded as "Person Shrugging" followed by zero-width joiner and then the masculine gender presentation modifier
cty.StringVal("\U0001f937"), // Just the "Person Shrugging" character without any modifiers
cty.True,
},
{
cty.StringVal("hello world"),
cty.StringVal(" "),
cty.False,
},
{
cty.StringVal(" "),
cty.StringVal(""),
cty.True,
},
{
cty.StringVal(" "),
cty.StringVal("hello"),
cty.False,
},
{
cty.UnknownVal(cty.String),
cty.StringVal("a"),
cty.UnknownVal(cty.Bool).RefineNotNull(),
},
{
cty.UnknownVal(cty.String),
cty.StringVal(""),
cty.UnknownVal(cty.Bool).RefineNotNull(),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("StartsWith(%#v, %#v)", test.String, test.Prefix), func(t *testing.T) {
got, err := StartsWithFunc.Call([]cty.Value{test.String, test.Prefix})
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf(
"wrong result\nstring: %#v\nprefix: %#v\ngot: %#v\nwant: %#v",
test.String, test.Prefix, got, test.Want,
)
}
})
}
}

@ -0,0 +1,83 @@
package function
import (
"fmt"
"math/big"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
)
var SumFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
},
Type: function.StaticReturnType(cty.Number),
RefineResult: refineNotNull,
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
if !args[0].CanIterateElements() {
return cty.NilVal, function.NewArgErrorf(0, "cannot sum noniterable")
}
if args[0].LengthInt() == 0 { // Easy path
return cty.NilVal, function.NewArgErrorf(0, "cannot sum an empty list")
}
arg := args[0].AsValueSlice()
ty := args[0].Type()
if !ty.IsListType() && !ty.IsSetType() && !ty.IsTupleType() {
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple. Received %s", ty.FriendlyName())
}
if !args[0].IsWhollyKnown() {
return cty.UnknownVal(cty.Number), nil
}
// big.Float.Add can panic if the input values are opposing infinities,
// so we must catch that here in order to remain within
// the cty Function abstraction.
defer func() {
if r := recover(); r != nil {
if _, ok := r.(big.ErrNaN); ok {
ret = cty.NilVal
err = fmt.Errorf("can't compute sum of opposing infinities")
} else {
// not a panic we recognize
panic(r)
}
}
}()
s := arg[0]
if s.IsNull() {
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
}
s, err = convert.Convert(s, cty.Number)
if err != nil {
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
}
for _, v := range arg[1:] {
if v.IsNull() {
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
}
v, err = convert.Convert(v, cty.Number)
if err != nil {
return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values")
}
s = s.Add(v)
}
return s, nil
},
})
// Sum adds numbers in a list, set, or tuple
func Sum(list cty.Value) (cty.Value, error) {
return SumFunc.Call([]cty.Value{list})
}

@ -0,0 +1,226 @@
package function
import (
"fmt"
"math"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestSum(t *testing.T) {
tests := []struct {
List cty.Value
Want cty.Value
Err string
}{
{
cty.ListVal([]cty.Value{
cty.NumberIntVal(1),
cty.NumberIntVal(2),
cty.NumberIntVal(3),
}),
cty.NumberIntVal(6),
"",
},
{
cty.ListVal([]cty.Value{
cty.NumberIntVal(1476),
cty.NumberIntVal(2093),
cty.NumberIntVal(2092495),
cty.NumberIntVal(64589234),
cty.NumberIntVal(234),
}),
cty.NumberIntVal(66685532),
"",
},
{
cty.ListVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
cty.StringVal("c"),
}),
cty.UnknownVal(cty.String),
"argument must be list, set, or tuple of number values",
},
{
cty.ListVal([]cty.Value{
cty.NumberIntVal(10),
cty.NumberIntVal(-19),
cty.NumberIntVal(5),
}),
cty.NumberIntVal(-4),
"",
},
{
cty.ListVal([]cty.Value{
cty.NumberFloatVal(10.2),
cty.NumberFloatVal(19.4),
cty.NumberFloatVal(5.7),
}),
cty.NumberFloatVal(35.3),
"",
},
{
cty.ListVal([]cty.Value{
cty.NumberFloatVal(-10.2),
cty.NumberFloatVal(-19.4),
cty.NumberFloatVal(-5.7),
}),
cty.NumberFloatVal(-35.3),
"",
},
{
cty.ListVal([]cty.Value{cty.NullVal(cty.Number)}),
cty.NilVal,
"argument must be list, set, or tuple of number values",
},
{
cty.ListVal([]cty.Value{
cty.NumberIntVal(5),
cty.NullVal(cty.Number),
}),
cty.NilVal,
"argument must be list, set, or tuple of number values",
},
{
cty.SetVal([]cty.Value{
cty.StringVal("a"),
cty.StringVal("b"),
cty.StringVal("c"),
}),
cty.UnknownVal(cty.String).RefineNotNull(),
"argument must be list, set, or tuple of number values",
},
{
cty.SetVal([]cty.Value{
cty.NumberIntVal(10),
cty.NumberIntVal(-19),
cty.NumberIntVal(5),
}),
cty.NumberIntVal(-4),
"",
},
{
cty.SetVal([]cty.Value{
cty.NumberIntVal(10),
cty.NumberIntVal(25),
cty.NumberIntVal(30),
}),
cty.NumberIntVal(65),
"",
},
{
cty.SetVal([]cty.Value{
cty.NumberFloatVal(2340.8),
cty.NumberFloatVal(10.2),
cty.NumberFloatVal(3),
}),
cty.NumberFloatVal(2354),
"",
},
{
cty.SetVal([]cty.Value{
cty.NumberFloatVal(2),
}),
cty.NumberFloatVal(2),
"",
},
{
cty.SetVal([]cty.Value{
cty.NumberFloatVal(-2),
cty.NumberFloatVal(-50),
cty.NumberFloatVal(-20),
cty.NumberFloatVal(-123),
cty.NumberFloatVal(-4),
}),
cty.NumberFloatVal(-199),
"",
},
{
cty.TupleVal([]cty.Value{
cty.NumberIntVal(12),
cty.StringVal("a"),
cty.NumberIntVal(38),
}),
cty.UnknownVal(cty.String).RefineNotNull(),
"argument must be list, set, or tuple of number values",
},
{
cty.NumberIntVal(12),
cty.NilVal,
"cannot sum noniterable",
},
{
cty.ListValEmpty(cty.Number),
cty.NilVal,
"cannot sum an empty list",
},
{
cty.MapVal(map[string]cty.Value{"hello": cty.True}),
cty.NilVal,
"argument must be list, set, or tuple. Received map of bool",
},
{
cty.UnknownVal(cty.Number),
cty.UnknownVal(cty.Number).RefineNotNull(),
"",
},
{
cty.UnknownVal(cty.List(cty.Number)),
cty.UnknownVal(cty.Number).RefineNotNull(),
"",
},
{ // known list containing unknown values
cty.ListVal([]cty.Value{cty.UnknownVal(cty.Number)}),
cty.UnknownVal(cty.Number).RefineNotNull(),
"",
},
{ // numbers too large to represent as float64
cty.ListVal([]cty.Value{
cty.MustParseNumberVal("1e+500"),
cty.MustParseNumberVal("1e+500"),
}),
cty.MustParseNumberVal("2e+500"),
"",
},
{ // edge case we have a special error handler for
cty.ListVal([]cty.Value{
cty.NumberFloatVal(math.Inf(1)),
cty.NumberFloatVal(math.Inf(-1)),
}),
cty.NilVal,
"can't compute sum of opposing infinities",
},
{
cty.ListVal([]cty.Value{
cty.StringVal("1"),
cty.StringVal("2"),
cty.StringVal("3"),
}),
cty.NumberIntVal(6),
"",
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("sum(%#v)", test.List), func(t *testing.T) {
got, err := Sum(test.List)
if test.Err != "" {
if err == nil {
t.Fatal("succeeded; want error")
} else if got, want := err.Error(), test.Err; got != want {
t.Fatalf("wrong error\n got: %s\nwant: %s", got, want)
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

@ -62,6 +62,7 @@ func Functions(basedir string) map[string]function.Function {
"dirname": filesystem.DirnameFunc,
"distinct": stdlib.DistinctFunc,
"element": stdlib.ElementFunc,
"endswith": pkrfunction.EndsWithFunc,
"file": filesystem.MakeFileFunc(basedir, false),
"fileexists": filesystem.MakeFileExistsFunc(basedir),
"fileset": filesystem.MakeFileSetFunc(basedir),
@ -106,9 +107,11 @@ func Functions(basedir string) map[string]function.Function {
"slice": stdlib.SliceFunc,
"sort": stdlib.SortFunc,
"split": stdlib.SplitFunc,
"startswith": pkrfunction.StartsWithFunc,
"strcontains": pkrfunction.StrContains,
"strrev": stdlib.ReverseFunc,
"substr": stdlib.SubstrFunc,
"sum": pkrfunction.SumFunc,
"textdecodebase64": TextDecodeBase64Func,
"textencodebase64": TextEncodeBase64Func,
"timestamp": pkrfunction.TimestampFunc,

@ -0,0 +1,17 @@
---
page_title: sum - Functions - Configuration Language
description: The sum function takes a list or set of numbers and returns the sum of those numbers.
---
# `sum` Function
`sum` takes a list or set of numbers and returns the sum of those numbers.
`sum` fails when given an empty list or set.
## Examples
```
> sum([10, 13, 6, 4.5])
33.5
```

@ -0,0 +1,27 @@
---
page_title: endswith - Functions - Configuration Language
description: |-
The endswith function takes two values: a string to check and a suffix string. It returns true if the first string ends with that exact suffix.
---
# `endswith` Function
`endswith` takes two values: a string to check and a suffix string. The function returns true if the first string ends with that exact suffix.
```hcl
endswith(string, suffix)
```
## Examples
```
> endswith("hello world", "world")
true
> endswith("hello world", "hello")
false
```
## Related Functions
- [`startswith`](/packer/docs/templates/hcl_templates/functions/string/startswith) takes two values: a string to check
and a prefix string. The function returns true if the string begins with that exact prefix.

@ -0,0 +1,27 @@
---
page_title: startsswith - Functions - Configuration Language
description: |-
The startswith function takes two values: a string to check and a prefix string. It returns true if the string begins with that exact prefix.
---
# `startswith` Function
`startswith` takes two values: a string to check and a prefix string. The function returns true if the string begins with that exact prefix.
```hcl
startswith(string, prefix)
```
## Examples
```
> startswith("hello world", "hello")
true
> startswith("hello world", "world")
false
```
## Related Functions
- [`endswith`](/packer/docs/templates/hcl_templates/functions/string/endswith) takes two values: a string to check
and a suffix string. The function returns true if the first string ends with that exact suffix.

@ -235,6 +235,10 @@
{
"title": "signum",
"path": "templates/hcl_templates/functions/numeric/signum"
},
{
"title": "sum",
"path": "templates/hcl_templates/functions/numeric/sum"
}
]
},
@ -245,6 +249,10 @@
"title": "chomp",
"path": "templates/hcl_templates/functions/string/chomp"
},
{
"title": "endswith",
"path": "templates/hcl_templates/functions/string/endswith"
},
{
"title": "format",
"path": "templates/hcl_templates/functions/string/format"
@ -285,6 +293,10 @@
"title": "split",
"path": "templates/hcl_templates/functions/string/split"
},
{
"title": "startswith",
"path": "templates/hcl_templates/functions/string/startswith"
},
{
"title": "strcontains",
"path": "templates/hcl_templates/functions/string/strcontains"

Loading…
Cancel
Save