Skip to content

Commit 96ff574

Browse files
knownvalue: Introduce function checks for primitive types (#412)
* extract: add extract bool and string value functions This introduces a new state check that extracts an underlying attribute value by traversing the state with tfjsonpath using a specified path. * extract: improve error message * extract: improve subtest name * knownvalue: add custom check functions for validation * knownvalue: address failing tests * knownvalue: remove redundant cast * docs: add knownvalue func check documentation * knownvalue: add whole number (integer) test * docs: correct spaces * docs: correct spaces * changie: add entry * remove copyloopvar It is unnecessary to copy loop variables because these variables have per-iteration scope instead of per-loop scope as it was in Go < 1.22.
1 parent 5f414d2 commit 96ff574

22 files changed

+1134
-4
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: FEATURES
2+
body: 'knownvalue: added function checks for custom validation of resource attribute or output values.'
3+
time: 2025-01-20T15:59:54.135609+01:00
4+
custom:
5+
Issue: "412"

knownvalue/bool_func.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package knownvalue
5+
6+
import "fmt"
7+
8+
var _ Check = boolFunc{}
9+
10+
type boolFunc struct {
11+
checkFunc func(v bool) error
12+
}
13+
14+
// CheckValue determines whether the passed value is of type bool, and
15+
// returns no error from the provided check function
16+
func (v boolFunc) CheckValue(other any) error {
17+
val, ok := other.(bool)
18+
19+
if !ok {
20+
return fmt.Errorf("expected bool value for BoolFunc check, got: %T", other)
21+
}
22+
23+
return v.checkFunc(val)
24+
}
25+
26+
// String returns the bool representation of the value.
27+
func (v boolFunc) String() string {
28+
// Validation is up the the implementer of the function, so there are no
29+
// bool literal or regex comparers to print here
30+
return "BoolFunc"
31+
}
32+
33+
// BoolFunc returns a Check for passing the bool value in state
34+
// to the provided check function
35+
func BoolFunc(fn func(v bool) error) boolFunc {
36+
return boolFunc{
37+
checkFunc: fn,
38+
}
39+
}

knownvalue/bool_func_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package knownvalue_test
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
13+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
14+
)
15+
16+
func TestBoolFunc_CheckValue(t *testing.T) {
17+
t.Parallel()
18+
19+
testCases := map[string]struct {
20+
self knownvalue.Check
21+
other any
22+
expectedError error
23+
}{
24+
"nil": {
25+
self: knownvalue.BoolFunc(func(bool) error { return nil }),
26+
expectedError: fmt.Errorf("expected bool value for BoolFunc check, got: <nil>"),
27+
},
28+
"wrong-type": {
29+
self: knownvalue.BoolFunc(func(bool) error { return nil }),
30+
other: json.Number("1.234"),
31+
expectedError: fmt.Errorf("expected bool value for BoolFunc check, got: json.Number"),
32+
},
33+
"failure": {
34+
self: knownvalue.BoolFunc(func(b bool) error {
35+
if b != true {
36+
return fmt.Errorf("%t was not true", b)
37+
}
38+
return nil
39+
}),
40+
other: false,
41+
expectedError: fmt.Errorf("%t was not true", false),
42+
},
43+
"success": {
44+
self: knownvalue.BoolFunc(func(b bool) error {
45+
if b != true {
46+
return fmt.Errorf("%t was not foo", b)
47+
}
48+
return nil
49+
}),
50+
other: true,
51+
},
52+
}
53+
54+
for name, testCase := range testCases {
55+
t.Run(name, func(t *testing.T) {
56+
t.Parallel()
57+
58+
got := testCase.self.CheckValue(testCase.other)
59+
60+
if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" {
61+
t.Errorf("unexpected difference: %s", diff)
62+
}
63+
})
64+
}
65+
}
66+
67+
func TestBoolFunc_String(t *testing.T) {
68+
t.Parallel()
69+
70+
got := knownvalue.BoolFunc(func(bool) error { return nil }).String()
71+
72+
if diff := cmp.Diff(got, "BoolFunc"); diff != "" {
73+
t.Errorf("unexpected difference: %s", diff)
74+
}
75+
}

