Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tftypes: Introduce unknown value refinement support for Value #448

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions tftypes/refinement/collection_length_lower_bound.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package refinement

import "fmt"

// CollectionLengthLowerBound represents an unknown value refinement which indicates the length of the final collection value will be
// at least the specified int64 value. This refinement can only be applied to List, Map, and Set types.
type CollectionLengthLowerBound struct {
value int64
}

func (n CollectionLengthLowerBound) Equal(other Refinement) bool {
otherVal, ok := other.(CollectionLengthLowerBound)
if !ok {
return false
}

return n.LowerBound() == otherVal.LowerBound()
}

func (n CollectionLengthLowerBound) String() string {
return fmt.Sprintf("length lower bound = %d", n.LowerBound())
}

// LowerBound returns the int64 value that the final value's collection length will be at least.
func (n CollectionLengthLowerBound) LowerBound() int64 {
return n.value
}

func (n CollectionLengthLowerBound) unimplementable() {}

// NewCollectionLengthLowerBound returns the CollectionLengthLowerBound unknown value refinement which indicates the length of the final
// collection value will be at least the specified int64 value. This refinement can only be applied to List, Map, and Set types.
func NewCollectionLengthLowerBound(value int64) Refinement {
return CollectionLengthLowerBound{
value: value,
}
}
40 changes: 40 additions & 0 deletions tftypes/refinement/collection_length_upper_bound.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package refinement

import "fmt"

// CollectionLengthUpperBound represents an unknown value refinement which indicates the length of the final collection value will be
// at most the specified int64 value. This refinement can only be applied to List, Map, and Set types.
type CollectionLengthUpperBound struct {
value int64
}

func (n CollectionLengthUpperBound) Equal(other Refinement) bool {
otherVal, ok := other.(CollectionLengthUpperBound)
if !ok {
return false
}

return n.UpperBound() == otherVal.UpperBound()
}

func (n CollectionLengthUpperBound) String() string {
return fmt.Sprintf("length upper bound = %d", n.UpperBound())
}

// UpperBound returns the int64 value that the final value's collection length will be at most.
func (n CollectionLengthUpperBound) UpperBound() int64 {
return n.value
}

func (n CollectionLengthUpperBound) unimplementable() {}

// NewCollectionLengthUpperBound returns the CollectionLengthUpperBound unknown value refinement which indicates the length of the final
// collection value will be at most the specified int64 value. This refinement can only be applied to List, Map, and Set types.
func NewCollectionLengthUpperBound(value int64) Refinement {
return CollectionLengthUpperBound{
value: value,
}
}
11 changes: 11 additions & 0 deletions tftypes/refinement/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

// The refinement package contains the interfaces and structs that represent unknown value refinement data. Refinements contain
// additional constraints about unknown values and what their eventual known values can be. In certain scenarios, Terraform can
// use these constraints to produce known results from unknown values. (like evaluating a count expression comparing an unknown
// value to "null")
//
// Unknown value refinements can be added to a `tftypes.Value` via the `(tftypes.Value).Refine` method. Refinements on an unknown
// `tftypes.Value` can be retrieved via the `(tftypes.Value).Refinements()` method.
package refinement
58 changes: 58 additions & 0 deletions tftypes/refinement/nullness.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package refinement

// Nullness represents an unknown value refinement that indicates the final value will definitely not be null (Nullness = false). This refinement
// can be applied to a value of any type (excluding DynamicPseudoType).
//
// While an unknown value can be refined to indicate that the final value will definitely be null (Nullness = true), there is no practical reason
// to do this. This option is exposed to maintain parity with Terraform's type system, while all practical usages of this refinement should collapse
// to known null values instead.
type Nullness struct {
value bool
}

func (n Nullness) Equal(other Refinement) bool {
otherVal, ok := other.(Nullness)
if !ok {
return false
}

return n.Nullness() == otherVal.Nullness()
}

func (n Nullness) String() string {
if n.value {
// This case should never happen, as an unknown value that is definitely null should be
// represented as a known null value.
return "null"
}

return "not null"
}

