From 129f5fe74d0f2c0dcc0bfae3ad267c8ef859d6cb Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 21 May 2018 17:39:26 -0700 Subject: [PATCH] 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. --- lang/funcs/collection.go | 120 +++++++++++++++++ lang/funcs/collection_test.go | 224 +++++++++++++++++++++++++++++++ lang/funcs/crypto.go | 29 ++++ lang/funcs/crypto_test.go | 17 +++ lang/funcs/filesystem.go | 88 ++++++++++++ lang/funcs/filesystem_test.go | 98 ++++++++++++++ lang/funcs/string.go | 132 ++++++++++++++++++ lang/funcs/string_test.go | 246 ++++++++++++++++++++++++++++++++++ lang/funcs/testdata/hello.txt | 1 + lang/funcs/testdata/icon.png | Bin 0 -> 806 bytes lang/functions.go | 25 +++- 11 files changed, 973 insertions(+), 7 deletions(-) create mode 100644 lang/funcs/collection.go create mode 100644 lang/funcs/collection_test.go create mode 100644 lang/funcs/crypto.go create mode 100644 lang/funcs/crypto_test.go create mode 100644 lang/funcs/filesystem.go create mode 100644 lang/funcs/filesystem_test.go create mode 100644 lang/funcs/string.go create mode 100644 lang/funcs/string_test.go create mode 100644 lang/funcs/testdata/hello.txt create mode 100644 lang/funcs/testdata/icon.png diff --git a/lang/funcs/collection.go b/lang/funcs/collection.go new file mode 100644 index 000000000..b7b00db35 --- /dev/null +++ b/lang/funcs/collection.go @@ -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}) +} diff --git a/lang/funcs/collection_test.go b/lang/funcs/collection_test.go new file mode 100644 index 000000000..62e3c9376 --- /dev/null +++ b/lang/funcs/collection_test.go @@ -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) + } + }) + } +} diff --git a/lang/funcs/crypto.go b/lang/funcs/crypto.go new file mode 100644 index 000000000..bfa8543d9 --- /dev/null +++ b/lang/funcs/crypto.go @@ -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) +} diff --git a/lang/funcs/crypto_test.go b/lang/funcs/crypto_test.go new file mode 100644 index 000000000..29affc6da --- /dev/null +++ b/lang/funcs/crypto_test.go @@ -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) + } +} diff --git a/lang/funcs/filesystem.go b/lang/funcs/filesystem.go new file mode 100644 index 000000000..785dac9ea --- /dev/null +++ b/lang/funcs/filesystem.go @@ -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}) +} diff --git a/lang/funcs/filesystem_test.go b/lang/funcs/filesystem_test.go new file mode 100644 index 000000000..2e7eaad50 --- /dev/null +++ b/lang/funcs/filesystem_test.go @@ -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) + } + }) + } +} diff --git a/lang/funcs/string.go b/lang/funcs/string.go new file mode 100644 index 000000000..e7653ed7a --- /dev/null +++ b/lang/funcs/string.go @@ -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}) +} diff --git a/lang/funcs/string_test.go b/lang/funcs/string_test.go new file mode 100644 index 000000000..6c27a8d2e --- /dev/null +++ b/lang/funcs/string_test.go @@ -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) + } + }) + } +} diff --git a/lang/funcs/testdata/hello.txt b/lang/funcs/testdata/hello.txt new file mode 100644 index 000000000..5e1c309da --- /dev/null +++ b/lang/funcs/testdata/hello.txt @@ -0,0 +1 @@ +Hello World \ No newline at end of file diff --git a/lang/funcs/testdata/icon.png b/lang/funcs/testdata/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a474f146faee923bb5a874fd6cb0809bba8ce59a GIT binary patch literal 806 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>332`Z|9_0%)40GV zF@DcL$p2}y=Ubpitp8&MhX0*{}xJ`pM2qX`E&9==A3uc!ft=>%4P5WGfl)`YxdpUI{lIg0_&Wz#k($7 zo&cJ_nB?v5!qCAg>jC6&7I;J!GcYhO1YyQK)BiRD1=&kHeO=ifvakvX@QeBStOg1- zdAc};NL;QxcT((NfB?${rwKETE4ZAQSGn%{|9Us~VioSaFN4bimpAx`ojh&%(@E&W zqSu)kdmEp2XmPe^t!kl{4g!gau6mWI#P8nbu~o$zed z!)MFH-|^qqZ7b-_%*MZSy$pMx77{OkBH{Opb3j`*jCb%3r>Epd$~Nl7e8 zwMs5Z1yT$~28QOk1}3@&rXhwFR)%I)hNjvEMpgy}o2Q))MbVI(pOTqYiCe>=)5R}= z8YDqB1m~xflqVLYGL)B>>t*I;7bhncr0V4trO$q6BL!5%4N?@6S(1~=;9itpS};vs zsRt+=UKJ8i5|mi3P*9YgmYI{PP*Pcts*qVwlFYzRG3W6o9*)8=4UJR&r_Xpk4Pszc z=GIH*7FHJao-D#Ftl-jMayW%qd2@)u=^Iy09657D<_P=g29E_^dJM0`1xr3TnN9^- O!QkoY=d#Wzp$P!sd@q*( literal 0 HcmV?d00001 diff --git a/lang/functions.go b/lang/functions.go index 797a27410..731fcd312 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -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 }