knownvalue/float32_func.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package knownvalue
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"strconv"
10+
)
11+
12+
var _ Check = float32Func{}
13+
14+
type float32Func struct {
15+
checkFunc func(v float32) error
16+
}
17+
18+
// CheckValue determines whether the passed value is of type float32, and
19+
// returns no error from the provided check function
20+
func (v float32Func) CheckValue(other any) error {
21+
jsonNum, ok := other.(json.Number)
22+
23+
if !ok {
24+
return fmt.Errorf("expected json.Number value for Float32Func check, got: %T", other)
25+
}
26+
27+
otherVal, err := strconv.ParseFloat(string(jsonNum), 32)
28+
if err != nil {
29+
return fmt.Errorf("expected json.Number to be parseable as float32 value for Float32Func check: %s", err)
30+
}
31+
32+
return v.checkFunc(float32(otherVal))
33+
}
34+
35+
// String returns the float32 representation of the value.
36+
func (v float32Func) String() string {
37+
// Validation is up the the implementer of the function, so there are no
38+
// float32 literal or regex comparers to print here
39+
return "Float32Func"
40+
}
41+
42+
// Float32Func returns a Check for passing the float32 value in state
43+
// to the provided check function
44+
func Float32Func(fn func(v float32) error) float32Func {
45+
return float32Func{
46+
checkFunc: fn,
47+
}
48+
}

knownvalue/float32_func_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package knownvalue_test
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
13+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
14+
)
15+
16+
func TestFloat32Func_CheckValue(t *testing.T) {
17+
t.Parallel()
18+
19+
testCases := map[string]struct {
20+
self knownvalue.Check
21+
other any
22+
expectedError error
23+
}{
24+
"nil": {
25+
self: knownvalue.Float32Func(func(float32) error { return nil }),
26+
expectedError: fmt.Errorf("expected json.Number value for Float32Func check, got: <nil>"),
27+
},
28+
"wrong-type": {
29+
self: knownvalue.Float32Func(func(float32) error { return nil }),
30+
other: "wrongtype",
31+
expectedError: fmt.Errorf("expected json.Number value for Float32Func check, got: string"),
32+
},
33+
"no-digits": {
34+
self: knownvalue.Float32Func(func(float32) error { return nil }),
35+
other: json.Number("str"),
36+
expectedError: fmt.Errorf("expected json.Number to be parseable as float32 value for Float32Func check: strconv.ParseFloat: parsing \"str\": invalid syntax"),
37+
},
38+
"failure": {
39+
self: knownvalue.Float32Func(func(f float32) error {
40+
if f != 1.1 {
41+
return fmt.Errorf("%f was not 1.1", f)
42+
}
43+
return nil
44+
}),
45+
other: json.Number("1.2"),
46+
expectedError: fmt.Errorf("%f was not 1.1", 1.2),
47+
},
48+
"success": {
49+
self: knownvalue.Float32Func(func(f float32) error {
50+
if f != 1.1 {
51+
return fmt.Errorf("%f was not 1.1", f)
52+
}
53+
return nil
54+
}),
55+
other: json.Number("1.1"),
56+
},
57+
}
58+
59+
for name, testCase := range testCases {
60+
t.Run(name, func(t *testing.T) {
61+
t.Parallel()
62+
63+
got := testCase.self.CheckValue(testCase.other)
64+
65+
if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" {
66+
t.Errorf("unexpected difference: %s", diff)
67+
}
68+
})
69+
}
70+
}
71+
72+
func TestFloat32Func_String(t *testing.T) {
73+
t.Parallel()
74+
75+
got := knownvalue.Float32Func(func(float32) error { return nil }).String()
76+
77+
if diff := cmp.Diff(got, "Float32Func"); diff != "" {
78+
t.Errorf("unexpected difference: %s", diff)
79+
}
80+
}

