-
Notifications
You must be signed in to change notification settings - Fork 10
/
collection.go
709 lines (643 loc) · 21.1 KB
/
collection.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
package funcs
import (
"errors"
"fmt"
"math/big"
"sort"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/function"
"github.com/zclconf/go-cty/cty/function/stdlib"
"github.com/zclconf/go-cty/cty/gocty"
)
var LengthFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "value",
Type: cty.DynamicPseudoType,
AllowDynamicType: true,
AllowUnknown: true,
AllowMarked: true,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
collTy := args[0].Type()
switch {
case collTy == cty.String || collTy.IsTupleType() || collTy.IsObjectType() || collTy.IsListType() || collTy.IsMapType() || collTy.IsSetType() || collTy == cty.DynamicPseudoType:
return cty.Number, nil
default:
return cty.Number, errors.New("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()
marks := coll.Marks()
switch {
case collTy == cty.DynamicPseudoType:
return cty.UnknownVal(cty.Number).WithMarks(marks), nil
case collTy.IsTupleType():
l := len(collTy.TupleElementTypes())
return cty.NumberIntVal(int64(l)).WithMarks(marks), nil
case collTy.IsObjectType():
l := len(collTy.AttributeTypes())
return cty.NumberIntVal(int64(l)).WithMarks(marks), 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), errors.New("impossible value type for length(...)")
}
},
})
// AllTrueFunc constructs a function that returns true if all elements of the
// list are true. If the list is empty, return true.
var AllTrueFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.Bool),
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
result := cty.True
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
if !v.IsKnown() {
return cty.UnknownVal(cty.Bool), nil
}
if v.IsNull() {
return cty.False, nil
}
result = result.And(v)
if result.False() {
return cty.False, nil
}
}
return result, nil
},
})
// AnyTrueFunc constructs a function that returns true if any element of the
// list is true. If the list is empty, return false.
var AnyTrueFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.List(cty.Bool),
},
},
Type: function.StaticReturnType(cty.Bool),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
result := cty.False
var hasUnknown bool
for it := args[0].ElementIterator(); it.Next(); {
_, v := it.Element()
if !v.IsKnown() {
hasUnknown = true
continue
}
if v.IsNull() {
continue
}
result = result.Or(v)
if result.True() {
return cty.True, nil
}
}
if hasUnknown {
return cty.UnknownVal(cty.Bool), nil
}
return result, nil
},
})
// CoalesceFunc constructs a function that takes any number of arguments and
// returns the first one that isn't empty. This function was copied from go-cty
// stdlib and modified so that it returns the first *non-empty* non-null element
// from a sequence, instead of merely the first non-null.
var CoalesceFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
argTypes := make([]cty.Type, len(args))
for i, val := range args {
argTypes[i] = val.Type()
}
retType, _ := convert.UnifyUnsafe(argTypes)
if retType == cty.NilType {
return cty.NilType, errors.New("all arguments must have the same type")
}
return retType, nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
for _, argVal := range args {
// We already know this will succeed because of the checks in our Type func above
argVal, _ = convert.Convert(argVal, retType)
if !argVal.IsKnown() {
return cty.UnknownVal(retType), nil
}
if argVal.IsNull() {
continue
}
if retType == cty.String && argVal.RawEquals(cty.StringVal("")) {
continue
}
return argVal, nil
}
return cty.NilVal, errors.New("no non-null, non-empty-string arguments")
},
})
// IndexFunc constructs a function that finds the element index for a given value in a list.
var IndexFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
{
Name: "value",
Type: cty.DynamicPseudoType,
},
},
Type: function.StaticReturnType(cty.Number),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
if !(args[0].Type().IsListType() || args[0].Type().IsTupleType()) {
return cty.NilVal, errors.New("argument must be a list or tuple")
}
if !args[0].IsKnown() {
return cty.UnknownVal(cty.Number), nil
}
if args[0].LengthInt() == 0 { // Easy path
return cty.NilVal, errors.New("cannot search an empty list")
}
for it := args[0].ElementIterator(); it.Next(); {
i, v := it.Element()
eq, err := stdlib.Equal(v, args[1])
if err != nil {
return cty.NilVal, err
}
if !eq.IsKnown() {
return cty.UnknownVal(cty.Number), nil
}
if eq.True() {
return i, nil
}
}
return cty.NilVal, errors.New("item not found")
},
})
// LookupFunc constructs a function that performs dynamic lookups of map types.
var LookupFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "inputMap",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
{
Name: "key",
Type: cty.String,
AllowMarked: true,
},
},
VarParam: &function.Parameter{
Name: "default",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
AllowMarked: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
if len(args) < 1 || len(args) > 3 {
return cty.NilType, fmt.Errorf("lookup() takes two or three arguments, got %d", len(args))
}
ty := args[0].Type()
switch {
case ty.IsObjectType():
if !args[1].IsKnown() {
return cty.DynamicPseudoType, nil
}
keyVal, _ := args[1].Unmark()
key := keyVal.AsString()
if ty.HasAttribute(key) {
return args[0].GetAttr(key).Type(), nil
} else if len(args) == 3 {
// if the key isn't found but a default is provided,
// return the default type
return args[2].Type(), nil
}
return cty.DynamicPseudoType, function.NewArgErrorf(0, "the given object has no attribute %q", key)
case ty.IsMapType():
if len(args) == 3 {
_, err = convert.Convert(args[2], ty.ElementType())
if err != nil {
return cty.NilType, function.NewArgErrorf(2, "the default value must have the same type as the map elements")
}
}
return ty.ElementType(), nil
default:
return cty.NilType, function.NewArgErrorf(0, "lookup() requires a map as the first argument")
}
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
var defaultVal cty.Value
defaultValueSet := false
if len(args) == 3 {
// intentionally leave default value marked
defaultVal = args[2]
defaultValueSet = true
}
// keep track of marks from the collection and key
var markses []cty.ValueMarks
// unmark collection, retain marks to reapply later
mapVar, mapMarks := args[0].Unmark()
markses = append(markses, mapMarks)
// include marks on the key in the result
keyVal, keyMarks := args[1].Unmark()
if len(keyMarks) > 0 {
markses = append(markses, keyMarks)
}
lookupKey := keyVal.AsString()
if !mapVar.IsKnown() {
return cty.UnknownVal(retType).WithMarks(markses...), nil
}
if mapVar.Type().IsObjectType() {
if mapVar.Type().HasAttribute(lookupKey) {
return mapVar.GetAttr(lookupKey).WithMarks(markses...), nil
}
} else if mapVar.HasIndex(cty.StringVal(lookupKey)) == cty.True {
return mapVar.Index(cty.StringVal(lookupKey)).WithMarks(markses...), nil
}
if defaultValueSet {
defaultVal, err = convert.Convert(defaultVal, retType)
if err != nil {
return cty.NilVal, err
}
return defaultVal.WithMarks(markses...), nil
}
return cty.UnknownVal(cty.DynamicPseudoType).WithMarks(markses...), fmt.Errorf(
"lookup failed to find '%s'", lookupKey)
},
})
// MatchkeysFunc constructs a function that constructs a new list by taking a
// subset of elements from one list whose indexes match the corresponding
// indexes of values in another list.
var MatchkeysFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "values",
Type: cty.List(cty.DynamicPseudoType),
},
{
Name: "keys",
Type: cty.List(cty.DynamicPseudoType),
},
{
Name: "searchset",
Type: cty.List(cty.DynamicPseudoType),
},
},
Type: func(args []cty.Value) (cty.Type, error) {
ty, _ := convert.UnifyUnsafe([]cty.Type{args[1].Type(), args[2].Type()})
if ty == cty.NilType {
return cty.NilType, errors.New("keys and searchset must be of the same type")
}
// the return type is based on args[0] (values)
return args[0].Type(), nil
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
if !args[0].IsKnown() {
return cty.UnknownVal(cty.List(retType.ElementType())), nil
}
if args[0].LengthInt() != args[1].LengthInt() {
return cty.ListValEmpty(retType.ElementType()), errors.New("length of keys and values should be equal")
}
output := make([]cty.Value, 0)
values := args[0]
// Keys and searchset must be the same type.
// We can skip error checking here because we've already verified that
// they can be unified in the Type function
ty, _ := convert.UnifyUnsafe([]cty.Type{args[1].Type(), args[2].Type()})
keys, _ := convert.Convert(args[1], ty)
searchset, _ := convert.Convert(args[2], ty)
// if searchset is empty, return an empty list.
if searchset.LengthInt() == 0 {
return cty.ListValEmpty(retType.ElementType()), nil
}
if !values.IsWhollyKnown() || !keys.IsWhollyKnown() {
return cty.UnknownVal(retType), nil
}
i := 0
for it := keys.ElementIterator(); it.Next(); {
_, key := it.Element()
for iter := searchset.ElementIterator(); iter.Next(); {
_, search := iter.Element()
eq, err := stdlib.Equal(key, search)
if err != nil {
return cty.NilVal, err
}
if !eq.IsKnown() {
return cty.ListValEmpty(retType.ElementType()), nil
}
if eq.True() {
v := values.Index(cty.NumberIntVal(int64(i)))
output = append(output, v)
break
}
}
i++
}
// if we haven't matched any key, then output is an empty list.
if len(output) == 0 {
return cty.ListValEmpty(retType.ElementType()), nil
}
return cty.ListVal(output), nil
},
})
// OneFunc returns either the first element of a one-element list, or null
// if given a zero-element list.
var OneFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
},
Type: func(args []cty.Value) (cty.Type, error) {
ty := args[0].Type()
switch {
case ty.IsListType() || ty.IsSetType():
return ty.ElementType(), nil
case ty.IsTupleType():
etys := ty.TupleElementTypes()
switch len(etys) {
case 0:
// No specific type information, so we'll ultimately return
// a null value of unknown type.
return cty.DynamicPseudoType, nil
case 1:
return etys[0], nil
}
}
return cty.NilType, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements")
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
val := args[0]
ty := val.Type()
// Our parameter spec above doesn't set AllowUnknown or AllowNull,
// so we can assume our top-level collection is both known and non-null
// in here.
switch {
case ty.IsListType() || ty.IsSetType():
lenVal := val.Length()
if !lenVal.IsKnown() {
return cty.UnknownVal(retType), nil
}
var l int
err := gocty.FromCtyValue(lenVal, &l)
if err != nil {
// It would be very strange to get here, because that would
// suggest that the length is either not a number or isn't
// an integer, which would suggest a bug in cty.
return cty.NilVal, fmt.Errorf("invalid collection length: %s", err)
}
switch l {
case 0:
return cty.NullVal(retType), nil
case 1:
var ret cty.Value
// We'll use an iterator here because that works for both lists
// and sets, whereas indexing directly would only work for lists.
// Since we've just checked the length, we should only actually
// run this loop body once.
for it := val.ElementIterator(); it.Next(); {
_, ret = it.Element()
}
return ret, nil
}
case ty.IsTupleType():
etys := ty.TupleElementTypes()
switch len(etys) {
case 0:
return cty.NullVal(retType), nil
case 1:
ret := val.Index(cty.NumberIntVal(0))
return ret, nil
}
}
return cty.NilVal, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements")
},
})
// SumFunc constructs a function that returns the sum of all
// numbers provided in a list
var SumFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "list",
Type: cty.DynamicPseudoType,
},
},
Type: function.StaticReturnType(cty.Number),
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, fmt.Sprintf("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")
}
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
},
})
// TransposeFunc constructs a function that takes a map of lists of strings and
// swaps the keys and values to produce a new map of lists of strings.
var TransposeFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "values",
Type: cty.Map(cty.List(cty.String)),
},
},
Type: function.StaticReturnType(cty.Map(cty.List(cty.String))),
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
inputMap := args[0]
if !inputMap.IsWhollyKnown() {
return cty.UnknownVal(retType), nil
}
outputMap := make(map[string]cty.Value)
tmpMap := make(map[string][]string)
for it := inputMap.ElementIterator(); it.Next(); {
inKey, inVal := it.Element()
for iter := inVal.ElementIterator(); iter.Next(); {
_, val := iter.Element()
if !val.Type().Equals(cty.String) {
return cty.MapValEmpty(cty.List(cty.String)), errors.New("input must be a map of lists of strings")
}
outKey := val.AsString()
if _, ok := tmpMap[outKey]; !ok {
tmpMap[outKey] = make([]string, 0)
}
outVal := tmpMap[outKey]
outVal = append(outVal, inKey.AsString())
sort.Strings(outVal)
tmpMap[outKey] = outVal
}
}
for outKey, outVal := range tmpMap {
values := make([]cty.Value, 0)
for _, v := range outVal {
values = append(values, cty.StringVal(v))
}
outputMap[outKey] = cty.ListVal(values)
}
if len(outputMap) == 0 {
return cty.MapValEmpty(cty.List(cty.String)), nil
}
return cty.MapVal(outputMap), nil
},
})
// ListFunc constructs a function that takes an arbitrary number of arguments
// and returns a list containing those values in the same order.
//
// This function is deprecated in Terraform v0.12
var ListFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
return cty.DynamicPseudoType, fmt.Errorf("the \"list\" function was deprecated in Terraform v0.12 and is no longer available; use tolist([ ... ]) syntax to write a literal list")
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return cty.DynamicVal, fmt.Errorf("the \"list\" function was deprecated in Terraform v0.12 and is no longer available; use tolist([ ... ]) syntax to write a literal list")
},
})
// MapFunc constructs a function that takes an even number of arguments and
// returns a map whose elements are constructed from consecutive pairs of arguments.
//
// This function is deprecated in Terraform v0.12
var MapFunc = function.New(&function.Spec{
Params: []function.Parameter{},
VarParam: &function.Parameter{
Name: "vals",
Type: cty.DynamicPseudoType,
AllowUnknown: true,
AllowDynamicType: true,
AllowNull: true,
},
Type: func(args []cty.Value) (ret cty.Type, err error) {
return cty.DynamicPseudoType, fmt.Errorf("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map")
},
Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
return cty.DynamicVal, fmt.Errorf("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map")
},
})
// 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})
}
// AllTrue returns true if all elements of the list are true. If the list is empty,
// return true.
func AllTrue(collection cty.Value) (cty.Value, error) {
return AllTrueFunc.Call([]cty.Value{collection})
}
// AnyTrue returns true if any element of the list is true. If the list is empty,
// return false.
func AnyTrue(collection cty.Value) (cty.Value, error) {
return AnyTrueFunc.Call([]cty.Value{collection})
}
// Coalesce takes any number of arguments and returns the first one that isn't empty.
func Coalesce(args ...cty.Value) (cty.Value, error) {
return CoalesceFunc.Call(args)
}
// Index finds the element index for a given value in a list.
func Index(list, value cty.Value) (cty.Value, error) {
return IndexFunc.Call([]cty.Value{list, value})
}
// List takes any number of list arguments and returns a list containing those
// values in the same order.
func List(args ...cty.Value) (cty.Value, error) {
return ListFunc.Call(args)
}
// Lookup performs a dynamic lookup into a map.
// There are two required arguments, map and key, plus an optional default,
// which is a value to return if no key is found in map.
func Lookup(args ...cty.Value) (cty.Value, error) {
return LookupFunc.Call(args)
}
// Map takes an even number of arguments and returns a map whose elements are constructed
// from consecutive pairs of arguments.
func Map(args ...cty.Value) (cty.Value, error) {
return MapFunc.Call(args)
}
// Matchkeys constructs a new list by taking a subset of elements from one list
// whose indexes match the corresponding indexes of values in another list.
func Matchkeys(values, keys, searchset cty.Value) (cty.Value, error) {
return MatchkeysFunc.Call([]cty.Value{values, keys, searchset})
}
// One returns either the first element of a one-element list, or null
// if given a zero-element list..
func One(list cty.Value) (cty.Value, error) {
return OneFunc.Call([]cty.Value{list})
}
// Sum adds numbers in a list, set, or tuple
func Sum(list cty.Value) (cty.Value, error) {
return SumFunc.Call([]cty.Value{list})
}
// Transpose takes a map of lists of strings and swaps the keys and values to
// produce a new map of lists of strings.
func Transpose(values cty.Value) (cty.Value, error) {
return TransposeFunc.Call([]cty.Value{values})
}