mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-26 08:51:02 -06:00
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:
parent
e528fe50e9
commit
129f5fe74d
120
lang/funcs/collection.go
Normal file
120
lang/funcs/collection.go
Normal 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})
|
||||
}
|
224
lang/funcs/collection_test.go
Normal file
224
lang/funcs/collection_test.go
Normal 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
29
lang/funcs/crypto.go
Normal 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
17
lang/funcs/crypto_test.go
Normal 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
88
lang/funcs/filesystem.go
Normal 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})
|
||||
}
|
98
lang/funcs/filesystem_test.go
Normal file
98
lang/funcs/filesystem_test.go
Normal 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
132
lang/funcs/string.go
Normal 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
246
lang/funcs/string_test.go
Normal 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
1
lang/funcs/testdata/hello.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
Hello World
|
BIN
lang/funcs/testdata/icon.png
vendored
Normal file
BIN
lang/funcs/testdata/icon.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 806 B |
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user