lang/funcs: port some of Terraform's built-in functions

These implementations are adaptations of the existing implementations in
config/interpolate_funcs.go, updated to work with the cty API.

The set of functions chosen here was motivated mainly by what Terraform's
existing context tests depend on, so we can get the contexts tests back
into good shape before fleshing out the rest of these functions.
This commit is contained in:
Martin Atkins 2018-05-21 17:39:26 -07:00
parent e528fe50e9
commit 129f5fe74d
11 changed files with 973 additions and 7 deletions

120
lang/funcs/collection.go Normal file
View File

@ -0,0 +1,120 @@
package funcs
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/function/stdlib"
"github.com/zclconf/go-cty/cty/gocty"
)
var ElementFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
{
Name: "index",
Type: cty.Number,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
list := args[0]
listTy := list.Type()
switch {
case listTy.IsListType():
return listTy.ElementType(), nil
case listTy.IsTupleType():
etys := listTy.TupleElementTypes()
var index int
err := gocty.FromCtyValue(args[1], &index)
if err != nil {
// e.g. fractional number where whole number is required
return cty.DynamicPseudoType, fmt.Errorf("invalid index: %s", err)
}
if len(etys) == 0 {
return cty.DynamicPseudoType, fmt.Errorf("cannot use element function with an empty list")
}
index = index % len(etys)
return etys[index], nil
default:
return cty.DynamicPseudoType, fmt.Errorf("cannot read elements from %s", listTy.FriendlyName())
}
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
var index int
err := gocty.FromCtyValue(args[1], &index)
if err != nil {
// can't happen because we checked this in the Type function above
return cty.DynamicVal, fmt.Errorf("invalid index: %s", err)
}
l := args[0].LengthInt()
if l == 0 {
return cty.DynamicVal, fmt.Errorf("cannot use element function with an empty list")
}
index = index % l
// We did all the necessary type checks in the type function above,
// so this is guaranteed not to fail.
return args[0].Index(cty.NumberIntVal(int64(index))), nil
},
})
var LengthFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "value",
Type: cty.DynamicPseudoType,
AllowDynamicType: true,
AllowUnknown: true,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
collTy := args[0].Type()
switch {
case collTy == cty.String || collTy.IsTupleType() || collTy.IsListType() || collTy.IsMapType() || collTy.IsSetType() || collTy == cty.DynamicPseudoType:
return cty.Number, nil
default:
return cty.Number, fmt.Errorf("argument must be a string, a collection type, or a structural type")
}
},
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
coll := args[0]
collTy := args[0].Type()
switch {
case collTy == cty.DynamicPseudoType:
return cty.UnknownVal(cty.Number), nil
case collTy.IsTupleType():
l := len(collTy.TupleElementTypes())
return cty.NumberIntVal(int64(l)), nil
case collTy.IsObjectType():
l := len(collTy.AttributeTypes())
return cty.NumberIntVal(int64(l)), nil
case collTy == cty.String:
// We'll delegate to the cty stdlib strlen function here, because
// it deals with all of the complexities of tokenizing unicode
// grapheme clusters.
return stdlib.Strlen(coll)
case collTy.IsListType() || collTy.IsSetType() || collTy.IsMapType():
return coll.Length(), nil
default:
// Should never happen, because of the checks in our Type func above
return cty.UnknownVal(cty.Number), fmt.Errorf("impossible value type for length(...)")
}
},
})
// Element returns a single element from a given list at the given index. If
// index is greater than the length of the list then it is wrapped modulo
// the list length.
func Element(list, index cty.Value) (cty.Value, error) {
return ElementFunc.Call([]cty.Value{list, index})
}
// Length returns the number of elements in the given collection or number of
// Unicode characters in the given string.
func Length(collection cty.Value) (cty.Value, error) {
return LengthFunc.Call([]cty.Value{collection})
}

View File

