From 42fa8cb9ba91249c9f7c8be2fbd092ebafb93032 Mon Sep 17 00:00:00 2001 From: ryodocx Date: Fri, 17 Jun 2022 09:26:24 +0900 Subject: [PATCH 1/7] add validator: StringRuneCountBetween and StringBytesBetween --- helper/validation/strings.go | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/helper/validation/strings.go b/helper/validation/strings.go index e739a1a1bfa..5e7bc475dd0 100644 --- a/helper/validation/strings.go +++ b/helper/validation/strings.go @@ -5,6 +5,7 @@ import ( "fmt" "regexp" "strings" + "unicode/utf8" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/structure" @@ -66,9 +67,16 @@ func StringIsWhiteSpace(i interface{}, k string) ([]string, []error) { return nil, nil } +// [deplicated] // StringLenBetween returns a SchemaValidateFunc which tests if the provided value -// is of type string and has length between min and max (inclusive) +// is of type string and has 'Byte' length between min and max (inclusive) func StringLenBetween(min, max int) schema.SchemaValidateFunc { + return StringBytesBetween(min, max) +} + +// StringBytesBetween returns a SchemaValidateFunc which tests if the provided value +// is of type string and has 'Byte' length between min and max (inclusive) +func StringBytesBetween(min, max int) schema.SchemaValidateFunc { return func(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { @@ -84,6 +92,24 @@ func StringLenBetween(min, max int) schema.SchemaValidateFunc { } } +// StringRuneCountBetween returns a SchemaValidateFunc which tests if the provided value +// is of type string and has 'Rune' length between min and max (inclusive) +func StringRuneCountBetween(min, max int) schema.SchemaValidateFunc { + return func(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) + return warnings, errors + } + + if c := utf8.RuneCountInString(v); c < min || c > max { + errors = append(errors, fmt.Errorf("expected length of %s to be in the range (%d - %d), got %s", k, min, max, v)) + } + + return warnings, errors + } +} + // StringMatch returns a SchemaValidateFunc which tests if the provided value // matches a given regexp. Optionally an error message can be provided to // return something friendlier than "must match some globby regexp". From a2a938bfb6376ef37319407bac93ef5f6f99b5b0 Mon Sep 17 00:00:00 2001 From: ryodocx Date: Fri, 17 Jun 2022 09:28:26 +0900 Subject: [PATCH 2/7] fix typo --- helper/validation/strings.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper/validation/strings.go b/helper/validation/strings.go index 5e7bc475dd0..d083ccf2541 100644 --- a/helper/validation/strings.go +++ b/helper/validation/strings.go @@ -67,7 +67,7 @@ func StringIsWhiteSpace(i interface{}, k string) ([]string, []error) { return nil, nil } -// [deplicated] +// [deprecated] // StringLenBetween returns a SchemaValidateFunc which tests if the provided value // is of type string and has 'Byte' length between min and max (inclusive) func StringLenBetween(min, max int) schema.SchemaValidateFunc { From 94c303f546fe731bdd7ec1abfec72640b840b289 Mon Sep 17 00:00:00 2001 From: ryodocx Date: Sun, 19 Jun 2022 22:38:17 +0900 Subject: [PATCH 3/7] add validation --- helper/validation/strings.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/helper/validation/strings.go b/helper/validation/strings.go index d083ccf2541..51fb40d0388 100644 --- a/helper/validation/strings.go +++ b/helper/validation/strings.go @@ -78,6 +78,17 @@ func StringLenBetween(min, max int) schema.SchemaValidateFunc { // is of type string and has 'Byte' length between min and max (inclusive) func StringBytesBetween(min, max int) schema.SchemaValidateFunc { return func(i interface{}, k string) (warnings []string, errors []error) { + + if min < 0 { + errors = append(errors, fmt.Errorf("min must be zero or natural number (actual: %d)", min)) + } + if max < 0 { + errors = append(errors, fmt.Errorf("max must be zero or natural number (actual: %d)", max)) + } + if min > max { + errors = append(errors, fmt.Errorf("min must be less than or equal to max (actual: min=%d, max=%d)", min, max)) + } + v, ok := i.(string) if !ok { errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) @@ -96,6 +107,17 @@ func StringBytesBetween(min, max int) schema.SchemaValidateFunc { // is of type string and has 'Rune' length between min and max (inclusive) func StringRuneCountBetween(min, max int) schema.SchemaValidateFunc { return func(i interface{}, k string) (warnings []string, errors []error) { + + if min < 0 { + errors = append(errors, fmt.Errorf("min must be zero or natural number (actual: %d)", min)) + } + if max < 0 { + errors = append(errors, fmt.Errorf("max must be zero or natural number (actual: %d)", max)) + } + if min > max { + errors = append(errors, fmt.Errorf("min must be less than or equal to max (actual: min=%d, max=%d)", min, max)) + } + v, ok := i.(string) if !ok { errors = append(errors, fmt.Errorf("expected type of %s to be string", k)) From 333aad5d918361f92b07434f4c1b1777ee9f2b72 Mon Sep 17 00:00:00 2001 From: ryodocx Date: Sun, 19 Jun 2022 22:39:45 +0900 Subject: [PATCH 4/7] add test --- helper/validation/strings_test.go | 338 ++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) diff --git a/helper/validation/strings_test.go b/helper/validation/strings_test.go index 61ae6b4e699..c0dbb9bff7b 100644 --- a/helper/validation/strings_test.go +++ b/helper/validation/strings_test.go @@ -240,6 +240,344 @@ func TestValidationStringIsWhiteSpace(t *testing.T) { } } +func TestValidationStringBytesBetween(t *testing.T) { + cases := map[string]struct { + Value interface{} + Min int + Max int + Error bool + }{ + "NotStringNil": { + Value: nil, + Error: true, + }, + "NotStringBool": { + Value: bool(true), + Error: true, + }, + "NotStringInt": { + Value: int(-1), + Error: true, + }, + "NotStringUint": { + Value: uint(1), + Error: true, + }, + "NotStringByteSlice": { + Value: []byte("hello"), + Error: true, + }, + "NotStringRuneSlice": { + Value: []rune("こんにちは"), + Error: true, + }, + "NotStringFloat32": { + Value: float32(1.23), + Error: true, + }, + "NotStringFloat64": { + Value: float32(-1.23), + Error: true, + }, + "MinNegativeNumber": { + Value: "MinNegativeNumber", + Min: -1, + Max: 17, + Error: true, + }, + "MinZero": { + Value: "MinZero", + Min: 0, + Max: 7, + Error: false, + }, + "MinPositiveNumber": { + Value: "MinPositiveNumber", + Min: 1, + Max: 17, + Error: false, + }, + "MaxNegativeNumber": { + Value: "MaxNegativeNumber", + Min: 0, + Max: -1, + Error: true, + }, + "MaxZero": { + Value: "", + Min: 0, + Max: 0, + Error: false, + }, + "MaxPositiveNumber": { + Value: "MaxPositiveNumber", + Min: 17, + Max: 17, + Error: false, + }, + "MinLowerThanByteLength": { + Value: "MinLowerThanByteLength", + Min: 21, + Max: 2147483647, + Error: false, + }, + "MinEqualByteLength": { + Value: "MinEqualByteLength", + Min: 18, + Max: 2147483647, + Error: false, + }, + "MinGreaterThanByteLength": { + Value: "MinGreaterThanByteLength", + Min: 25, + Max: 2147483647, + Error: true, + }, + "MaxLowerThanByteLength": { + Value: "MaxLowerThanByteLength", + Min: 0, + Max: 21, + Error: true, + }, + "MaxEqualByteLength": { + Value: "MaxEqualByteLength", + Min: 0, + Max: 18, + Error: false, + }, + "MaxGreaterThanByteLength": { + Value: "MaxGreaterThanByteLength", + Min: 0, + Max: 25, + Error: false, + }, + "Empty": { + Value: "", + Min: 0, + Max: 0, + Error: false, + }, + "WhiteSpace": { + Value: " ", + Min: 1, + Max: 1, + Error: false, + }, + "Tab": { + Value: "\t", + Min: 1, + Max: 1, + Error: false, + }, + "1ByteString": { + Value: "Hello world!", + Min: 12, + Max: 12, + Error: false, + }, + "2BytesString": { + Value: "αβγ", + Min: 6, + Max: 6, + Error: false, + }, + "3BytesString": { + Value: "こんにちは世界!", + Min: 24, + Max: 24, + Error: false, + }, + "4BytesString": { + Value: "👍", + Min: 4, + Max: 4, + Error: false, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + v := StringBytesBetween(tc.Min, tc.Max) + _, errors := v(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("StringBytesBetween(%d, %d) with '%v' produced an unexpected error", tc.Min, tc.Max, tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("StringBytesBetween(%d, %d) with '%v' did not error", tc.Min, tc.Max, tc.Value) + } + }) + } +} + +func TestValidationStringRuneCountBetween(t *testing.T) { + cases := map[string]struct { + Value interface{} + Min int + Max int + Error bool + }{ + "NotStringNil": { + Value: nil, + Error: true, + }, + "NotStringBool": { + Value: bool(true), + Error: true, + }, + "NotStringInt": { + Value: int(-1), + Error: true, + }, + "NotStringUint": { + Value: uint(1), + Error: true, + }, + "NotStringByteSlice": { + Value: []byte("hello"), + Error: true, + }, + "NotStringRuneSlice": { + Value: []rune("こんにちは"), + Error: true, + }, + "NotStringFloat32": { + Value: float32(1.23), + Error: true, + }, + "NotStringFloat64": { + Value: float32(-1.23), + Error: true, + }, + "MinNegativeNumber": { + Value: "MinNegativeNumber", + Min: -1, + Max: 17, + Error: true, + }, + "MinZero": { + Value: "MinZero", + Min: 0, + Max: 7, + Error: false, + }, + "MinPositiveNumber": { + Value: "MinPositiveNumber", + Min: 1, + Max: 17, + Error: false, + }, + "MaxNegativeNumber": { + Value: "MaxNegativeNumber", + Min: 0, + Max: -1, + Error: true, + }, + "MaxZero": { + Value: "", + Min: 0, + Max: 0, + Error: false, + }, + "MaxPositiveNumber": { + Value: "MaxPositiveNumber", + Min: 17, + Max: 17, + Error: false, + }, + "MinLowerThanByteLength": { + Value: "MinLowerThanByteLength", + Min: 21, + Max: 2147483647, + Error: false, + }, + "MinEqualByteLength": { + Value: "MinEqualByteLength", + Min: 18, + Max: 2147483647, + Error: false, + }, + "MinGreaterThanByteLength": { + Value: "MinGreaterThanByteLength", + Min: 25, + Max: 2147483647, + Error: true, + }, + "MaxLowerThanByteLength": { + Value: "MaxLowerThanByteLength", + Min: 0, + Max: 21, + Error: true, + }, + "MaxEqualByteLength": { + Value: "MaxEqualByteLength", + Min: 0, + Max: 18, + Error: false, + }, + "MaxGreaterThanByteLength": { + Value: "MaxGreaterThanByteLength", + Min: 0, + Max: 25, + Error: false, + }, + "Empty": { + Value: "", + Min: 0, + Max: 0, + Error: false, + }, + "WhiteSpace": { + Value: " ", + Min: 1, + Max: 1, + Error: false, + }, + "Tab": { + Value: "\t", + Min: 1, + Max: 1, + Error: false, + }, + "1ByteString": { + Value: "Hello world!", + Min: 12, + Max: 12, + Error: false, + }, + "2BytesString": { + Value: "αβγ", + Min: 3, + Max: 3, + Error: false, + }, + "3BytesString": { + Value: "こんにちは世界!", + Min: 8, + Max: 8, + Error: false, + }, + "4BytesString": { + Value: "👍", + Min: 1, + Max: 1, + Error: false, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + v := StringRuneCountBetween(tc.Min, tc.Max) + _, errors := v(tc.Value, tn) + + if len(errors) > 0 && !tc.Error { + t.Errorf("StringRuneCountBetween(%d, %d) with '%v' produced an unexpected error", tc.Min, tc.Max, tc.Value) + } else if len(errors) == 0 && tc.Error { + t.Errorf("StringRuneCountBetween(%d, %d) with '%v' did not error", tc.Min, tc.Max, tc.Value) + } + }) + } +} + func TestValidationStringIsBase64(t *testing.T) { cases := map[string]struct { Value interface{} From e6e1044f95291a8babd6957b4824b7c0425049c7 Mon Sep 17 00:00:00 2001 From: ryodocx Date: Sun, 19 Jun 2022 22:46:18 +0900 Subject: [PATCH 5/7] add comments --- helper/validation/strings.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/helper/validation/strings.go b/helper/validation/strings.go index 51fb40d0388..c837c02ce61 100644 --- a/helper/validation/strings.go +++ b/helper/validation/strings.go @@ -67,7 +67,8 @@ func StringIsWhiteSpace(i interface{}, k string) ([]string, []error) { return nil, nil } -// [deprecated] +// Deprecated: Use StringBytesBetween() instead. +// **Recommend StringRuneCountBetween()** in order to count 'String length' correctly // StringLenBetween returns a SchemaValidateFunc which tests if the provided value // is of type string and has 'Byte' length between min and max (inclusive) func StringLenBetween(min, max int) schema.SchemaValidateFunc { @@ -76,6 +77,7 @@ func StringLenBetween(min, max int) schema.SchemaValidateFunc { // StringBytesBetween returns a SchemaValidateFunc which tests if the provided value // is of type string and has 'Byte' length between min and max (inclusive) +// **Recommend StringRuneCountBetween()** in order to count 'String length' correctly func StringBytesBetween(min, max int) schema.SchemaValidateFunc { return func(i interface{}, k string) (warnings []string, errors []error) { From a1aaaff6d65e3c93dfd9649682aac700e07f8160 Mon Sep 17 00:00:00 2001 From: ryodocx Date: Mon, 20 Jun 2022 00:17:29 +0900 Subject: [PATCH 6/7] add changelog --- .changelog/TBD.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changelog/TBD.txt diff --git a/.changelog/TBD.txt b/.changelog/TBD.txt new file mode 100644 index 00000000000..d76208dd2f2 --- /dev/null +++ b/.changelog/TBD.txt @@ -0,0 +1,11 @@ +```release-note:note +helper/validation: The `StringLenBetween()` function is being deprecated in favor of the `StringBytesBetween()` function +``` + +```release-note:feature +helper/validation: Added `StringRuneCountBetween()` function for validate string with `number of characters` +``` + +```release-note:enhancement +helper/validation: Added validation for parameters at `StringLenBetween()` function +``` From 011a6a39fbc523ce20c851eae5b0cdf1a50cb73a Mon Sep 17 00:00:00 2001 From: ryodocx Date: Mon, 20 Jun 2022 00:22:29 +0900 Subject: [PATCH 7/7] rename --- .changelog/{TBD.txt => 985.txt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .changelog/{TBD.txt => 985.txt} (100%) diff --git a/.changelog/TBD.txt b/.changelog/985.txt similarity index 100% rename from .changelog/TBD.txt rename to .changelog/985.txt