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

validation: add additional Int, Network, Time and Web validators #296

Merged
merged 15 commits into from
Jan 16, 2020
40 changes: 40 additions & 0 deletions helper/validation/int.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package validation

import (
"fmt"
"math"

"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)
Expand Down Expand Up @@ -63,6 +64,25 @@ func IntAtMost(max int) schema.SchemaValidateFunc {
}
}

// IntDivisibleBy returns a SchemaValidateFunc which tests if the provided value
// is of type int and is divisible by a given number
func IntDivisibleBy(divisor int) schema.SchemaValidateFunc {
return func(i interface{}, k string) (warnings []string, errors []error) {
v, ok := i.(int)
if !ok {
errors = append(errors, fmt.Errorf("expected type of %s to be int", k))
return
}

if math.Mod(float64(v), float64(divisor)) != 0 {
errors = append(errors, fmt.Errorf("expected %s to be divisible by %d, got: %v", k, divisor, i))
return
}

return warnings, errors
}
}

// IntInSlice returns a SchemaValidateFunc which tests if the provided value
// is of type int and matches the value of an element in the valid slice
func IntInSlice(valid []int) schema.SchemaValidateFunc {
Expand All @@ -83,3 +103,23 @@ func IntInSlice(valid []int) schema.SchemaValidateFunc {
return
}
}

// IntNotInSlice returns a SchemaValidateFunc which tests if the provided value
// is of type int and matches the value of an element in the valid slice
func IntNotInSlice(valid []int) schema.SchemaValidateFunc {
return func(i interface{}, k string) (_ []string, errors []error) {
v, ok := i.(int)
if !ok {
errors = append(errors, fmt.Errorf("expected type of %s to be an integer", k))
return
}

for _, validInt := range valid {
if v == validInt {
errors = append(errors, fmt.Errorf("expected %s to not be one of %v, got %d", k, valid, v))
}
}

return
}
}
82 changes: 82 additions & 0 deletions helper/validation/int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,42 @@ func TestValidationIntAtMost(t *testing.T) {
})
}

func TestValidationIntDivisibleBy(t *testing.T) {
cases := map[string]struct {
Value interface{}
Divisor int
Error bool
}{
"NotInt": {
Value: "words",
Divisor: 2,
Error: true,
},
"NotDivisible": {
Value: 15,
Divisor: 7,
Error: true,
},
"Divisible": {
Value: 14,
Divisor: 7,
Error: false,
},
}

for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
_, errors := IntDivisibleBy(tc.Divisor)(tc.Value, tn)

if len(errors) > 0 && !tc.Error {
t.Errorf("IntDivisibleBy(%v) produced an unexpected error for %v", tc.Divisor, tc.Value)
} else if len(errors) == 0 && tc.Error {
t.Errorf("IntDivisibleBy(%v) did not error for %v", tc.Divisor, tc.Value)
}
})
}
}

func TestValidationIntInSlice(t *testing.T) {
runTestCases(t, []testCase{
{
Expand All @@ -92,3 +128,49 @@ func TestValidationIntInSlice(t *testing.T) {
},
})
}

func TestValidationIntNotInSlice(t *testing.T) {
cases := map[string]struct {
Value interface{}
Slice []int
Error bool
}{
"NotInt": {
Value: "words",
Slice: []int{7, 77},
Error: true,
},
"NotInSlice": {
Value: 1,
Slice: []int{7, 77},
Error: false,
},
"InSlice": {
Value: 7,
Slice: []int{7, 77},
Error: true,
},
"InSliceOfOne": {
Value: 7,
Slice: []int{7},
Error: true,
},
"NotInSliceOfOne": {
Value: 1,
Slice: []int{7},
Error: false,
},
}

for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
_, errors := IntNotInSlice(tc.Slice)(tc.Value, tn)

if len(errors) > 0 && !tc.Error {
t.Errorf("IntNotInSlice(%v) produced an unexpected error for %v", tc.Slice, tc.Value)
} else if len(errors) == 0 && tc.Error {
t.Errorf("IntNotInSlice(%v) did not error for %v", tc.Slice, tc.Value)
}
})
}
}
161 changes: 126 additions & 35 deletions helper/validation/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,81 +9,172 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)