@ -0,0 +1,224 @@
package funcs
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestElement(t *testing.T) {
tests := []struct {
List cty.Value
Index cty.Value
Want cty.Value
}{
{
cty.ListVal([]cty.Value{
cty.StringVal("hello"),
}),
cty.NumberIntVal(0),
cty.StringVal("hello"),
},
{
cty.ListVal([]cty.Value{
cty.StringVal("hello"),
}),
cty.NumberIntVal(1),
cty.StringVal("hello"),
},
{
cty.ListVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("bonjour"),
}),
cty.NumberIntVal(0),
cty.StringVal("hello"),
},
{
cty.ListVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("bonjour"),
}),
cty.NumberIntVal(1),
cty.StringVal("bonjour"),
},
{
cty.ListVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("bonjour"),
}),
cty.NumberIntVal(2),
cty.StringVal("hello"),
},
{
cty.TupleVal([]cty.Value{
cty.StringVal("hello"),
}),
cty.NumberIntVal(0),
cty.StringVal("hello"),
},
{
cty.TupleVal([]cty.Value{
cty.StringVal("hello"),
}),
cty.NumberIntVal(1),
cty.StringVal("hello"),
},
{
cty.TupleVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("bonjour"),
}),
cty.NumberIntVal(0),
cty.StringVal("hello"),
},
{
cty.TupleVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("bonjour"),
}),
cty.NumberIntVal(1),
cty.StringVal("bonjour"),
},
{
cty.TupleVal([]cty.Value{
cty.StringVal("hello"),
cty.StringVal("bonjour"),
}),
cty.NumberIntVal(2),
cty.StringVal("hello"),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("Element(%#v, %#v)", test.List, test.Index), func(t *testing.T) {
got, err := Element(test.List, test.Index)
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)
}
})
}
}
func TestLength(t *testing.T) {
tests := []struct {
Value cty.Value
Want cty.Value
}{
{
cty.ListValEmpty(cty.Number),
cty.NumberIntVal(0),
},
{
cty.ListVal([]cty.Value{cty.True}),
cty.NumberIntVal(1),
},
{
cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
cty.NumberIntVal(1),
},
{
cty.SetValEmpty(cty.Number),
cty.NumberIntVal(0),
},
{
cty.SetVal([]cty.Value{cty.True}),
cty.NumberIntVal(1),
},
{
cty.MapValEmpty(cty.Bool),
cty.NumberIntVal(0),
},
{
cty.MapVal(map[string]cty.Value{"hello": cty.True}),
cty.NumberIntVal(1),
},
{
cty.EmptyTupleVal,
cty.NumberIntVal(0),
},
{
cty.TupleVal([]cty.Value{cty.True}),
cty.NumberIntVal(1),
},
{
cty.UnknownVal(cty.List(cty.Bool)),
cty.UnknownVal(cty.Number),
},
{
cty.DynamicVal,
cty.UnknownVal(cty.Number),
},
{
cty.StringVal("hello"),
cty.NumberIntVal(5),
},
{
cty.StringVal(""),
cty.NumberIntVal(0),
},
{
cty.StringVal("1"),
cty.NumberIntVal(1),
},
{
cty.StringVal("Живой Журнал"),
cty.NumberIntVal(12),
},
{
// note that the dieresis here is intentionally a combining
// ligature.
cty.StringVal("noël"),
cty.NumberIntVal(4),
},
{
// The Es in this string has three combining acute accents.
// This tests something that NFC-normalization cannot collapse
// into a single precombined codepoint, since otherwise we might
// be cheating and relying on the single-codepoint forms.
cty.StringVal("wé́́é́́é́́!"),
cty.NumberIntVal(5),
},
{
// Go's normalization forms don't handle this ligature, so we
// will produce the wrong result but this is now a compatibility
// constraint and so we'll test it.
cty.StringVal("baffle"),
cty.NumberIntVal(4),
},
{
cty.StringVal("😸😾"),
cty.NumberIntVal(2),
},
{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.Number),
},
{
cty.DynamicVal,
cty.UnknownVal(cty.Number),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("Length(%#v)", test.Value), func(t *testing.T) {
got, err := Length(test.Value)
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)
}
})
}
}

29
lang/funcs/crypto.go Normal file
View File

