Skip to content

Commit dfefeb8

Browse files
authored
tftypes: add tftypes.Value.IsFullyNull() (#541)
* tftypes: add tftypes.IsFullyNull() * Tests for List types * Add tests for partially null and for sets * Add comment * refactor * test cases for primitives and dynamic * add changelog * lint
1 parent 5a14a89 commit dfefeb8

File tree

3 files changed

+237
-0
lines changed

3 files changed

+237
-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: 'tftypes: `tftypes.Value.IsFullyNull()` allows SDKs to determine when a value is null or consists of only null elements and attributes.'
3+
time: 2025-07-29T11:27:48.486954-04:00
4+
custom:
5+
Issue: "541"

tftypes/value.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,46 @@ func (val Value) IsNull() bool {
574574
return val.value == nil
575575
}
576576

577+
// IsFullyNull returns true if the Value is null or if the Value is an
578+
// aggregate that consists of only fully null elements and attributes.
579+
func (val Value) IsFullyNull() bool {
580+
if val.IsNull() {
581+
return true
582+
}
583+
584+
switch val.Type().(type) {
585+
case primitive:
586+
return false // already checked IsNull() and not an aggregate
587+
588+
case List, Set, Tuple:
589+
sliceVal, ok := val.value.([]Value)
590+
if !ok {
591+
panic(fmt.Sprintf("impossible type assertion failure: %T to slice", val))
592+
}
593+
for _, v := range sliceVal {
594+
if !v.IsFullyNull() {
595+
return false
596+
}
597+
}
598+
return true
599+
600+
case Map, Object:
601+
mapVal, ok := val.value.(map[string]Value)
602+
if !ok {
603+
panic(fmt.Sprintf("impossible type assertion failure: %T to map", val))
604+
}
605+
for _, v := range mapVal {
606+
if !v.IsFullyNull() {
607+
return false
608+
}
609+
}
610+
return true
611+
612+
default:
613+
panic(fmt.Sprintf("unknown type %T", val.Type()))
614+
}
615+
}
616+
577617
// MarshalMsgPack returns a msgpack representation of the Value. This is used
578618
// for constructing tfprotov5.DynamicValues.
579619
//

tftypes/value_test.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,198 @@ func TestValueIsKnown(t *testing.T) {
723723
}
724724
}
725725

