Skip to content

Commit 920d80f

Browse files
committed
add number bounds
1 parent 0c191c7 commit 920d80f

File tree

6 files changed

+201
-58
lines changed

6 files changed

+201
-58
lines changed

tftypes/refinement/nullness.go

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,9 @@
11
package refinement
22

3-
import "github.com/vmihailenco/msgpack/v5"
4-
53
type Nullness struct {
64
value bool
75
}
86

9-
func (n Nullness) Encode(enc *msgpack.Encoder) error {
10-
err := enc.EncodeInt(int64(KeyNullness))
11-
if err != nil {
12-
return err
13-
}
14-
15-
// It shouldn't be possible for an unknown value to be definitely null (i.e. nullness.value = true),
16-
// as that should be represented by a known null value instead. This encoding is in place to be compliant
17-
// with Terraform's encoding which uses a definitely null refinement to collapse into a known null value.
18-
return enc.EncodeBool(n.value)
19-
}
20-
217
func (n Nullness) Equal(Refinement) bool {
228
return false
239
}
@@ -26,8 +12,8 @@ func (n Nullness) String() string {
2612
return "todo - Nullness"
2713
}
2814

29-
func (n Nullness) NotNull() bool {
30-
return !n.value
15+
func (n Nullness) Nullness() bool {
16+
return n.value
3117
}
3218

3319
func (n Nullness) unimplementable() {}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package refinement
2+
3+
import (
4+
"math/big"
5+
)
6+
7+
type NumberLowerBound struct {
8+
inclusive bool
9+
value *big.Float
10+
}
11+
12+
func (n NumberLowerBound) Equal(Refinement) bool {
13+
return false
14+
}
15+
16+
func (n NumberLowerBound) String() string {
17+
return "todo - NumberLowerBound"
18+
}
19+
20+
func (n NumberLowerBound) IsInclusive() bool {
21+
return n.inclusive
22+
}
23+
24+
func (n NumberLowerBound) LowerBound() *big.Float {
25+
return n.value
26+
}
27+
28+
func (n NumberLowerBound) unimplementable() {}
29+
30+
func NewNumberLowerBound(value *big.Float, inclusive bool) Refinement {
31+
return NumberLowerBound{
32+
value: value,
33+
inclusive: inclusive,
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package refinement
2+
3+
import (
4+
"math/big"
5+
)
6+
7+
type NumberUpperBound struct {
8+
inclusive bool
9+
value *big.Float
10+
}
11+
12+
func (n NumberUpperBound) Equal(Refinement) bool {
13+
return false
14+
}
15+
16+
func (n NumberUpperBound) String() string {
17+
return "todo - NumberUpperBound"
18+
}
19+
20+
func (n NumberUpperBound) IsInclusive() bool {
21+
return n.inclusive
22+
}
23+
24+
func (n NumberUpperBound) UpperBound() *big.Float {
25+
return n.value
26+
}
27+
28+
func (n NumberUpperBound) unimplementable() {}
29+
30+
func NewNumberUpperBound(value *big.Float, inclusive bool) Refinement {
31+
return NumberUpperBound{
32+
value: value,
33+
inclusive: inclusive,
34+
}
35+
}

tftypes/refinement/refinement.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package refinement
22

33
import (
44
"fmt"
5-
6-
"github.com/vmihailenco/msgpack/v5"
75
)
86

97
type Key int64
@@ -15,23 +13,26 @@ func (k Key) String() string {
1513
return "nullness"
1614
case KeyStringPrefix:
1715
return "string_prefix"
16+
case KeyNumberLowerBound:
17+
return "number_lower_bound"
18+
case KeyNumberUpperBound:
19+
return "number_upper_bound"
1820
default:
1921
return fmt.Sprintf("unsupported refinement: %d", k)
2022
}
2123
}
2224

2325
const (
24-
KeyNullness = Key(1)
25-
KeyStringPrefix = Key(2)
26-
// KeyNumberLowerBound = Key(3)
27-
// KeyNumberUpperBound = Key(4)
26+
KeyNullness = Key(1)
27+
KeyStringPrefix = Key(2)
28+
KeyNumberLowerBound = Key(3)
29+
KeyNumberUpperBound = Key(4)
2830
// KeyCollectionLengthLowerBound = Key(5)
2931
// KeyCollectionLengthUpperBound = Key(6)
3032
)
3133

3234
type Refinement interface {
3335
Equal(Refinement) bool
34-
Encode(*msgpack.Encoder) error
3536
String() string
3637
unimplementable() // prevents external implementations, all refinements are defined in the Terraform/HCL type system go-cty.
3738
}

tftypes/refinement/string_prefix.go

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,9 @@
11
package refinement
22

3-
import "github.com/vmihailenco/msgpack/v5"
4-
53
type StringPrefix struct {
64
value string
75
}
86

9-
// TODO: What if the prefix is empty? Should we skip encoding? Throw an error earlier? Throw an error here?
10-
// - I think an empty prefix string is valid, empty string is still more information then wholly unknown?
11-
// - I wonder what Terraform does in this situation today
12-
func (s StringPrefix) Encode(enc *msgpack.Encoder) error {
13-
// Matching go-cty for the max prefix length allowed here
14-
//
15-
// This ensures the total size of the refinements blob does not exceed the limit
16-
// set by the decoder (1024).
17-
maxPrefixLength := 256
18-
prefix := s.value
19-
if len(s.value) > maxPrefixLength {
20-
prefix = prefix[:maxPrefixLength-1]
21-
}
22-
23-
err := enc.EncodeInt(int64(KeyStringPrefix))
24-
if err != nil {
25-
return err
26-
}
27-
28-
return enc.EncodeString(prefix)
29-
}
30-
317
func (s StringPrefix) Equal(Refinement) bool {
328
return false
339
}

tftypes/value_msgpack.go

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,44 @@ func msgpackUnmarshalUnknown(dec *msgpack.Decoder, typ Type, path *AttributePath
441441
// TODO: If terraform doesn't support an empty string prefix, then neither should we, potentially return an error here.
442442

443443
newRefinements[keyCode] = refinement.NewStringPrefix(prefix)
444+
case refinement.KeyNumberLowerBound, refinement.KeyNumberUpperBound:
445+
if !typ.Is(Number) {
446+
return Value{}, path.NewErrorf("failed to decode msgpack extension body: numeric bound refinement for non-number type")
447+
}
448+
449+
// We know these refinements are a tuple of [number, bool] so we can re-use the msgpack decoding logic on this refinement
450+
tfValBound, err := msgpackUnmarshal(rfnDec, Tuple{ElementTypes: []Type{Number, Bool}}, path)
451+
if err != nil || tfValBound.IsNull() || !tfValBound.IsKnown() {
452+
return Value{}, path.NewErrorf("failed to decode msgpack extension body: numeric bound refinement must be [number, bool] tuple")
453+
}
454+
455+
tupleVal := []Value{}
456+
err = tfValBound.As(&tupleVal)
457+
if err != nil {
458+
return Value{}, path.NewErrorf("failed to decode msgpack extension body: numeric bound refinement tuple value conversion failed: %w", err)
459+
}
460+
461+
if len(tupleVal) != 2 {
462+
return Value{}, path.NewErrorf("failed to decode msgpack extension body: numeric bound refinement tuple value conversion failed: expected 2 elements, got %d elements", len(tupleVal))
463+
}
464+
465+
var boundVal *big.Float
466+
err = tupleVal[0].As(&boundVal)
467+
if err != nil {
468+
return Value{}, path.NewErrorf("failed to decode msgpack extension body: numeric bound refinement bound value conversion failed: %w", err)
469+
}
470+
471+
var inclusiveVal bool
472+
err = tupleVal[1].As(&inclusiveVal)
473+
if err != nil {
474+
return Value{}, path.NewErrorf("failed to decode msgpack extension body: numeric bound refinement inclusive value conversion failed: %w", err)
475+
}
476+
477+
if keyCode == refinement.KeyNumberLowerBound {
478+
newRefinements[keyCode] = refinement.NewNumberLowerBound(boundVal, inclusiveVal)
479+
} else {
480+
newRefinements[keyCode] = refinement.NewNumberUpperBound(boundVal, inclusiveVal)
481+
}
444482
default:
445483
err := rfnDec.Skip()
446484
if err != nil {
@@ -511,23 +549,95 @@ func marshalUnknownValue(val Value, typ Type, p *AttributePath, enc *msgpack.Enc
511549
refnEnc := msgpack.NewEncoder(&refnBuf)
512550
mapLen := 0
513551

514-
// TODO: Should the refinement interface be defining the encoding? Should we export the refinement implementation details?
515-
// - If we keep it in the interface, then we can simplify this logic
516-
for kind, refn := range val.refinements {
517-
switch kind {
518-
case refinement.KeyNullness:
519-
err := refn.Encode(refnEnc)
552+
for _, refn := range val.refinements {
553+
switch refnVal := refn.(type) {
554+
case refinement.Nullness:
555+
err := refnEnc.EncodeInt(int64(refinement.KeyNullness))
556+
if err != nil {
557+
return p.NewErrorf("error encoding Nullness value refinement key: %w", err)
558+
}
559+
560+
// It shouldn't be possible for an unknown value to be definitely null (i.e. nullness.value = true),
561+
// as that should be represented by a known null value instead. This encoding is in place to be compliant
562+
// with Terraform's encoding which uses a definitely null refinement to collapse into a known null value.
563+
err = refnEnc.EncodeBool(refnVal.Nullness())
520564
if err != nil {
521565
return p.NewErrorf("error encoding Nullness value refinement: %w", err)
522566
}
567+
523568
mapLen++
524-
case refinement.KeyStringPrefix:
525-
// TODO: If the prefix is empty, we shouldn't encode a refinement. This should
526-
// probably be reflected in the interface.
527-
err := refn.Encode(refnEnc)
569+
case refinement.StringPrefix:
570+
if rawPrefix := refnVal.PrefixValue(); rawPrefix != "" {
571+
// Matching go-cty for the max prefix length allowed here
572+
//
573+
// This ensures the total size of the refinements blob does not exceed the limit
574+
// set by the decoder (1024).
575+
maxPrefixLength := 256
576+
prefix := rawPrefix
577+
if len(rawPrefix) > maxPrefixLength {
578+
prefix = prefix[:maxPrefixLength-1]
579+
}
580+
581+
err := refnEnc.EncodeInt(int64(refinement.KeyStringPrefix))
582+
if err != nil {
583+
return p.NewErrorf("error encoding StringPrefix value refinement key: %w", err)
584+
}
585+
586+
err = refnEnc.EncodeString(prefix)
587+
if err != nil {
588+
return p.NewErrorf("error encoding StringPrefix value refinement: %w", err)
589+
}
590+
591+
mapLen++
592+
}
593+
594+
case refinement.NumberLowerBound:
595+
// TODO: should check this isn't negative infinity? To match go-cty
596+
boundTfType := Tuple{ElementTypes: []Type{Number, Bool}}
597+
598+
// TODO: Do we need to do this? Kind of nasty
599+
boundTfVal := NewValue(
600+
boundTfType,
601+
[]Value{
602+
NewValue(Number, refnVal.LowerBound()),
603+
NewValue(Bool, refnVal.IsInclusive()),
604+
},
605+
)
606+
607+
err := refnEnc.EncodeInt(int64(refinement.KeyNumberLowerBound))
608+
if err != nil {
609+
return p.NewErrorf("error encoding NumberLowerBound value refinement key: %w", err)
610+
}
611+
612+
err = marshalMsgPack(boundTfVal, boundTfType, p, refnEnc)
613+
if err != nil {
614+
return p.NewErrorf("error encoding NumberLowerBound value refinement: %w", err)
615+
}
616+
617+
mapLen++
618+
case refinement.NumberUpperBound:
619+
// TODO: should check this isn't positive infinity? To match go-cty
620+
boundTfType := Tuple{ElementTypes: []Type{Number, Bool}}
621+
622+
// TODO: Do we need to do this? Kind of nasty
623+
boundTfVal := NewValue(
624+
boundTfType,
625+
[]Value{
626+
NewValue(Number, refnVal.UpperBound()),
627+
NewValue(Bool, refnVal.IsInclusive()),
628+
},
629+
)
630+
631+
err := refnEnc.EncodeInt(int64(refinement.KeyNumberUpperBound))
528632
if err != nil {
529-
return p.NewErrorf("error encoding StringPrefix value refinement: %w", err)
633+
return p.NewErrorf("error encoding NumberUpperBound value refinement key: %w", err)
530634
}
635+
636+
err = marshalMsgPack(boundTfVal, boundTfType, p, refnEnc)
637+
if err != nil {
638+
return p.NewErrorf("error encoding NumberUpperBound value refinement: %w", err)
639+
}
640+
531641
mapLen++
532642
default:
533643
continue

0 commit comments

Comments
 (0)