@ -0,0 +1,29 @@
package funcs
import (
uuid "github.com/hashicorp/go-uuid"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
var UUIDFunc = function.New(&function.Spec{
Params: []function.Parameter{},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
result, err := uuid.GenerateUUID()
if err != nil {
return cty.UnknownVal(cty.String), err
}
return cty.StringVal(result), nil
},
})
// UUID generates and returns a Type-4 UUID in the standard hexadecimal string
// format.
//
// This is not a pure function: it will generate a different result for each
// call. It must therefore be registered as an impure function in the function
// table in the "lang" package.
func UUID() (cty.Value, error) {
return UUIDFunc.Call(nil)
}

17
lang/funcs/crypto_test.go Normal file
View File

@ -0,0 +1,17 @@
package funcs
import (
"testing"
)
func TestUUID(t *testing.T) {
result, err := UUID()
if err != nil {
t.Fatal(err)
}
resultStr := result.AsString()
if got, want := len(resultStr), 36; got != want {
t.Errorf("wrong result length %d; want %d", got, want)
}
}

88
lang/funcs/filesystem.go Normal file
View File

@ -0,0 +1,88 @@
package funcs
import (
"encoding/base64"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"unicode/utf8"
homedir "github.com/mitchellh/go-homedir"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
// MakeFileFunc constructs a function that takes a file path and returns the
// contents of that file, either directly as a string (where valid UTF-8 is
// required) or as a string containing base64 bytes.
func MakeFileFunc(baseDir string, encBase64 bool) function.Function {
return function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "path",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
path := args[0].AsString()
path, err := homedir.Expand(path)
if err != nil {
return cty.UnknownVal(cty.String), fmt.Errorf("failed to expand ~: %s", err)
}
if !filepath.IsAbs(path) {
path = filepath.Join(baseDir, path)
}
// Ensure that the path is canonical for the host OS
path = filepath.Clean(path)
src, err := ioutil.ReadFile(path)
if err != nil {
// ReadFile does not return Terraform-user-friendly error
// messages, so we'll provide our own.
if os.IsNotExist(err) {
return cty.UnknownVal(cty.String), fmt.Errorf("no file exists at %s", path)
}
return cty.UnknownVal(cty.String), fmt.Errorf("failed to read %s", path)
}
switch {
case encBase64:
enc := base64.StdEncoding.EncodeToString(src)
return cty.StringVal(enc), nil
default:
if !utf8.Valid(src) {
return cty.UnknownVal(cty.String), fmt.Errorf("contents of %s are not valid UTF-8; to read arbitrary bytes, use the filebase64 function instead", path)
}
return cty.StringVal(string(src)), nil
}
},
})
}
// File reads the contents of the file at the given path.
//
// The file must contain valid UTF-8 bytes, or this function will return an error.
//
// The underlying function implementation works relative to a particular base
// directory, so this wrapper takes a base directory string and uses it to
// construct the underlying function before calling it.
func File(baseDir string, path cty.Value) (cty.Value, error) {
fn := MakeFileFunc(baseDir, false)
return fn.Call([]cty.Value{path})
}
// FileBase64 reads the contents of the file at the given path.
//
// The bytes from the file are encoded as base64 before returning.
//
// The underlying function implementation works relative to a particular base
// directory, so this wrapper takes a base directory string and uses it to
// construct the underlying function before calling it.
func FileBase64(baseDir string, path cty.Value) (cty.Value, error) {
fn := MakeFileFunc(baseDir, true)
return fn.Call([]cty.Value{path})
}

View File

