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 StringIsEmpty, StringIsWhitespace, StringIsNotEmpty, StringIsNotWhitespace, StringIsBase64, UUID, UUIDOrEmpty #294

Merged
merged 13 commits into from
Jan 14, 2020
2 changes: 2 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/terraform-plugin-sdk.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions helper/validation/strings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package validation

import (
"encoding/base64"
"fmt"
"strings"
)

// StringIsNotEmpty is a ValidateFunc that ensures a string is not empty or consisting entirely of whitespace characters
func StringIsNotEmpty(i interface{}, k string) ([]string, []error) {
katbyte marked this conversation as resolved.
Show resolved Hide resolved
v, ok := i.(string)
if !ok {
return nil, []error{fmt.Errorf("expected type of %q to be string", k)}
}

if strings.TrimSpace(v) == "" {
return nil, []error{fmt.Errorf("expected %q it not be an empty string (whitespace not allowed)", k)}
}

return nil, nil
}

// StringIsBase64 is a ValidateFunc that ensures a string can be parsed as Base64
func StringIsBase64(i interface{}, k string) (warnings []string, errors []error) {
// Empty string is not allowed
if warnings, errors = StringIsNotEmpty(i, k); len(errors) > 0 {
return
}

// NoEmptyStrings checks it is a string
v, _ := i.(string)
if _, err := base64.StdEncoding.DecodeString(v); err != nil {
errors = append(errors, fmt.Errorf("expected %q to be a base64 string, got %v", k, v))
}

return
}
118 changes: 118 additions & 0 deletions helper/validation/strings_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package validation

import (
"testing"
)

func TestValidationStringIsNotEmpty(t *testing.T) {
cases := map[string]struct {
Value interface{}
Error bool
}{
"NotString": {
Value: 7,
Error: true,
},
"Empty": {
Value: "",
Error: true,
},
"SingleSpace": {
Value: " ",
Error: true,
},
"MultipleSpaces": {
Value: " ",
Error: true,
},
"CarriageReturn": {
Value: "\r",
Error: true,
},
"NewLine": {
Value: "\n",
Error: true,
},
"Tab": {
Value: "\t",
Error: true,
},
"FormFeed": {
Value: "\f",
Error: true,
},
"VerticalTab": {
Value: "\v",
Error: true,
},
"SingleChar": {
Value: "\v",
Error: true,
},
"MultipleChars": {
Value: "-_-",
Error: false,
},
"Sentence": {
Value: "Hello kt's sentence.",
Error: false,
},

"StartsWithWhitespace": {
Value: " 7",
Error: false,
},
"EndsWithWhitespace": {
Value: "7 ",
Error: false,
},
}

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

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

func TestValidationStringIsBase64(t *testing.T) {
cases := map[string]struct {
Value interface{}
Error bool
}{
"NotString": {
Value: 7,
Error: true,
},
"Empty": {
Value: "",
Error: true,
},
"NotBase64": {
Value: "Do'h!",
Error: true,
},
"Base64": {
Value: "RG8naCE=",
Error: false,
},
}

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

if len(errors) > 0 && !tc.Error {
t.Errorf("StringIsBase64(%s) produced an unexpected error", tc.Value)
} else if len(errors) == 0 && tc.Error {
t.Errorf("StringIsBase64(%s) did not error", tc.Value)
}
})
}
}
41 changes: 41 additions & 0 deletions helper/validation/uuid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package validation

import (
"fmt"
"regexp"

"github.com/hashicorp/go-uuid"
)

// UUIDRegExp is a Regular Expression that can be used to validate UUIDs
var UUIDRegExp = regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
Copy link
Contributor

Choose a reason for hiding this comment

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

nice 👍


// UUID is a ValidateFunc that ensures a string can be parsed as UUID
func UUID(i interface{}, k string) (warnings []string, errors []error) {
v, ok := i.(string)
if !ok {
errors = append(errors, fmt.Errorf("expected type of %q to be string", k))
return
}

if _, err := uuid.ParseUUID(v); err != nil {
errors = append(errors, fmt.Errorf("expected %q to be a valid UUID, got %v", k, v))
}

return warnings, errors
}

// UUID is a ValidateFunc that ensures a string is empty or can be parsed as UUID
func UUIDOrEmpty(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.

I would prefer encourage composition with validation.Any.

validation.Any(validation.UUID, validation.StringIsEmpty)

I mentioned earlier it might be nice to add StringIsEmpty as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ooo i like that

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

if v == "" {
return
}

return UUID(i, k)
}
85 changes: 85 additions & 0 deletions helper/validation/uuid_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package validation

import (
"testing"
)

func TestValidationUUID(t *testing.T) {
cases := map[string]struct {
Value interface{}
Error bool
}{
"NotString": {
Value: 7,
Error: true,
},
"Empty": {
Value: "",
Error: true,
},
"InvalidUuid": {
Value: "00000000-0000-123-0000-000000000000",
Error: true,
},
"ValidUuidWithOutDashs": {
Value: "12345678123412341234123456789012",
Error: true,
},
"ValidUuid": {
Value: "00000000-0000-0000-0000-000000000000",
Error: false,
},
}

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

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

func TestValidationUUIDorEmpty(t *testing.T) {
cases := map[string]struct {
Value interface{}
Error bool
}{
"NotString": {
Value: 7,
Error: true,
},
"Empty": {
Value: "",
Error: false,
},
"InvalidUuid": {
Value: "00000000-0000-123-0000-000000000000",
Error: true,
},
"ValidUuidWithOutDashs": {
Value: "12345678123412341234123456789012",
Error: true,
},
"ValidUuid": {
Value: "00000000-0000-0000-0000-000000000000",
Error: false,
},
}

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

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