// Nullness returns the underlying refinement data indicating:
// - When "false", the final value will definitely not be null.
// - When "true", the final value will definitely be null.
//
// While an unknown value can be refined to indicate that the final value will definitely be null (Nullness = true), there is no practical reason
// to do this. This option is exposed to maintain parity with Terraform's type system, while all practical usages of this refinement should collapse
// to known null values instead.
func (n Nullness) Nullness() bool {
return n.value
}

func (n Nullness) unimplementable() {}

// NewNullness returns the Nullness unknown value refinement that indicates the final value will definitely not be null (Nullness = false). This refinement
// can be applied to a value of any type (excluding DynamicPseudoType).
//
// While an unknown value can be refined to indicate that the final value will definitely be null (Nullness = true), there is no practical reason
// to do this. This option is exposed to maintain parity with Terraform's type system, while all practical usages of this refinement should collapse
// to known null values instead.
func NewNullness(value bool) Refinement {
return Nullness{
value: value,
}
}
56 changes: 56 additions & 0 deletions tftypes/refinement/number_lower_bound.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package refinement

import (
"fmt"
"math/big"
)

// NumberLowerBound represents an unknown value refinement that indicates the final value will not be less than the specified
// *big.Float value, as well as whether that bound is inclusive or exclusive. This refinement can only be applied to the Number type.
type NumberLowerBound struct {
inclusive bool
value *big.Float
}

func (n NumberLowerBound) Equal(other Refinement) bool {
otherVal, ok := other.(NumberLowerBound)
if !ok {
return false
}

return n.IsInclusive() == otherVal.IsInclusive() && n.LowerBound().Cmp(otherVal.LowerBound()) == 0
}

func (n NumberLowerBound) String() string {
rangeDescription := "inclusive"
if !n.IsInclusive() {
rangeDescription = "exclusive"
}

return fmt.Sprintf("lower bound = %s (%s)", n.LowerBound().String(), rangeDescription)
}

// IsInclusive returns whether the bound returned by the `LowerBound` method is inclusive or exclusive.
func (n NumberLowerBound) IsInclusive() bool {
return n.inclusive
}

// LowerBound returns the *big.Float value that the final value will not be less than. The `IsInclusive` method must also be used during
// comparison to determine whether the bound is inclusive or exclusive.
func (n NumberLowerBound) LowerBound() *big.Float {
return n.value
}

func (n NumberLowerBound) unimplementable() {}

// NewNumberLowerBound returns the NumberLowerBound unknown value refinement that indicates the final value will not be less than the specified
// *big.Float value, as well as whether that bound is inclusive or exclusive. This refinement can only be applied to the Number type.
func NewNumberLowerBound(value *big.Float, inclusive bool) Refinement {
return NumberLowerBound{
value: value,
inclusive: inclusive,
}
}
56 changes: 56 additions & 0 deletions tftypes/refinement/number_upper_bound.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package refinement

import (
"fmt"
"math/big"
)

// NumberUpperBound represents an unknown value refinement that indicates the final value will not be greater than the specified
// *big.Float value, as well as whether that bound is inclusive or exclusive. This refinement can only be applied to the Number type.
type NumberUpperBound struct {
inclusive bool
value *big.Float
}

func (n NumberUpperBound) Equal(other Refinement) bool {
otherVal, ok := other.(NumberUpperBound)
if !ok {
return false
}

return n.IsInclusive() == otherVal.IsInclusive() && n.UpperBound().Cmp(otherVal.UpperBound()) == 0
}

func (n NumberUpperBound) String() string {
rangeDescription := "inclusive"
if !n.IsInclusive() {
rangeDescription = "exclusive"
}

return fmt.Sprintf("upper bound = %s (%s)", n.UpperBound().String(), rangeDescription)
}

// IsInclusive returns whether the bound returned by the `UpperBound` method is inclusive or exclusive.
func (n NumberUpperBound) IsInclusive() bool {
return n.inclusive
}

// UpperBound returns the *big.Float value that the final value will not be greater than. The `IsInclusive` method must also be used during
// comparison to determine whether the bound is inclusive or exclusive.
func (n NumberUpperBound) UpperBound() *big.Float {
return n.value
}

