Skip to content
Merged
5 changes: 5 additions & 0 deletions .changes/unreleased/FEATURES-20250120-155954.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: FEATURES
body: 'knownvalue: added function checks for custom validation of resource attribute or output values.'
time: 2025-01-20T15:59:54.135609+01:00
custom:
Issue: "412"
39 changes: 39 additions & 0 deletions knownvalue/bool_func.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue

import "fmt"

var _ Check = boolFunc{}

type boolFunc struct {
checkFunc func(v bool) error
}

// CheckValue determines whether the passed value is of type bool, and
// returns no error from the provided check function
func (v boolFunc) CheckValue(other any) error {
val, ok := other.(bool)

if !ok {
return fmt.Errorf("expected bool value for BoolFunc check, got: %T", other)
}

return v.checkFunc(val)
}

// String returns the bool representation of the value.
func (v boolFunc) String() string {
// Validation is up the the implementer of the function, so there are no
// bool literal or regex comparers to print here
return "BoolFunc"
}

// BoolFunc returns a Check for passing the bool value in state
// to the provided check function
func BoolFunc(fn func(v bool) error) boolFunc {
return boolFunc{
checkFunc: fn,
}
}
75 changes: 75 additions & 0 deletions knownvalue/bool_func_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue_test

import (
"encoding/json"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform-plugin-testing/knownvalue"
)