726+
func TestValueIsNull(t *testing.T) {
727+
t.Parallel()
728+
type testCase struct {
729+
value Value
730+
expectedIsNull bool
731+
expectedIsFullyNull bool
732+
}
733+
734+
simpleObjectTyp := Object{
735+
AttributeTypes: map[string]Type{
736+
"capacity": Number,
737+
},
738+
OptionalAttributes: map[string]struct{}{
739+
"capacity": {},
740+
},
741+
}
742+
743+
simpleListTyp := List{
744+
ElementType: String,
745+
}
746+
747+
simpleSetTyp := Set{
748+
ElementType: String,
749+
}
750+
751+
networkTyp := Object{
752+
AttributeTypes: map[string]Type{
753+
"name": String,
754+
"speed": Number,
755+
"labels": simpleListTyp,
756+
},
757+
}
758+
759+
objectTyp := Object{
760+
AttributeTypes: map[string]Type{
761+
"id": String,
762+
"network": networkTyp,
763+
},
764+
}
765+
766+
listTyp := List{
767+
ElementType: networkTyp,
768+
}
769+
770+
tests := map[string]testCase{
771+
"primitive": {
772+
value: NewValue(Number, 990),
773+
expectedIsNull: false,
774+
expectedIsFullyNull: false,
775+
},
776+
"primitive-null": {
777+
value: NewValue(Number, nil),
778+
expectedIsNull: true,
779+
expectedIsFullyNull: true,
780+
},
781+
"dynamic": {
782+
value: NewValue(DynamicPseudoType, "hello"),
783+
expectedIsNull: false,
784+
expectedIsFullyNull: false,
785+
},
786+
"dynamic-null": {
787+
value: NewValue(DynamicPseudoType, nil),
788+
expectedIsNull: true,
789+
expectedIsFullyNull: true,
790+
},
791+
"simple-object": {
792+
value: NewValue(simpleObjectTyp, map[string]Value{"capacity": NewValue(Number, 4096)}),
793+
expectedIsNull: false,
794+
expectedIsFullyNull: false,
795+
},
796+
"simple-object-null": {
797+
value: NewValue(simpleObjectTyp, nil),
798+
expectedIsNull: true,
799+
expectedIsFullyNull: true,
800+
},
801+
"simple-object-with-empty-attributes-map": {
802+
value: NewValue(simpleObjectTyp, map[string]Value{}),
803+
expectedIsNull: false,
804+
expectedIsFullyNull: true,
805+
},
806+
"simple-object-with-nil-primitive": {
807+
value: NewValue(simpleObjectTyp, map[string]Value{"capacity": NewValue(Number, nil)}),
808+
expectedIsNull: false,
809+
expectedIsFullyNull: true,
810+
},
811+
"object-with-no-nils": {
812+
value: NewValue(objectTyp, map[string]Value{
813+
"id": NewValue(String, "#00decaf"),
814+
"network": NewValue(networkTyp, map[string]Value{
815+
"name": NewValue(String, "eth0"),
816+
"speed": NewValue(Number, 1000000000),
817+
"labels": NewValue(simpleListTyp, []Value{
818+
NewValue(String, "connected"),
819+
}),
820+
}),
821+
}),
822+
expectedIsNull: false,
823+
expectedIsFullyNull: false,
824+
},
825+
"object-with-shallow-nils": {
826+
value: NewValue(objectTyp, map[string]Value{
827+
"id": NewValue(String, nil),
828+
"network": NewValue(networkTyp, nil),
829+
}),
830+
expectedIsNull: false,
831+
expectedIsFullyNull: true,
832+
},
833+
"object-with-deep-nils": {
834+
value: NewValue(objectTyp, map[string]Value{
835+
"id": NewValue(String, nil),
836+
"network": NewValue(networkTyp, map[string]Value{
837+
"name": NewValue(String, nil),
838+
"speed": NewValue(Number, nil),
839+
"labels": NewValue(simpleListTyp, []Value{
840+
NewValue(String, nil),
841+
}),
842+
}),
843+
}),
844+
expectedIsNull: false,
845+
expectedIsFullyNull: true,
846+
},
847+
"object-with-some-nils": {
848+
value: NewValue(objectTyp, map[string]Value{
849+
"id": NewValue(String, nil),
850+
"network": NewValue(networkTyp, map[string]Value{
851+
"name": NewValue(String, nil),
852+
"speed": NewValue(Number, 1000),
853+
"labels": NewValue(simpleListTyp, []Value{
854+
NewValue(String, nil),
855+
}),
856+
}),
857+
}),
858+
expectedIsNull: false,
859+
expectedIsFullyNull: false,
860+
},
861+
"simple-list-with-no-nils": {
862+
value: NewValue(simpleListTyp, []Value{
863+
NewValue(String, "restarting"),
864+
}),
865+
expectedIsNull: false,
866+
expectedIsFullyNull: false,
867+
},
868+
"simple-list-with-some-nils": {
869+
value: NewValue(simpleListTyp, []Value{
870+
NewValue(String, "east-mars"),
871+
NewValue(String, "south-mars"),
872+
NewValue(String, nil),
873+
}),
874+
expectedIsNull: false,
875+
expectedIsFullyNull: false,
876+
},
877+
"simple-list-with-shallow-nils": {
878+
value: NewValue(simpleListTyp, []Value{
879+
NewValue(String, nil),
880+
}),
881+
expectedIsNull: false,
882+
expectedIsFullyNull: true,
883+
},
884+
"list-with-deep-nils": {
885+
value: NewValue(listTyp, []Value{
886+
NewValue(networkTyp, nil),
887+
}),
888+
expectedIsNull: false,
889+
expectedIsFullyNull: true,
890+
},
891+
"simple-set-with-shallow-nils": {
892+
value: NewValue(simpleSetTyp, []Value{
893+
NewValue(String, nil),
894+
}),
895+
expectedIsNull: false,
896+
expectedIsFullyNull: true,
897+
},
898+
}
899+
900+
for name, test := range tests {
901+
t.Run(name, func(t *testing.T) {
902+
t.Parallel()
903+
904+
actualIsNull := test.value.IsNull()
905+
actualIsFullyNull := test.value.IsFullyNull()
906+
907+
if test.expectedIsNull != actualIsNull {
908+
t.Errorf("expected IsNull() to be %v; actual: %v", test.expectedIsNull, actualIsNull)
909+
}
910+
911+
if test.expectedIsFullyNull != actualIsFullyNull {
912+
t.Errorf("expected IsFullyNull() to be %v; actual: %v", test.expectedIsNull, actualIsNull)
913+
}
914+
})
915+
}
916+
}
917+
726918
func TestValueEqual(t *testing.T) {
727919
t.Parallel()
728920
type testCase struct {

0 commit comments

Comments
 (0)