Skip to content

Commit 34b700f

Browse files
Add Length() function to collection values (#1187)
* Add Length() function to all collection types * Add tests * Doc update Signed-off-by: Zhiwei Liang <zhiwei.liang@zliang.me> * Add options Signed-off-by: Zhiwei Liang <zhiwei.liang@zliang.me> * Add changelog entries --------- Signed-off-by: Zhiwei Liang <zhiwei.liang@zliang.me> Co-authored-by: Selena Goods <github@simplebox.anonaddy.com>
1 parent 53ec28d commit 34b700f

13 files changed

+414
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
body: 'types/basetypes: Added `Length()` method for the `ListValue` type.'
3+
time: 2026-01-27T12:28:35.393455-05:00
4+
custom:
5+
Issue: "1067"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
body: 'types/basetypes: Added `Length()` method for the `MapValue` type.'
3+
time: 2026-01-27T12:29:47.763838-05:00
4+
custom:
5+
Issue: "1067"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
body: 'types/basetypes: Added `Length()` method for the `SetValue` type.'
3+
time: 2026-01-27T12:29:59.948117-05:00
4+
custom:
5+
Issue: "1067"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: ENHANCEMENTS
2+
body: 'types/basetypes: Added `Length()` method for the `TupleValue` type.'
3+
time: 2026-01-27T12:30:33.573716-05:00
4+
custom:
5+
Issue: "1067"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright IBM Corp. 2021, 2025
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package basetypes
5+
6+
// CollectionLengthOptions is a collection of toggles to control the behavior
7+
// of the Length method on collection types (List, Set, Map, Tuple).
8+
type CollectionLengthOptions struct {
9+
// UnhandledNullAsZero controls what happens when Length is called on a
10+
// null value. When set to true, zero will be returned. When set to false,
11+
// a panic will occur.
12+
UnhandledNullAsZero bool
13+
14+
// UnhandledUnknownAsZero controls what happens when Length is called on
15+
// an unknown value. When set to true, zero will be returned. When set to
16+
// false, a panic will occur.
17+
UnhandledUnknownAsZero bool
18+
}

types/basetypes/list_value.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,23 @@ func (l ListValue) Elements() []attr.Value {
173173
return result
174174
}
175175

176+
// Length returns the number of elements in the List.
177+
//
178+
// If the List is null or unknown, the behavior depends on the options:
179+
// - If UnhandledNullAsZero or UnhandledUnknownAsZero is true, zero is returned.
180+
// - If false, a panic occurs.
181+
func (l ListValue) Length(opts CollectionLengthOptions) int {
182+
if l.IsNull() && !opts.UnhandledNullAsZero {
183+
panic("cannot call Length on a null List")
184+
}
185+
186+
if l.IsUnknown() && !opts.UnhandledUnknownAsZero {
187+
panic("cannot call Length on an unknown List")
188+
}
189+
190+
return len(l.elements)
191+
}
192+
176193
// ElementsAs populates `target` with the elements of the ListValue, throwing an
177194
// error if the elements cannot be stored in `target`.
178195
func (l ListValue) ElementsAs(ctx context.Context, target interface{}, allowUnhandled bool) diag.Diagnostics {

types/basetypes/list_value_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,83 @@ func TestListValueType(t *testing.T) {
841841
}
842842
}
843843

844+
func TestListValueLength(t *testing.T) {
845+
t.Parallel()
846+
847+
testCases := map[string]struct {
848+
input ListValue
849+
opts CollectionLengthOptions
850+
expected int
851+
}{
852+
"known-empty": {
853+
input: NewListValueMust(StringType{}, []attr.Value{}),
854+
opts: CollectionLengthOptions{},
855+
expected: 0,
856+
},
857+
"known-single": {
858+
input: NewListValueMust(StringType{}, []attr.Value{NewStringValue("test")}),
859+
opts: CollectionLengthOptions{},
860+
expected: 1,
861+
},
862+
"known-multiple": {
863+
input: NewListValueMust(StringType{}, []attr.Value{
864+
NewStringValue("hello"),
865+
NewStringValue("world"),
866+
}),
867+
opts: CollectionLengthOptions{},
868+
expected: 2,
869+
},
870+
"null-unhandled-as-zero": {
871+
input: NewListNull(StringType{}),
872+
opts: CollectionLengthOptions{UnhandledNullAsZero: true},
873+
expected: 0,
874+
},
875+
"unknown-unhandled-as-zero": {
876+
input: NewListUnknown(StringType{}),
877+
opts: CollectionLengthOptions{UnhandledUnknownAsZero: true},
878+
expected: 0,
879+
},
880+
}
881+
882+
for name, testCase := range testCases {
883+
t.Run(name, func(t *testing.T) {
884+
t.Parallel()
885+
886+
got := testCase.input.Length(testCase.opts)
887+
888+
if got != testCase.expected {
889+
t.Errorf("Expected %d, got %d", testCase.expected, got)
890+
}
891+
})
892+
}
893+
}
894+
895+
func TestListValueLength_PanicOnNull(t *testing.T) {
896+
t.Parallel()
897+
898+
defer func() {
899+
if r := recover(); r == nil {
900+
t.Errorf("Expected panic when calling Length on null List with UnhandledNullAsZero=false")
901+
}
902+
}()
903+
904+
list := NewListNull(StringType{})
905+
list.Length(CollectionLengthOptions{UnhandledNullAsZero: false})
906+
}
907+
908+
func TestListValueLength_PanicOnUnknown(t *testing.T) {
909+
t.Parallel()
910+
911+
defer func() {
912+
if r := recover(); r == nil {
913+
t.Errorf("Expected panic when calling Length on unknown List with UnhandledUnknownAsZero=false")
914+
}
915+
}()
916+
917+
list := NewListUnknown(StringType{})
918+
list.Length(CollectionLengthOptions{UnhandledUnknownAsZero: false})
919+
}
920+
844921
func TestListTypeValidate(t *testing.T) {
845922
t.Parallel()
846923

types/basetypes/map_value.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,23 @@ func (m MapValue) Elements() map[string]attr.Value {
178178
return result
179179
}
180180

181+
// Length returns the number of elements in the Map.
182+
//
183+
// If the Map is null or unknown, the behavior depends on the options:
184+
// - If UnhandledNullAsZero or UnhandledUnknownAsZero is true, zero is returned.
185+
// - If false, a panic occurs.
186+
func (m MapValue) Length(opts CollectionLengthOptions) int {
187+
if m.IsNull() && !opts.UnhandledNullAsZero {
188+
panic("cannot call Length on a null Map")
189+
}
190+
191+
if m.IsUnknown() && !opts.UnhandledUnknownAsZero {
192+
panic("cannot call Length on an unknown Map")
193+
}
194+
195+
return len(m.elements)
196+
}
197+
181198
// ElementsAs populates `target` with the elements of the MapValue, throwing an
182199
// error if the elements cannot be stored in `target`.
183200
func (m MapValue) ElementsAs(ctx context.Context, target interface{}, allowUnhandled bool) diag.Diagnostics {

types/basetypes/map_value_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,83 @@ func TestMapValueType(t *testing.T) {
856856
}
857857
}
858858

859+
func TestMapValueLength(t *testing.T) {
860+
t.Parallel()
861+
862+
testCases := map[string]struct {
863+
input MapValue
864+
opts CollectionLengthOptions
865+
expected int
866+
}{
867+
"known-empty": {
868+
input: NewMapValueMust(StringType{}, map[string]attr.Value{}),
869+
opts: CollectionLengthOptions{},
870+
expected: 0,
871+
},
872+
"known-single": {
873+
input: NewMapValueMust(StringType{}, map[string]attr.Value{"key": NewStringValue("test")}),
874+
opts: CollectionLengthOptions{},
875+
expected: 1,
876+
},
877+
"known-multiple": {
878+
input: NewMapValueMust(StringType{}, map[string]attr.Value{
879+
"key1": NewStringValue("hello"),
880+
"key2": NewStringValue("world"),
881+
}),
882+
opts: CollectionLengthOptions{},
883+
expected: 2,
884+
},
885+
"null-unhandled-as-zero": {
886+
input: NewMapNull(StringType{}),
887+
opts: CollectionLengthOptions{UnhandledNullAsZero: true},
888+
expected: 0,
889+
},
890+
"unknown-unhandled-as-zero": {
891+
input: NewMapUnknown(StringType{}),
892+
opts: CollectionLengthOptions{UnhandledUnknownAsZero: true},
893+
expected: 0,
894+
},
895+
}
896+
897+
for name, testCase := range testCases {
898+
t.Run(name, func(t *testing.T) {
899+
t.Parallel()
900+
901+
got := testCase.input.Length(testCase.opts)
902+
903+
if got != testCase.expected {
904+
t.Errorf("Expected %d, got %d", testCase.expected, got)
905+
}
906+
})
907+
}
908+
}
909+
910+
func TestMapValueLength_PanicOnNull(t *testing.T) {
911+
t.Parallel()
912+
913+
defer func() {
914+
if r := recover(); r == nil {
915+
t.Errorf("Expected panic when calling Length on null Map with UnhandledNullAsZero=false")
916+
}
917+
}()
918+
919+
m := NewMapNull(StringType{})
920+
m.Length(CollectionLengthOptions{UnhandledNullAsZero: false})
921+
}
922+
923+
func TestMapValueLength_PanicOnUnknown(t *testing.T) {
924+
t.Parallel()
925+
926+
defer func() {
927+
if r := recover(); r == nil {
928+
t.Errorf("Expected panic when calling Length on unknown Map with UnhandledUnknownAsZero=false")
929+
}
930+
}()
931+
932+
m := NewMapUnknown(StringType{})
933+
m.Length(CollectionLengthOptions{UnhandledUnknownAsZero: false})
934+
}
935+
859936
func TestMapTypeValidate(t *testing.T) {
860937
t.Parallel()
861938

types/basetypes/set_value.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,23 @@ func (s SetValue) Elements() []attr.Value {
173173
return result
174174
}
175175

176+
// Length returns the number of elements in the Set.
177+
//
178+
// If the Set is null or unknown, the behavior depends on the options:
179+
// - If UnhandledNullAsZero or UnhandledUnknownAsZero is true, zero is returned.
180+
// - If false, a panic occurs.
181+
func (s SetValue) Length(opts CollectionLengthOptions) int {
182+
if s.IsNull() && !opts.UnhandledNullAsZero {
183+
panic("cannot call Length on a null Set")
184+
}
185+
186+
if s.IsUnknown() && !opts.UnhandledUnknownAsZero {
187+
panic("cannot call Length on an unknown Set")
188+
}
189+
190+
return len(s.elements)
191+
}
192+
176193
// ElementsAs populates `target` with the elements of the SetValue, throwing an
177194
// error if the elements cannot be stored in `target`.
178195
func (s SetValue) ElementsAs(ctx context.Context, target interface{}, allowUnhandled bool) diag.Diagnostics {

0 commit comments

Comments
 (0)