From 5b9731ff0e2caee57f76906d3e8348f278dc816f Mon Sep 17 00:00:00 2001 From: magodo Date: Tue, 16 Apr 2024 14:20:54 +0800 Subject: [PATCH] Extend `attr.Value` interface to support `IsFullyNullableKnown()` This new method is similar to the `Value.IsFullyKnown()` that is available in the `github.com/hashicorp/terraform-plugin-go/tftypes`. The difference here is that in `tftypes`, each value can only has two states: a concrete value (including `nil`) or "unknown". While in the fw, each value can has three states: null, unknown and known. This is why the method name is chose so (as I can't figure out another better name, as `IsFullyNotKnown` or `IsPartiallyUnknown` are ambiguous than the current one, IMO). The reason for introducing this method is to allow provider developers to check the state of an aggregate value during the `ModifyPlan`, where the code might stop processing that property if its value contains any unknwon value. Currently, the developer has two solutions: - Convert the `ToTerraformValue()` to convert the `attr.Value` to `tftypes.Value`, then call its `IsFullyKnown()`. This works fine (and it is also used in the FW itself somewhere), while it is a bit over kill to do the conversion where the intent is only to check the whole (un)known-ness. - Self implement the `IsFullyKnown()` for the `attr.Value`, similar to: ```go func IsFullyKnown(val attr.Value) bool { if val == nil { return true } if val.IsUnknown() { return false } switch v := val.(type) { case types.Dynamic: return IsFullyKnown(v.UnderlyingValue()) case types.List: for _, e := range v.Elements() { if !IsFullyKnown(e) { return false } } return true case types.Set: for _, e := range v.Elements() { if !IsFullyKnown(e) { return false } } return true case types.Tuple: for _, e := range v.Elements() { if !IsFullyKnown(e) { return false } } return true case types.Map: for _, e := range v.Elements() { if !IsFullyKnown(e) { return false } } return true case types.Object: for _, e := range v.Attributes() { if !IsFullyKnown(e) { return false } } return true default: return true } } ``` This PR tries to put this common logic to the FW so that more developers can save the run/develop time effort for the same purpose. I chose to extend the `attr.Value` interface, instead of introducing a helper method in the `attr` package, as a random choice. If the latter looks better, then I can rework this PR. --- attr/value.go | 9 ++++++++- internal/testing/testtypes/bool.go | 4 ++++ internal/testing/testtypes/invalid.go | 4 ++++ internal/testing/testtypes/number.go | 4 ++++ .../testing/testtypes/numberwithvalidateattribute.go | 8 ++++++++ internal/testing/testtypes/string.go | 4 ++++ .../testing/testtypes/stringwithvalidateattribute.go | 8 ++++++++ .../testing/testtypes/stringwithvalidateparameter.go | 4 ++++ types/basetypes/bool_value.go | 5 +++++ types/basetypes/dynamic_value.go | 6 ++++++ types/basetypes/float64_value.go | 5 +++++ types/basetypes/int64_value.go | 5 +++++ types/basetypes/list_value.go | 11 +++++++++++ types/basetypes/map_value.go | 11 +++++++++++ types/basetypes/missing_value.go | 5 +++++ types/basetypes/number_value.go | 5 +++++ types/basetypes/object_value.go | 11 +++++++++++ types/basetypes/set_value.go | 11 +++++++++++ types/basetypes/string_value.go | 5 +++++ types/basetypes/tuple_value.go | 11 +++++++++++ 20 files changed, 135 insertions(+), 1 deletion(-) diff --git a/attr/value.go b/attr/value.go index b34a3bb73..4ce77e9de 100644 --- a/attr/value.go +++ b/attr/value.go @@ -57,9 +57,16 @@ type Value interface { // IsNull returns true if the Value is not set, or is explicitly set to null. IsNull() bool - // IsUnknown returns true if the value is not yet known. + // IsUnKnown returns true if the value is not yet known. + // If the value is an aggregate type, only the top level of the aggregate type + // is checked; elements and attributes are not checked. IsUnknown() bool + // IsFullyNullableKnown returns true if the value is nullable known. If the value + // is an aggregate type, IsFullyNullableKnown only returns true if all elements + // and attributes are nullable known, as well. + IsFullyNullableKnown() bool + // String returns a summary representation of either the underlying Value, // or UnknownValueString (``) when IsUnknown() returns true, // or NullValueString (``) when IsNull() return true. diff --git a/internal/testing/testtypes/bool.go b/internal/testing/testtypes/bool.go index 12e635f5b..62212deff 100644 --- a/internal/testing/testtypes/bool.go +++ b/internal/testing/testtypes/bool.go @@ -116,6 +116,10 @@ func (b Bool) IsUnknown() bool { return b.Bool.IsUnknown() } +func (b Bool) IsFullyNullableKnown() bool { + return b.Bool.IsFullyNullableKnown() +} + func (b Bool) String() string { return b.Bool.String() } diff --git a/internal/testing/testtypes/invalid.go b/internal/testing/testtypes/invalid.go index c6461d365..60b7dc47c 100644 --- a/internal/testing/testtypes/invalid.go +++ b/internal/testing/testtypes/invalid.go @@ -63,6 +63,10 @@ func (i Invalid) IsUnknown() bool { return false } +func (i Invalid) IsFullyNullableKnown() bool { + return false +} + func (i Invalid) String() string { return "" } diff --git a/internal/testing/testtypes/number.go b/internal/testing/testtypes/number.go index 9d4f3ca55..6075b1ea7 100644 --- a/internal/testing/testtypes/number.go +++ b/internal/testing/testtypes/number.go @@ -119,3 +119,7 @@ func (n Number) IsNull() bool { func (n Number) IsUnknown() bool { return n.Number.IsUnknown() } + +func (n Number) IsFullyNullableKnown() bool { + return n.Number.IsFullyNullableKnown() +} diff --git a/internal/testing/testtypes/numberwithvalidateattribute.go b/internal/testing/testtypes/numberwithvalidateattribute.go index 09067e74f..69fd4ea2e 100644 --- a/internal/testing/testtypes/numberwithvalidateattribute.go +++ b/internal/testing/testtypes/numberwithvalidateattribute.go @@ -75,6 +75,10 @@ func (v NumberValueWithValidateAttributeError) IsUnknown() bool { return v.InternalNumber.IsUnknown() } +func (v NumberValueWithValidateAttributeError) IsFullyNullableKnown() bool { + return v.InternalNumber.IsFullyNullableKnown() +} + func (v NumberValueWithValidateAttributeError) String() string { return v.InternalNumber.String() } @@ -145,6 +149,10 @@ func (v NumberValueWithValidateAttributeWarning) IsUnknown() bool { return v.InternalNumber.IsUnknown() } +func (v NumberValueWithValidateAttributeWarning) IsFullyNullableKnown() bool { + return v.InternalNumber.IsFullyNullableKnown() +} + func (v NumberValueWithValidateAttributeWarning) String() string { return v.InternalNumber.String() } diff --git a/internal/testing/testtypes/string.go b/internal/testing/testtypes/string.go index f1b06da81..f59c70535 100644 --- a/internal/testing/testtypes/string.go +++ b/internal/testing/testtypes/string.go @@ -127,6 +127,10 @@ func (s String) IsUnknown() bool { return s.InternalString.IsUnknown() } +func (s String) IsFullyNullableKnown() bool { + return s.InternalString.IsFullyNullableKnown() +} + func (s String) String() string { return s.InternalString.String() } diff --git a/internal/testing/testtypes/stringwithvalidateattribute.go b/internal/testing/testtypes/stringwithvalidateattribute.go index cb6440db0..f26984802 100644 --- a/internal/testing/testtypes/stringwithvalidateattribute.go +++ b/internal/testing/testtypes/stringwithvalidateattribute.go @@ -75,6 +75,10 @@ func (v StringValueWithValidateAttributeError) IsUnknown() bool { return v.InternalString.IsUnknown() } +func (v StringValueWithValidateAttributeError) IsFullyNullableKnown() bool { + return v.InternalString.IsFullyNullableKnown() +} + func (v StringValueWithValidateAttributeError) String() string { return v.InternalString.String() } @@ -145,6 +149,10 @@ func (v StringValueWithValidateAttributeWarning) IsUnknown() bool { return v.InternalString.IsUnknown() } +func (v StringValueWithValidateAttributeWarning) IsFullyNullableKnown() bool { + return v.InternalString.IsFullyNullableKnown() +} + func (v StringValueWithValidateAttributeWarning) String() string { return v.InternalString.String() } diff --git a/internal/testing/testtypes/stringwithvalidateparameter.go b/internal/testing/testtypes/stringwithvalidateparameter.go index 751bbcd47..1b4f871c6 100644 --- a/internal/testing/testtypes/stringwithvalidateparameter.go +++ b/internal/testing/testtypes/stringwithvalidateparameter.go @@ -75,6 +75,10 @@ func (v StringValueWithValidateParameterError) IsUnknown() bool { return v.InternalString.IsUnknown() } +func (v StringValueWithValidateParameterError) IsFullyNullableKnown() bool { + return v.InternalString.IsFullyNullableKnown() +} + func (v StringValueWithValidateParameterError) String() string { return v.InternalString.String() } diff --git a/types/basetypes/bool_value.go b/types/basetypes/bool_value.go index aa10b3981..0717c2bb6 100644 --- a/types/basetypes/bool_value.go +++ b/types/basetypes/bool_value.go @@ -138,6 +138,11 @@ func (b BoolValue) IsUnknown() bool { return b.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the Bool represents a currently nullable known value. +func (b BoolValue) IsFullyNullableKnown() bool { + return !b.IsUnknown() +} + // String returns a human-readable representation of the Bool value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. diff --git a/types/basetypes/dynamic_value.go b/types/basetypes/dynamic_value.go index 07946e18c..8bf058d36 100644 --- a/types/basetypes/dynamic_value.go +++ b/types/basetypes/dynamic_value.go @@ -140,6 +140,12 @@ func (v DynamicValue) IsUnknown() bool { return v.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the DynamicValue's underlying value +// represents a currently nullable known value. +func (v DynamicValue) IsFullyNullableKnown() bool { + return v.value == nil || v.value.IsFullyNullableKnown() +} + // String returns a human-readable representation of the DynamicValue. The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. func (v DynamicValue) String() string { diff --git a/types/basetypes/float64_value.go b/types/basetypes/float64_value.go index fb9c19a5e..1267ce143 100644 --- a/types/basetypes/float64_value.go +++ b/types/basetypes/float64_value.go @@ -174,6 +174,11 @@ func (f Float64Value) IsUnknown() bool { return f.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the Float64 represents a currently nullable known value. +func (f Float64Value) IsFullyNullableKnown() bool { + return !f.IsUnknown() +} + // String returns a human-readable representation of the Float64 value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. diff --git a/types/basetypes/int64_value.go b/types/basetypes/int64_value.go index bf8b3bd53..eb671920a 100644 --- a/types/basetypes/int64_value.go +++ b/types/basetypes/int64_value.go @@ -138,6 +138,11 @@ func (i Int64Value) IsUnknown() bool { return i.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the Int64 represents a currently nullable known value. +func (i Int64Value) IsFullyNullableKnown() bool { + return !i.IsUnknown() +} + // String returns a human-readable representation of the Int64 value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. diff --git a/types/basetypes/list_value.go b/types/basetypes/list_value.go index f89cbb73f..281dc2fa0 100644 --- a/types/basetypes/list_value.go +++ b/types/basetypes/list_value.go @@ -297,6 +297,17 @@ func (l ListValue) IsUnknown() bool { return l.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the List represents a currently nullable known value, +// including all its elements, recursively. +func (l ListValue) IsFullyNullableKnown() bool { + for _, elem := range l.elements { + if !elem.IsFullyNullableKnown() { + return false + } + } + return true +} + // String returns a human-readable representation of the List value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. diff --git a/types/basetypes/map_value.go b/types/basetypes/map_value.go index 56a64361c..0b41c9916 100644 --- a/types/basetypes/map_value.go +++ b/types/basetypes/map_value.go @@ -304,6 +304,17 @@ func (m MapValue) IsUnknown() bool { return m.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the Map represents a currently nullable known value, +// including all its elements, recursively. +func (m MapValue) IsFullyNullableKnown() bool { + for _, elem := range m.elements { + if !elem.IsFullyNullableKnown() { + return false + } + } + return true +} + // String returns a human-readable representation of the Map value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. diff --git a/types/basetypes/missing_value.go b/types/basetypes/missing_value.go index 37f600d7d..c2a918900 100644 --- a/types/basetypes/missing_value.go +++ b/types/basetypes/missing_value.go @@ -42,6 +42,11 @@ func (v missingValue) IsUnknown() bool { return false } +// IsFullyNullableKnown returns false. +func (v missingValue) IsFullyNullableKnown() bool { + return !v.IsUnknown() +} + // String returns a human-readable representation of the value. // // The string returned here is not protected by any compatibility guarantees, diff --git a/types/basetypes/number_value.go b/types/basetypes/number_value.go index 28c89de5d..befdb9e2e 100644 --- a/types/basetypes/number_value.go +++ b/types/basetypes/number_value.go @@ -138,6 +138,11 @@ func (n NumberValue) IsUnknown() bool { return n.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the Number represents a currently nullable known value. +func (n NumberValue) IsFullyNullableKnown() bool { + return !n.IsUnknown() +} + // String returns a human-readable representation of the Number value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. diff --git a/types/basetypes/object_value.go b/types/basetypes/object_value.go index baeb8c0eb..adbd035c3 100644 --- a/types/basetypes/object_value.go +++ b/types/basetypes/object_value.go @@ -361,6 +361,17 @@ func (o ObjectValue) IsUnknown() bool { return o.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the Object represents a currently nullable known value, +// including all its attributes, recursively. +func (o ObjectValue) IsFullyNullableKnown() bool { + for _, attr := range o.attributes { + if !attr.IsFullyNullableKnown() { + return false + } + } + return true +} + // String returns a human-readable representation of the Object value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. diff --git a/types/basetypes/set_value.go b/types/basetypes/set_value.go index 9beb2da3d..140b48e52 100644 --- a/types/basetypes/set_value.go +++ b/types/basetypes/set_value.go @@ -305,6 +305,17 @@ func (s SetValue) IsUnknown() bool { return s.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the Set represents a currently nullable known value, +// including all its elements, recursively. +func (s SetValue) IsFullyNullableKnown() bool { + for _, elem := range s.elements { + if !elem.IsFullyNullableKnown() { + return false + } + } + return true +} + // String returns a human-readable representation of the Set value. // The string returned here is not protected by any compatibility guarantees, // and is intended for logging and error reporting. diff --git a/types/basetypes/string_value.go b/types/basetypes/string_value.go index 46ac22485..7ac60492c 100644 --- a/types/basetypes/string_value.go +++ b/types/basetypes/string_value.go @@ -148,6 +148,11 @@ func (s StringValue) IsUnknown() bool { return s.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the String represents a currently nullable known value. +func (s StringValue) IsFullyNullableKnown() bool { + return s.state != attr.ValueStateUnknown +} + // String returns a human-readable representation of the String value. Use // the ValueString method for Terraform data handling instead. // diff --git a/types/basetypes/tuple_value.go b/types/basetypes/tuple_value.go index 5987d3824..a32490f72 100644 --- a/types/basetypes/tuple_value.go +++ b/types/basetypes/tuple_value.go @@ -188,6 +188,17 @@ func (v TupleValue) IsUnknown() bool { return v.state == attr.ValueStateUnknown } +// IsFullyNullableKnown returns true if the Tuple represents a currently nullable known value, +// including all its elements, recursively. +func (v TupleValue) IsFullyNullableKnown() bool { + for _, elem := range v.elements { + if !elem.IsFullyNullableKnown() { + return false + } + } + return true +} + // String returns a human-readable representation of the Tuple. The string returned here is not protected by any // compatibility guarantees, and is intended for logging and error reporting. func (v TupleValue) String() string {