func TestBoolFunc_CheckValue(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
self knownvalue.Check
other any
expectedError error
}{
"nil": {
self: knownvalue.BoolFunc(func(bool) error { return nil }),
expectedError: fmt.Errorf("expected bool value for BoolFunc check, got: <nil>"),
},
"wrong-type": {
self: knownvalue.BoolFunc(func(bool) error { return nil }),
other: json.Number("1.234"),
expectedError: fmt.Errorf("expected bool value for BoolFunc check, got: json.Number"),
},
"failure": {
self: knownvalue.BoolFunc(func(b bool) error {
if b != true {
return fmt.Errorf("%t was not true", b)
}
return nil
}),
other: false,
expectedError: fmt.Errorf("%t was not true", false),
},
"success": {
self: knownvalue.BoolFunc(func(b bool) error {
if b != true {
return fmt.Errorf("%t was not foo", b)
}
return nil
}),
other: true,
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

got := testCase.self.CheckValue(testCase.other)

if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}

func TestBoolFunc_String(t *testing.T) {
t.Parallel()

got := knownvalue.BoolFunc(func(bool) error { return nil }).String()

if diff := cmp.Diff(got, "BoolFunc"); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
}
48 changes: 48 additions & 0 deletions knownvalue/float32_func.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue

import (
"encoding/json"
"fmt"
"strconv"
)

var _ Check = float32Func{}

type float32Func struct {
checkFunc func(v float32) error
}

// CheckValue determines whether the passed value is of type float32, and
// returns no error from the provided check function
func (v float32Func) CheckValue(other any) error {
jsonNum, ok := other.(json.Number)

if !ok {
return fmt.Errorf("expected json.Number value for Float32Func check, got: %T", other)
}

otherVal, err := strconv.ParseFloat(string(jsonNum), 32)
if err != nil {
return fmt.Errorf("expected json.Number to be parseable as float32 value for Float32Func check: %s", err)
}

return v.checkFunc(float32(otherVal))
}

// String returns the float32 representation of the value.
func (v float32Func) String() string {
// Validation is up the the implementer of the function, so there are no
// float32 literal or regex comparers to print here
return "Float32Func"
}

// Float32Func returns a Check for passing the float32 value in state
// to the provided check function
func Float32Func(fn func(v float32) error) float32Func {
return float32Func{
checkFunc: fn,
}
}
80 changes: 80 additions & 0 deletions knownvalue/float32_func_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue_test

import (
"encoding/json"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform-plugin-testing/knownvalue"
)

func TestFloat32Func_CheckValue(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
self knownvalue.Check
other any
expectedError error
}{
"nil": {
self: knownvalue.Float32Func(func(float32) error { return nil }),
expectedError: fmt.Errorf("expected json.Number value for Float32Func check, got: <nil>"),
},
"wrong-type": {
self: knownvalue.Float32Func(func(float32) error { return nil }),
other: "wrongtype",
expectedError: fmt.Errorf("expected json.Number value for Float32Func check, got: string"),
},
"no-digits": {
self: knownvalue.Float32Func(func(float32) error { return nil }),
other: json.Number("str"),
expectedError: fmt.Errorf("expected json.Number to be parseable as float32 value for Float32Func check: strconv.ParseFloat: parsing \"str\": invalid syntax"),
},
"failure": {
self: knownvalue.Float32Func(func(f float32) error {
if f != 1.1 {
return fmt.Errorf("%f was not 1.1", f)
}
return nil
}),
other: json.Number("1.2"),
expectedError: fmt.Errorf("%f was not 1.1", 1.2),
},
"success": {
self: knownvalue.Float32Func(func(f float32) error {
if f != 1.1 {
return fmt.Errorf("%f was not 1.1", f)
}
return nil
}),
other: json.Number("1.1"),
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

got := testCase.self.CheckValue(testCase.other)

if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}

func TestFloat32Func_String(t *testing.T) {
t.Parallel()

got := knownvalue.Float32Func(func(float32) error { return nil }).String()

if diff := cmp.Diff(got, "Float32Func"); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
}
48 changes: 48 additions & 0 deletions knownvalue/float64_func.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue

import (
"encoding/json"
"fmt"
"strconv"
)

var _ Check = float64Func{}

type float64Func struct {
checkFunc func(v float64) error
}

// CheckValue determines whether the passed value is of type float64, and
// returns no error from the provided check function
func (v float64Func) CheckValue(other any) error {
jsonNum, ok := other.(json.Number)

if !ok {
return fmt.Errorf("expected json.Number value for Float64Func check, got: %T", other)
}

otherVal, err := strconv.ParseFloat(string(jsonNum), 64)
if err != nil {
return fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Func check: %s", err)
}

return v.checkFunc(otherVal)
}

// String returns the float64 representation of the value.
func (v float64Func) String() string {
// Validation is up the the implementer of the function, so there are no
// float64 literal or regex comparers to print here
return "Float64Func"
}

// Float64Func returns a Check for passing the float64 value in state
// to the provided check function
func Float64Func(fn func(v float64) error) float64Func {
return float64Func{
checkFunc: fn,
}
}
80 changes: 80 additions & 0 deletions knownvalue/float64_func_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package knownvalue_test

import (
"encoding/json"
"fmt"
"testing"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform-plugin-testing/knownvalue"
)

func TestFloat64Func_CheckValue(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
self knownvalue.Check
other any
expectedError error
}{
"nil": {
self: knownvalue.Float64Func(func(float64) error { return nil }),
expectedError: fmt.Errorf("expected json.Number value for Float64Func check, got: <nil>"),
},
"wrong-type": {
self: knownvalue.Float64Func(func(float64) error { return nil }),
other: "wrongtype",
expectedError: fmt.Errorf("expected json.Number value for Float64Func check, got: string"),
},
"no-digits": {
self: knownvalue.Float64Func(func(float64) error { return nil }),
other: json.Number("str"),
expectedError: fmt.Errorf("expected json.Number to be parseable as float64 value for Float64Func check: strconv.ParseFloat: parsing \"str\": invalid syntax"),
},
"failure": {
self: knownvalue.Float64Func(func(f float64) error {
if f != 1.1 {
return fmt.Errorf("%f was not 1.1", f)
}
return nil
}),
other: json.Number("1.2"),
expectedError: fmt.Errorf("%f was not 1.1", 1.2),
},
"success": {
self: knownvalue.Float64Func(func(f float64) error {
if f != 1.1 {
return fmt.Errorf("%f was not 1.1", f)
}
return nil
}),
other: json.Number("1.1"),
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()

got := testCase.self.CheckValue(testCase.other)

if diff := cmp.Diff(got, testCase.expectedError, equateErrorMessage); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
})
}
}

func TestFloat64Func_String(t *testing.T) {
t.Parallel()

got := knownvalue.Float64Func(func(float64) error { return nil }).String()

if diff := cmp.Diff(got, "Float64Func"); diff != "" {
t.Errorf("unexpected difference: %s", diff)
}
}
Loading
Loading