knownvalue/float64_func.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package knownvalue
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"strconv"
10+
)
11+
12+
var _ Check = float64Func{}
13+
14+
type float64Func struct {
15+
checkFunc func(v float64) error
16+
}
17+
18+
// CheckValue determines whether the passed value is of type float64, and
19+
// returns no error from the provided check function
20+
func (v float64Func) CheckValue(other any) error {
21+
jsonNum, ok := other.(json.Number)
22+
23+
if !ok {
24+
return fmt.Errorf("expected json.Number value for Float64Func check, got: %T", other)
25+
}
26+
27+
otherVal, err := strconv.ParseFloat(string(jsonNum), 64)
28+
if err != nil {
29+
return fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Func check: %s", err)
30+
}
31+
32+
return v.checkFunc(otherVal)
33+
}
34+
35+
// String returns the float64 representation of the value.
36+
func (v float64Func) String() string {
37+
// Validation is up the the implementer of the function, so there are no
38+
// float64 literal or regex comparers to print here
39+
return "Float64Func"
40+
}
41+
42+
// Float64Func returns a Check for passing the float64 value in state
43+
// to the provided check function
44+
func Float64Func(fn func(v float64) error) float64Func {
45+
return float64Func{
46+
checkFunc: fn,
47+
}
48+
}

knownvalue/float64_func_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package knownvalue_test
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
13+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
14+
)
15+
16+
func TestFloat64Func_CheckValue(t *testing.T) {
17+
t.Parallel()
18+
19+
testCases := map[string]struct {
20+
self knownvalue.Check
21+
other any
22+
expectedError error
23+
}{
24+
"nil": {
25+
self: knownvalue.Float64Func(func(float64) error { return nil }),
26+
expectedError: fmt.Errorf("expected json.Number value for Float64Func check, got: <nil>"),
27+
},
28+
"wrong-type": {
29+
self: knownvalue.Float64Func(func(float64) error { return nil }),
30+
other: "wrongtype",
31+
expectedError: fmt.Errorf("expected json.Number value for Float64Func check, got: string"),
32+
},
33+
"no-digits": {
34+
self: knownvalue.Float64Func(func(float64) error { return nil }),
35+
other: json.Number("str"),
36+
expectedError: fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Func check: strconv.ParseFloat: parsing \"str\": invalid syntax"),
37+
},
38+
"failure": {
39+
self: knownvalue.Float64Func(func(f float64) error {
40+
if f != 1.1 {
41+
return fmt.Errorf("%f was not 1.1", f)
42+
}
43+
return nil
44+
}),
45+
other: json.Number("1.2"),
46+
expectedError: fmt.Errorf("%f was not 1.1", 1.2),
47+
},
48+
"success": {
49+
self: knownvalue.Float64Func(func(f float64) error {
50+
if f != 1.1 {
51+
return fmt.Errorf("%f was not 1.1", f)
52+
}
53+
return nil
54+
}),
55+
other: json.Number("1.1"),
56+
},
57+
}
58+
59+
for name, testCase := range testCases {
60+
t.Run(name, func(t *testing.T) {
61+
t.Parallel()
62+
63+
got := testCase.self.CheckValue(testCase.other)
64+
65+
if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" {
66+
t.Errorf("unexpected difference: %s", diff)
67+
}
68+
})
69+
}
70+
}
71+
72+
func TestFloat64Func_String(t *testing.T) {
73+
t.Parallel()
74+
75+
got := knownvalue.Float64Func(func(float64) error { return nil }).String()
76+
77+
if diff := cmp.Diff(got, "Float64Func"); diff != "" {
78+
t.Errorf("unexpected difference: %s", diff)
79+
}
80+
}

0 commit comments

Comments
 (0)