@ -0,0 +1,98 @@
package funcs
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestFile(t *testing.T) {
tests := []struct {
Path cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("testdata/hello.txt"),
cty.StringVal("Hello World"),
false,
},
{
cty.StringVal("testdata/icon.png"),
cty.NilVal,
true, // Not valid UTF-8
},
{
cty.StringVal("testdata/missing"),
cty.NilVal,
true, // no file exists
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("File(\".\", %#v)", test.Path), func(t *testing.T) {
got, err := File(".", test.Path)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
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)
}
})
}
}
func TestFileBase64(t *testing.T) {
tests := []struct {
Path cty.Value
Want cty.Value
Err bool
}{
{
cty.StringVal("testdata/hello.txt"),
cty.StringVal("SGVsbG8gV29ybGQ="),
false,
},
{
cty.StringVal("testdata/icon.png"),
cty.StringVal("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAq1BMVEX///9cTuVeUeRcTuZcTuZcT+VbSe1cTuVdT+MAAP9JSbZcT+VcTuZAQLFAQLJcTuVcTuZcUuBBQbA/P7JAQLJaTuRcT+RcTuVGQ7xAQLJVVf9cTuVcTuVGRMFeUeRbTeJcTuU/P7JeTeZbTOVcTeZAQLJBQbNAQLNaUORcTeZbT+VcTuRAQLNAQLRdTuRHR8xgUOdgUN9cTuVdTeRdT+VZTulcTuVAQLL///8+GmETAAAANnRSTlMApibw+osO6DcBB3fIX87+oRk3yehB0/Nj/gNs7nsTRv3dHmu//JYUMLVr3bssjxkgEK5CaxeK03nIAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAADoQAAA6EBvJf9gwAAAAd0SU1FB+EEBRIQDxZNTKsAAACCSURBVBjTfc7JFsFQEATQQpCYxyBEzJ55rvf/f0ZHcyQLvelTd1GngEwWycs5+UISyKLraSi9geWKK9Gr1j7AeqOJVtt2XtD1Bchef2BjQDAcCTC0CsA4mihMtXw2XwgsV2sFw812F+4P3y2GdI6nn3FGSs//4HJNAXDzU4Dg/oj/E+bsEbhf5cMsAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE3LTA0LTA1VDE4OjE2OjE1KzAyOjAws5bLVQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNy0wNC0wNVQxODoxNjoxNSswMjowMMLLc+kAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAAC3RFWHRUaXRsZQBHcm91cJYfIowAAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII="),
false,
},
{
cty.StringVal("testdata/missing"),
cty.NilVal,
true, // no file exists
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("FileBase64(\".\", %#v)", test.Path), func(t *testing.T) {
got, err := FileBase64(".", test.Path)
if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
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)
}
})
}
}

132
lang/funcs/string.go Normal file
View File

@ -0,0 +1,132 @@
package funcs
import (
"fmt"
"sort"
"strings"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
)
var JoinFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "separator",
Type: cty.String,
},
},
VarParam: &function.Parameter{
Name: "lists",
Type: cty.List(cty.String),
},
Type: function.StaticReturnType(cty.String),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
sep := args[0].AsString()
listVals := args[1:]
if len(listVals) < 1 {
return cty.UnknownVal(cty.String), fmt.Errorf("at least one list is required")
}
l := 0
for _, list := range listVals {
if !list.IsWhollyKnown() {
return cty.UnknownVal(cty.String), nil
}
l += list.LengthInt()
}
items := make([]string, 0, l)
for _, list := range listVals {
for it := list.ElementIterator(); it.Next(); {
_, val := it.Element()
items = append(items, val.AsString())
}
}
return cty.StringVal(strings.Join(items, sep)), nil
},
})
var SortFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.String),
},
},
Type: function.StaticReturnType(cty.List(cty.String)),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
listVal := args[0]
if !listVal.IsWhollyKnown() {
// If some of the element values aren't known yet then we
// can't yet preduct the order of the result.
return cty.UnknownVal(retType), nil
}
if listVal.LengthInt() == 0 { // Easy path
return listVal, nil
}
list := make([]string, 0, listVal.LengthInt())
for it := listVal.ElementIterator(); it.Next(); {
_, v := it.Element()
list = append(list, v.AsString())
}
sort.Strings(list)
retVals := make([]cty.Value, len(list))
for i, s := range list {
retVals[i] = cty.StringVal(s)
}
return cty.ListVal(retVals), nil
},
})
var SplitFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "separator",
Type: cty.String,
},
{
Name: "str",
Type: cty.String,
},
},
Type: function.StaticReturnType(cty.List(cty.String)),
Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
sep := args[0].AsString()
str := args[1].AsString()
elems := strings.Split(str, sep)
elemVals := make([]cty.Value, len(elems))
for i, s := range elems {
elemVals[i] = cty.StringVal(s)
}
if len(elemVals) == 0 {
return cty.ListValEmpty(cty.String), nil
}
return cty.ListVal(elemVals), nil
},
})
// Join concatenates together the string elements of one or more lists with a
// given separator.
func Join(sep cty.Value, lists ...cty.Value) (cty.Value, error) {
args := make([]cty.Value, len(lists)+1)
args[0] = sep
copy(args[1:], lists)
return JoinFunc.Call(args)
}
// Sort re-orders the elements of a given list of strings so that they are
// in ascending lexicographical order.
func Sort(list cty.Value) (cty.Value, error) {
return SortFunc.Call([]cty.Value{list})
}
// Split divides a given string by a given separator, returning a list of
// strings containing the characters between the separator sequences.
func Split(sep, str cty.Value) (cty.Value, error) {
return SplitFunc.Call([]cty.Value{sep, str})
}