func (n NumberUpperBound) unimplementable() {}

// NewNumberUpperBound returns the NumberUpperBound unknown value refinement that indicates the final value will not be greater than the specified
// *big.Float value, as well as whether that bound is inclusive or exclusive. This refinement can only be applied to the Number type.
func NewNumberUpperBound(value *big.Float, inclusive bool) Refinement {
return NumberUpperBound{
value: value,
inclusive: inclusive,
}
}
124 changes: 124 additions & 0 deletions tftypes/refinement/refinement.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package refinement

import (
"fmt"
"sort"
"strings"
)

type Key int64

func (k Key) String() string {
switch k {
case KeyNullness:
return "nullness"
case KeyStringPrefix:
return "string_prefix"
case KeyNumberLowerBound:
return "number_lower_bound"
case KeyNumberUpperBound:
return "number_upper_bound"
case KeyCollectionLengthLowerBound:
return "collection_length_lower_bound"
case KeyCollectionLengthUpperBound:
return "collection_length_upper_bound"
default:
return fmt.Sprintf("unsupported refinement: %d", k)
}
}

const (
// KeyNullness represents a refinement that specifies whether the final value will not be null.
//
// MAINTINAER NOTE: In practice, this refinement data will only contain "false", indicating the final value
// cannot be null. If the refinement data was ever set to "true", that would indicate the final value will be null, in which
// case the value is not unknown, it is known and should not have any refinement data.
//
// This refinement is relevant for all types except tftypes.DynamicPseudoType.
KeyNullness = Key(1)

// KeyStringPrefix represents a refinement that specifies a known prefix of a final string value.
//
// This refinement is only relevant for tftypes.String.
KeyStringPrefix = Key(2)

// KeyNumberLowerBound represents a refinement that specifies the lower bound of possible values for a final number value.
// The refinement data contains a boolean which indicates whether the bound is inclusive.
//
// This refinement is only relevant for tftypes.Number.
KeyNumberLowerBound = Key(3)

// KeyNumberUpperBound represents a refinement that specifies the upper bound of possible values for a final number value.
// The refinement data contains a boolean which indicates whether the bound is inclusive.
//
// This refinement is only relevant for tftypes.Number.
KeyNumberUpperBound = Key(4)

// KeyCollectionLengthLowerBound represents a refinement that specifies the lower bound of possible length for a final collection value.
//
// This refinement is only relevant for tftypes.List, tftypes.Set, and tftypes.Map.
KeyCollectionLengthLowerBound = Key(5)

// KeyCollectionLengthUpperBound represents a refinement that specifies the upper bound of possible length for a final collection value.
//
// This refinement is only relevant for tftypes.List, tftypes.Set, and tftypes.Map.
KeyCollectionLengthUpperBound = Key(6)
)

// Refinement represents an unknown value refinement with data constraints relevant to the final value. This interface can be asserted further
// with the associated structs in the `refinement` package to extract underlying refinement data.
type Refinement interface {
// Equal should return true if the Refinement is considered equivalent to the
// Refinement passed as an argument.
Equal(Refinement) bool

// String should return a human-friendly version of the Refinement.
String() string

unimplementable() // prevents external implementations, all refinements are defined in the Terraform/HCL type system go-cty.
}

// Refinements represents a map of unknown value refinement data.
type Refinements map[Key]Refinement

func (r Refinements) Equal(other Refinements) bool {
if len(r) != len(other) {
return false
}

for key, refnVal := range r {
otherRefnVal, ok := other[key]
if !ok {
// Didn't find a refinement at the same key
return false
}

if !refnVal.Equal(otherRefnVal) {
// Refinement data is not equal
return false
}
}

return true
}
func (r Refinements) String() string {
var res strings.Builder

keys := make([]Key, 0, len(r))
for k := range r {
keys = append(keys, k)
}

sort.Slice(keys, func(a, b int) bool { return keys[a] < keys[b] })
for pos, key := range keys {
if pos != 0 {
res.WriteString(", ")
}
res.WriteString(r[key].String())
}

return res.String()
}
Loading
Loading