// CIDRNetwork returns a SchemaValidateFunc which tests if the provided value
// is of type string, is in valid CIDR network notation, and has significant bits between min and max (inclusive)
func CIDRNetwork(min, max int) schema.SchemaValidateFunc {
// SingleIP returns a SchemaValidateFunc which tests if the provided value
// is of type string, and in valid single Value notation
func SingleIP() schema.SchemaValidateFunc {
return func(i interface{}, k string) (s []string, es []error) {
v, ok := i.(string)
if !ok {
es = append(es, fmt.Errorf("expected type of %s to be string", k))
return
}

_, ipnet, err := net.ParseCIDR(v)
if err != nil {
es = append(es, fmt.Errorf(
"expected %s to contain a valid CIDR, got: %s with err: %s", k, v, err))
return
ip := net.ParseIP(v)
if ip == nil {
es = append(es, fmt.Errorf("expected %s to contain a valid IP, got: %s", k, v))
}
return
}
}

if ipnet == nil || v != ipnet.String() {
es = append(es, fmt.Errorf(
"expected %s to contain a valid network CIDR, expected %s, got %s",
k, ipnet, v))
}
// IsIPv6Address is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv6 address
func IsIPv6Address(i interface{}, k string) (warnings []string, errors []error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs doc/comment

v, ok := i.(string)
if !ok {
errors = append(errors, fmt.Errorf("expected type of %q to be string", k))
return
}

sigbits, _ := ipnet.Mask.Size()
if sigbits < min || sigbits > max {
es = append(es, fmt.Errorf(
"expected %q to contain a network CIDR with between %d and %d significant bits, got: %d",
k, min, max, sigbits))
}
ip := net.ParseIP(v)
if six := ip.To16(); six == nil {
errors = append(errors, fmt.Errorf("expected %s to contain a valid IPv6 address, got: %s", k, v))
}

return warnings, errors
}

// IsIPv4Address is a SchemaValidateFunc which tests if the provided value is of type string and a valid IPv4 address
func IsIPv4Address(i interface{}, k string) (warnings []string, errors []error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs doc/comment

v, ok := i.(string)
if !ok {
errors = append(errors, fmt.Errorf("expected type of %q to be string", k))
return
}

ip := net.ParseIP(v)
if four := ip.To4(); four == nil {
errors = append(errors, fmt.Errorf("expected %s to contain a valid IPv4 address, got: %s", k, v))
}

return warnings, errors
}

// SingleIP returns a SchemaValidateFunc which tests if the provided value
// is of type string, and in valid single IP notation
func SingleIP() schema.SchemaValidateFunc {
// IPRange returns a SchemaValidateFunc which tests if the provided value is of type string, and in valid IP range
func IPRange() schema.SchemaValidateFunc {
return func(i interface{}, k string) (s []string, es []error) {
v, ok := i.(string)
if !ok {
es = append(es, fmt.Errorf("expected type of %s to be string", k))
return
}

ip := net.ParseIP(v)
if ip == nil {
ips := strings.Split(v, "-")
if len(ips) != 2 {
es = append(es, fmt.Errorf(
"expected %s to contain a valid IP range, got: %s", k, v))
return
}
ip1 := net.ParseIP(ips[0])
ip2 := net.ParseIP(ips[1])
if ip1 == nil || ip2 == nil || bytes.Compare(ip1, ip2) > 0 {
es = append(es, fmt.Errorf(
"expected %s to contain a valid IP, got: %s", k, v))
"expected %s to contain a valid IP range, got: %s", k, v))
}
return
}
}

// IPRange returns a SchemaValidateFunc which tests if the provided value
// is of type string, and in valid IP range notation
func IPRange() schema.SchemaValidateFunc {
// IsCIDR is a SchemaValidateFunc which tests if the provided value is of type string and a valid CIDR
func IsCIDR(i interface{}, k string) (warnings []string, errors []error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs doc/comment

v, ok := i.(string)
if !ok {
errors = append(errors, fmt.Errorf("expected type of %s to be string", k))
return
}

_, _, err := net.ParseCIDR(v)
if err != nil {
errors = append(errors, fmt.Errorf("expected %q to be a valid IPv4 Value, got %v: %v", k, i, err))
}

return warnings, errors
}

// CIDRNetwork returns a SchemaValidateFunc which tests if the provided value
// is of type string, is in valid Value network notation, and has significant bits between min and max (inclusive)
func CIDRNetwork(min, max int) schema.SchemaValidateFunc {
return func(i interface{}, k string) (s []string, es []error) {
v, ok := i.(string)
if !ok {
es = append(es, fmt.Errorf("expected type of %s to be string", k))
return
}

ips := strings.Split(v, "-")
if len(ips) != 2 {
_, ipnet, err := net.ParseCIDR(v)
if err != nil {
es = append(es, fmt.Errorf(
"expected %s to contain a valid IP range, got: %s", k, v))
"expected %s to contain a valid Value, got: %s with err: %s", k, v, err))
return
}
ip1 := net.ParseIP(ips[0])
ip2 := net.ParseIP(ips[1])
if ip1 == nil || ip2 == nil || bytes.Compare(ip1, ip2) > 0 {

if ipnet == nil || v != ipnet.String() {
es = append(es, fmt.Errorf(
"expected %s to contain a valid IP range, got: %s", k, v))
"expected %s to contain a valid network Value, expected %s, got %s",
k, ipnet, v))
}

sigbits, _ := ipnet.Mask.Size()
if sigbits < min || sigbits > max {
es = append(es, fmt.Errorf(
"expected %q to contain a network Value with between %d and %d significant bits, got: %d",
k, min, max, sigbits))
}

return
}
}

// IsMACAddress is a SchemaValidateFunc which tests if the provided value is of type string and a valid MAC address
func IsMACAddress(i interface{}, k string) (warnings []string, errors []error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs doc/comment

v, ok := i.(string)
if !ok {
errors = append(errors, fmt.Errorf("expected type of %q to be string", k))
return
}

if _, err := net.ParseMAC(v); err != nil {
errors = append(errors, fmt.Errorf("expected %q to be a valid MAC address, got %v: %v", k, i, err))
}

return warnings, errors
}

// IsPortNumber is a SchemaValidateFunc which tests if the provided value is of type string and a valid TCP Port Number
func IsPortNumber(i interface{}, k string) (warnings []string, errors []error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs doc/comment

v, ok := i.(int)
if !ok {
errors = append(errors, fmt.Errorf("expected type of %q to be int", k))
return
}

if 1 > v || v > 65535 {
errors = append(errors, fmt.Errorf("expected %q to be a valid port number, got: %v", k, v))
}

return warnings, errors
}

// IsPortNumberOrZero is a SchemaValidateFunc which tests if the provided value is of type string and a valid TCP Port Number or zero
func IsPortNumberOrZero(i interface{}, k string) (warnings []string, errors []error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs doc/comment

v, ok := i.(int)
if !ok {
errors = append(errors, fmt.Errorf("expected type of %q to be int", k))
return
}

if 0 > v || v > 65535 {
errors = append(errors, fmt.Errorf("expected %q to be a valid port number or 0, got: %v", k, v))
}

return warnings, errors
}
Loading