246
lang/funcs/string_test.go Normal file
View File

@ -0,0 +1,246 @@
package funcs
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestJoin(t *testing.T) {
tests := []struct {
Sep cty.Value
Lists []cty.Value
Want cty.Value
}{
{
cty.StringVal(" "),
[]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
cty.StringVal("World"),
}),
},
cty.StringVal("Hello World"),
},
{
cty.StringVal(" "),
[]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
cty.StringVal("World"),
}),
cty.ListVal([]cty.Value{
cty.StringVal("Foo"),
cty.StringVal("Bar"),
}),
},
cty.StringVal("Hello World Foo Bar"),
},
{
cty.StringVal(" "),
[]cty.Value{
cty.ListValEmpty(cty.String),
},
cty.StringVal(""),
},
{
cty.StringVal(" "),
[]cty.Value{
cty.ListValEmpty(cty.String),
cty.ListValEmpty(cty.String),
cty.ListValEmpty(cty.String),
},
cty.StringVal(""),
},
{
cty.StringVal(" "),
[]cty.Value{
cty.ListValEmpty(cty.String),
cty.ListVal([]cty.Value{
cty.StringVal("Foo"),
cty.StringVal("Bar"),
}),
},
cty.StringVal("Foo Bar"),
},
{
cty.UnknownVal(cty.String),
[]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
cty.StringVal("World"),
}),
},
cty.UnknownVal(cty.String),
},
{
cty.StringVal(" "),
[]cty.Value{
cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
cty.UnknownVal(cty.String),
}),
},
cty.UnknownVal(cty.String),
},
{
cty.StringVal(" "),
[]cty.Value{
cty.UnknownVal(cty.List(cty.String)),
},
cty.UnknownVal(cty.String),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("Join(%#v, %#v...)", test.Sep, test.Lists), func(t *testing.T) {
got, err := Join(test.Sep, test.Lists...)
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)
}
})
}
}
func TestSort(t *testing.T) {
tests := []struct {
List cty.Value
Want cty.Value
}{
{
cty.ListValEmpty(cty.String),
cty.ListValEmpty(cty.String),
},
{
cty.ListVal([]cty.Value{
cty.StringVal("banana"),
}),
cty.ListVal([]cty.Value{
cty.StringVal("banana"),
}),
},
{
cty.ListVal([]cty.Value{
cty.StringVal("banana"),
cty.StringVal("apple"),
}),
cty.ListVal([]cty.Value{
cty.StringVal("apple"),
cty.StringVal("banana"),
}),
},
{
cty.ListVal([]cty.Value{
cty.StringVal("8"),
cty.StringVal("9"),
cty.StringVal("10"),
}),
cty.ListVal([]cty.Value{
cty.StringVal("10"), // lexicographical sort, not numeric sort
cty.StringVal("8"),
cty.StringVal("9"),
}),
},
{
cty.UnknownVal(cty.List(cty.String)),
cty.UnknownVal(cty.List(cty.String)),
},
{
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
}),
cty.UnknownVal(cty.List(cty.String)),
},
{
cty.ListVal([]cty.Value{
cty.UnknownVal(cty.String),
cty.StringVal("banana"),
}),
cty.UnknownVal(cty.List(cty.String)),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("Sort(%#v)", test.List), func(t *testing.T) {
got, err := Sort(test.List)
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)
}
})
}
}
func TestSplit(t *testing.T) {
tests := []struct {
Sep cty.Value
Str cty.Value
Want cty.Value
}{
{
cty.StringVal(" "),
cty.StringVal("Hello World"),
cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
cty.StringVal("World"),
}),
},
{
cty.StringVal(" "),
cty.StringVal("Hello"),
cty.ListVal([]cty.Value{
cty.StringVal("Hello"),
}),
},
{
cty.StringVal(" "),
cty.StringVal(""),
cty.ListVal([]cty.Value{
cty.StringVal(""),
}),
},
{
cty.StringVal(""),
cty.StringVal(""),
cty.ListValEmpty(cty.String),
},
{
cty.UnknownVal(cty.String),
cty.StringVal("Hello World"),
cty.UnknownVal(cty.List(cty.String)),
},
{
cty.StringVal(" "),
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.List(cty.String)),
},
{
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.List(cty.String)),
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("Split(%#v, %#v)", test.Sep, test.Str), func(t *testing.T) {
got, err := Split(test.Sep, test.Str)
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)
}
})
}
}

1
lang/funcs/testdata/hello.txt vendored Normal file
View File

@ -0,0 +1 @@
Hello World

BIN
lang/funcs/testdata/icon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

View File

@ -6,6 +6,8 @@ import (
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/function/stdlib"
"github.com/hashicorp/terraform/lang/funcs"
)
var impureFunctions = []string{
@ -18,6 +20,13 @@ var impureFunctions = []string{
func (s *Scope) Functions() map[string]function.Function {
s.funcsLock.Lock()
if s.funcs == nil {
// Some of our functions are just directly the cty stdlib functions.
// Others are implemented in the subdirectory "funcs" here in this
// repository. New functions should generally start out their lives
// in the "funcs" directory and potentially graduate to cty stdlib
// later if the functionality seems to be something domain-agnostic
// that would be useful to all applications using cty functions.
s.funcs = map[string]function.Function{
"abs": stdlib.AbsoluteFunc,
"basename": unimplFunc, // TODO
@ -40,9 +49,10 @@ func (s *Scope) Functions() map[string]function.Function {
"csvdecode": stdlib.CSVDecodeFunc,
"dirname": unimplFunc, // TODO
"distinct": unimplFunc, // TODO
"element": unimplFunc, // TODO
"element": funcs.ElementFunc,
"chunklist": unimplFunc, // TODO
"file": unimplFunc, // TODO
"file": funcs.MakeFileFunc(s.BaseDir, false),
"filebase64": funcs.MakeFileFunc(s.BaseDir, true),
"matchkeys": unimplFunc, // TODO
"flatten": unimplFunc, // TODO
"floor": unimplFunc, // TODO
@ -50,12 +60,13 @@ func (s *Scope) Functions() map[string]function.Function {
"formatlist": stdlib.FormatListFunc,
"indent": unimplFunc, // TODO
"index": unimplFunc, // TODO
"join": unimplFunc, // TODO
"join": funcs.JoinFunc,
"jsondecode": stdlib.JSONDecodeFunc,
"jsonencode": stdlib.JSONEncodeFunc,
"length": unimplFunc, // TODO
"length": funcs.LengthFunc,
"list": unimplFunc, // TODO
"log": unimplFunc, // TODO
"lookup": unimplFunc, // TODO
"lower": stdlib.LowerFunc,
"map": unimplFunc, // TODO
"max": stdlib.MaxFunc,
@ -71,8 +82,8 @@ func (s *Scope) Functions() map[string]function.Function {
"sha512": unimplFunc, // TODO
"signum": unimplFunc, // TODO
"slice": unimplFunc, // TODO
"sort": unimplFunc, // TODO
"split": unimplFunc, // TODO
"sort": funcs.SortFunc,
"split": funcs.SplitFunc,
"substr": stdlib.SubstrFunc,
"timestamp": unimplFunc, // TODO
"timeadd": unimplFunc, // TODO
@ -81,7 +92,7 @@ func (s *Scope) Functions() map[string]function.Function {
"trimspace": unimplFunc, // TODO
"upper": stdlib.UpperFunc,
"urlencode": unimplFunc, // TODO
"uuid": unimplFunc, // TODO
"uuid": funcs.UUIDFunc,
"zipmap": unimplFunc, // TODO
}