From 0f085750df2b4dc0dda6ef736d091eadd974b1ef Mon Sep 17 00:00:00 2001 From: Muhammad Meganata Adam Date: Tue, 18 Jun 2024 05:17:54 +0700 Subject: [PATCH 1/5] feat: Add KeysInValidator for validating fields containing specific keys --- validation/keysin.go | 39 +++++++++++++++++++++ validation/keysin_test.go | 72 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 validation/keysin.go create mode 100644 validation/keysin_test.go diff --git a/validation/keysin.go b/validation/keysin.go new file mode 100644 index 00000000..b911a70b --- /dev/null +++ b/validation/keysin.go @@ -0,0 +1,39 @@ +package validation + +import "strings" + +// KeysInValidator validates the field under validation must contain all the given keys. +type KeysInValidator struct { + BaseValidator + Keys []string +} + +// Validate checks the field under validation satisfies this validator's criteria. +func (v *KeysInValidator) Validate(ctx *Context) bool { + obj, ok := ctx.Value.(map[string]interface{}) + if !ok { + return false + } + + for _, key := range v.Keys { + if _, ok := obj[key]; !ok { + return false + } + } + return true +} + +// Name returns the string name of the validator. +func (v *KeysInValidator) Name() string { return "keysin" } + +// MessagePlaceholders returns the ":keys placeholder. +func (v *KeysInValidator) MessagePlaceholders(_ *Context) []string { + return []string{ + ":keys", strings.Join(v.Keys, ", "), + } +} + +// KeysIn the field under validation must contain all the given keys. +func KeysIn(keys ...string) *KeysInValidator { + return &KeysInValidator{Keys: keys} +} diff --git a/validation/keysin_test.go b/validation/keysin_test.go new file mode 100644 index 00000000..7b3a4e35 --- /dev/null +++ b/validation/keysin_test.go @@ -0,0 +1,72 @@ +package validation + +import "testing" + +func TestKeysInValidator_Validate(t *testing.T) { + type fields struct { + BaseValidator BaseValidator + Keys []string + } + type args struct { + ctx *Context + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "Valid", + fields: fields{ + Keys: []string{"key1", "key2"}, + }, + args: args{ + ctx: &Context{ + Value: map[string]interface{}{ + "key1": "value", + "key2": "value", + }, + }, + }, + want: true, + }, + { + name: "Invalid", + fields: fields{ + Keys: []string{"key1", "key2"}, + }, + args: args{ + ctx: &Context{ + Value: map[string]interface{}{ + "key1": "value", + }, + }, + }, + want: false, + }, + { + name: "Invalid - Not a map", + fields: fields{ + Keys: []string{"key1", "key2"}, + }, + args: args{ + ctx: &Context{ + Value: "not a map", + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := &KeysInValidator{ + BaseValidator: tt.fields.BaseValidator, + Keys: tt.fields.Keys, + } + if got := v.Validate(tt.args.ctx); got != tt.want { + t.Errorf("KeysInValidator.Validate() = %v, want %v", got, tt.want) + } + }) + } +} From 43af5d24ea28cf9115cdfd571092ad1e4883f16a Mon Sep 17 00:00:00 2001 From: Muhammad Meganata Adam Date: Tue, 18 Jun 2024 21:58:45 +0700 Subject: [PATCH 2/5] refactor: Remove KeysInValidator and related code --- lang/default.go | 2 ++ validation/keysin.go | 39 --------------------- validation/keysin_test.go | 72 --------------------------------------- validation/string.go | 43 +++++++++++++++++++++++ validation/string_test.go | 31 +++++++++++++++++ 5 files changed, 76 insertions(+), 111 deletions(-) delete mode 100644 validation/keysin.go delete mode 100644 validation/keysin_test.go diff --git a/lang/default.go b/lang/default.go index 6f5a57cd..77e6bbb1 100644 --- a/lang/default.go +++ b/lang/default.go @@ -189,6 +189,8 @@ var enUS = &Language{ "unique.element": "The :field element value has already been taken.", "exists": "The :field does not exist.", "exists.element": "The :field element value does not exist.", + "keysin": "The :field must have the following keys: :values.", + "keysin.element": "The :field elements must have the following keys: :values.", }, fields: map[string]string{ "": "body", diff --git a/validation/keysin.go b/validation/keysin.go deleted file mode 100644 index b911a70b..00000000 --- a/validation/keysin.go +++ /dev/null @@ -1,39 +0,0 @@ -package validation - -import "strings" - -// KeysInValidator validates the field under validation must contain all the given keys. -type KeysInValidator struct { - BaseValidator - Keys []string -} - -// Validate checks the field under validation satisfies this validator's criteria. -func (v *KeysInValidator) Validate(ctx *Context) bool { - obj, ok := ctx.Value.(map[string]interface{}) - if !ok { - return false - } - - for _, key := range v.Keys { - if _, ok := obj[key]; !ok { - return false - } - } - return true -} - -// Name returns the string name of the validator. -func (v *KeysInValidator) Name() string { return "keysin" } - -// MessagePlaceholders returns the ":keys placeholder. -func (v *KeysInValidator) MessagePlaceholders(_ *Context) []string { - return []string{ - ":keys", strings.Join(v.Keys, ", "), - } -} - -// KeysIn the field under validation must contain all the given keys. -func KeysIn(keys ...string) *KeysInValidator { - return &KeysInValidator{Keys: keys} -} diff --git a/validation/keysin_test.go b/validation/keysin_test.go deleted file mode 100644 index 7b3a4e35..00000000 --- a/validation/keysin_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package validation - -import "testing" - -func TestKeysInValidator_Validate(t *testing.T) { - type fields struct { - BaseValidator BaseValidator - Keys []string - } - type args struct { - ctx *Context - } - tests := []struct { - name string - fields fields - args args - want bool - }{ - { - name: "Valid", - fields: fields{ - Keys: []string{"key1", "key2"}, - }, - args: args{ - ctx: &Context{ - Value: map[string]interface{}{ - "key1": "value", - "key2": "value", - }, - }, - }, - want: true, - }, - { - name: "Invalid", - fields: fields{ - Keys: []string{"key1", "key2"}, - }, - args: args{ - ctx: &Context{ - Value: map[string]interface{}{ - "key1": "value", - }, - }, - }, - want: false, - }, - { - name: "Invalid - Not a map", - fields: fields{ - Keys: []string{"key1", "key2"}, - }, - args: args{ - ctx: &Context{ - Value: "not a map", - }, - }, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - v := &KeysInValidator{ - BaseValidator: tt.fields.BaseValidator, - Keys: tt.fields.Keys, - } - if got := v.Validate(tt.args.ctx); got != tt.want { - t.Errorf("KeysInValidator.Validate() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/validation/string.go b/validation/string.go index a1c8933b..068ebf8c 100644 --- a/validation/string.go +++ b/validation/string.go @@ -124,3 +124,46 @@ func (v *DoesntStartWithValidator) MessagePlaceholders(_ *Context) []string { func DoesntStartWith(prefix ...string) *DoesntStartWithValidator { return &DoesntStartWithValidator{Prefix: prefix} } + +//------------------------------ + +// KeysInValidator validates the field under validation must only contain the given keys. +type KeysInValidator struct { + BaseValidator + Keys []string +} + +// Validate checks the field under validation satisfies this validator's criteria. +func (v *KeysInValidator) Validate(ctx *Context) bool { + obj, ok := ctx.Value.(map[string]interface{}) + if !ok { + return false + } + + allowedKeys := make(map[string]struct{}) + for _, key := range v.Keys { + allowedKeys[key] = struct{}{} + } + + for key := range obj { + if _, ok := allowedKeys[key]; !ok { + return false + } + } + return true +} + +// Name returns the string name of the validator. +func (v *KeysInValidator) Name() string { return "keys_in" } + +// MessagePlaceholders returns the ":values" placeholder. +func (v *KeysInValidator) MessagePlaceholders(_ *Context) []string { + return []string{ + ":values", strings.Join(v.Keys, ", "), + } +} + +// KeysIn creates a new KeysInValidator. +func KeysIn(keys ...string) *KeysInValidator { + return &KeysInValidator{Keys: keys} +} diff --git a/validation/string_test.go b/validation/string_test.go index 32911bca..6855f7d4 100644 --- a/validation/string_test.go +++ b/validation/string_test.go @@ -168,3 +168,34 @@ func TestDoesntStartWithValidator(t *testing.T) { }) } } + +func TestKeysInValidator(t *testing.T) { + t.Run("Constructor", func(t *testing.T) { + v := KeysIn([]string{"a", "b", "c"}...) + assert.NotNil(t, v) + assert.Equal(t, "keys_in", v.Name()) + assert.False(t, v.IsType()) + assert.False(t, v.IsTypeDependent()) + assert.Equal(t, []string{":values", "a, b, c"}, v.MessagePlaceholders(&Context{})) + }) + + cases := []struct { + value []string + input any + want bool + }{ + {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2, "c": 3}, want: true}, + {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2, "c": 3, "d": 4}, want: false}, + {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2}, want: false}, + } + + for _, tc := range cases { + tc := tc + t.Run(fmt.Sprintf("Validate_%v_%t", tc.value, tc.want), func(t *testing.T) { + v := KeysIn(tc.value...) + assert.Equal(t, tc.want, v.Validate(&Context{ + Value: tc.input, + })) + }) + } +} From 0064072a1c4573a0f7db1d29678f80d5ead49f38 Mon Sep 17 00:00:00 2001 From: Muhammad Meganata Adam Date: Wed, 19 Jun 2024 05:45:11 +0700 Subject: [PATCH 3/5] feat: Update KeysInValidator to include missing keys --- validation/string_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validation/string_test.go b/validation/string_test.go index 6855f7d4..fbfc191b 100644 --- a/validation/string_test.go +++ b/validation/string_test.go @@ -186,7 +186,7 @@ func TestKeysInValidator(t *testing.T) { }{ {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2, "c": 3}, want: true}, {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2, "c": 3, "d": 4}, want: false}, - {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2}, want: false}, + {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2}, want: true}, } for _, tc := range cases { From f6fcd84c6aabffc83d36f87c62ad94440429e837 Mon Sep 17 00:00:00 2001 From: SystemGlitch Date: Fri, 21 Jun 2024 10:54:49 +0200 Subject: [PATCH 4/5] Validation: KeysIn code review --- lang/default.go | 4 ++-- validation/string.go | 6 ++++-- validation/string_test.go | 14 +++++++++++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lang/default.go b/lang/default.go index 77e6bbb1..ce2f6f1d 100644 --- a/lang/default.go +++ b/lang/default.go @@ -189,8 +189,8 @@ var enUS = &Language{ "unique.element": "The :field element value has already been taken.", "exists": "The :field does not exist.", "exists.element": "The :field element value does not exist.", - "keysin": "The :field must have the following keys: :values.", - "keysin.element": "The :field elements must have the following keys: :values.", + "keysin": "The :field keys must be one of the following: :values.", + "keysin.element": "The :field elements keys must be one of the following: :values.", }, fields: map[string]string{ "": "body", diff --git a/validation/string.go b/validation/string.go index 068ebf8c..501f394f 100644 --- a/validation/string.go +++ b/validation/string.go @@ -127,7 +127,8 @@ func DoesntStartWith(prefix ...string) *DoesntStartWithValidator { //------------------------------ -// KeysInValidator validates the field under validation must only contain the given keys. +// KeysInValidator the field under validation must be an object and all its keys must +// be equal to one of the given values. type KeysInValidator struct { BaseValidator Keys []string @@ -163,7 +164,8 @@ func (v *KeysInValidator) MessagePlaceholders(_ *Context) []string { } } -// KeysIn creates a new KeysInValidator. +// KeysIn the field under validation must be an object and all its keys must +// be equal to one of the given values. func KeysIn(keys ...string) *KeysInValidator { return &KeysInValidator{Keys: keys} } diff --git a/validation/string_test.go b/validation/string_test.go index fbfc191b..3c30da03 100644 --- a/validation/string_test.go +++ b/validation/string_test.go @@ -171,7 +171,7 @@ func TestDoesntStartWithValidator(t *testing.T) { func TestKeysInValidator(t *testing.T) { t.Run("Constructor", func(t *testing.T) { - v := KeysIn([]string{"a", "b", "c"}...) + v := KeysIn("a", "b", "c") assert.NotNil(t, v) assert.Equal(t, "keys_in", v.Name()) assert.False(t, v.IsType()) @@ -180,18 +180,26 @@ func TestKeysInValidator(t *testing.T) { }) cases := []struct { - value []string input any + value []string want bool }{ {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2, "c": 3}, want: true}, {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2, "c": 3, "d": 4}, want: false}, {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2}, want: true}, + {value: []string{"a", "b", "c"}, input: map[string]any{}, want: true}, + {value: []string{"a", "b", "c"}, input: "", want: false}, + {value: []string{"a", "b", "c"}, input: 'a', want: false}, + {value: []string{"a", "b", "c"}, input: 2, want: false}, + {value: []string{"a", "b", "c"}, input: 2.5, want: false}, + {value: []string{"a", "b", "c"}, input: true, want: false}, + {value: []string{"a", "b", "c"}, input: nil, want: false}, + {value: []string{"a", "b", "c"}, input: (map[string]any)(nil), want: true}, } for _, tc := range cases { tc := tc - t.Run(fmt.Sprintf("Validate_%v_%t", tc.value, tc.want), func(t *testing.T) { + t.Run(fmt.Sprintf("Validate_%v_%t", tc.input, tc.want), func(t *testing.T) { v := KeysIn(tc.value...) assert.Equal(t, tc.want, v.Validate(&Context{ Value: tc.input, From cdef86ea39845026daee18abf9dd2d5033220d7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20LAMBERT?= Date: Fri, 21 Jun 2024 11:00:20 +0200 Subject: [PATCH 5/5] Validation: move KeysIn to its own file --- validation/keysin.go | 46 ++++++++++++++++++++++++++++++++++++++ validation/keysin_test.go | 47 +++++++++++++++++++++++++++++++++++++++ validation/string.go | 45 ------------------------------------- validation/string_test.go | 39 -------------------------------- 4 files changed, 93 insertions(+), 84 deletions(-) create mode 100644 validation/keysin.go create mode 100644 validation/keysin_test.go diff --git a/validation/keysin.go b/validation/keysin.go new file mode 100644 index 00000000..4c79f260 --- /dev/null +++ b/validation/keysin.go @@ -0,0 +1,46 @@ +package validation + +import "strings" + +// KeysInValidator the field under validation must be an object and all its keys must +// be equal to one of the given values. +type KeysInValidator struct { + BaseValidator + Keys []string +} + +// Validate checks the field under validation satisfies this validator's criteria. +func (v *KeysInValidator) Validate(ctx *Context) bool { + obj, ok := ctx.Value.(map[string]any) + if !ok { + return false + } + + allowedKeys := make(map[string]struct{}) + for _, key := range v.Keys { + allowedKeys[key] = struct{}{} + } + + for key := range obj { + if _, ok := allowedKeys[key]; !ok { + return false + } + } + return true +} + +// Name returns the string name of the validator. +func (v *KeysInValidator) Name() string { return "keys_in" } + +// MessagePlaceholders returns the ":values" placeholder. +func (v *KeysInValidator) MessagePlaceholders(_ *Context) []string { + return []string{ + ":values", strings.Join(v.Keys, ", "), + } +} + +// KeysIn the field under validation must be an object and all its keys must +// be equal to one of the given values. +func KeysIn(keys ...string) *KeysInValidator { + return &KeysInValidator{Keys: keys} +} diff --git a/validation/keysin_test.go b/validation/keysin_test.go new file mode 100644 index 00000000..2ad4fe3b --- /dev/null +++ b/validation/keysin_test.go @@ -0,0 +1,47 @@ +package validation + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeysInValidator(t *testing.T) { + t.Run("Constructor", func(t *testing.T) { + v := KeysIn("a", "b", "c") + assert.NotNil(t, v) + assert.Equal(t, "keys_in", v.Name()) + assert.False(t, v.IsType()) + assert.False(t, v.IsTypeDependent()) + assert.Equal(t, []string{":values", "a, b, c"}, v.MessagePlaceholders(&Context{})) + }) + + cases := []struct { + input any + allowedKeys []string + want bool + }{ + {allowedKeys: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2, "c": 3}, want: true}, + {allowedKeys: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2, "c": 3, "d": 4}, want: false}, + {allowedKeys: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2}, want: true}, + {allowedKeys: []string{"a", "b", "c"}, input: map[string]any{}, want: true}, + {allowedKeys: []string{"a", "b", "c"}, input: "", want: false}, + {allowedKeys: []string{"a", "b", "c"}, input: 'a', want: false}, + {allowedKeys: []string{"a", "b", "c"}, input: 2, want: false}, + {allowedKeys: []string{"a", "b", "c"}, input: 2.5, want: false}, + {allowedKeys: []string{"a", "b", "c"}, input: true, want: false}, + {allowedKeys: []string{"a", "b", "c"}, input: nil, want: false}, + {allowedKeys: []string{"a", "b", "c"}, input: (map[string]any)(nil), want: true}, + } + + for _, tc := range cases { + tc := tc + t.Run(fmt.Sprintf("Validate_%v_%t", tc.input, tc.want), func(t *testing.T) { + v := KeysIn(tc.allowedKeys...) + assert.Equal(t, tc.want, v.Validate(&Context{ + Value: tc.input, + })) + }) + } +} diff --git a/validation/string.go b/validation/string.go index 501f394f..a1c8933b 100644 --- a/validation/string.go +++ b/validation/string.go @@ -124,48 +124,3 @@ func (v *DoesntStartWithValidator) MessagePlaceholders(_ *Context) []string { func DoesntStartWith(prefix ...string) *DoesntStartWithValidator { return &DoesntStartWithValidator{Prefix: prefix} } - -//------------------------------ - -// KeysInValidator the field under validation must be an object and all its keys must -// be equal to one of the given values. -type KeysInValidator struct { - BaseValidator - Keys []string -} - -// Validate checks the field under validation satisfies this validator's criteria. -func (v *KeysInValidator) Validate(ctx *Context) bool { - obj, ok := ctx.Value.(map[string]interface{}) - if !ok { - return false - } - - allowedKeys := make(map[string]struct{}) - for _, key := range v.Keys { - allowedKeys[key] = struct{}{} - } - - for key := range obj { - if _, ok := allowedKeys[key]; !ok { - return false - } - } - return true -} - -// Name returns the string name of the validator. -func (v *KeysInValidator) Name() string { return "keys_in" } - -// MessagePlaceholders returns the ":values" placeholder. -func (v *KeysInValidator) MessagePlaceholders(_ *Context) []string { - return []string{ - ":values", strings.Join(v.Keys, ", "), - } -} - -// KeysIn the field under validation must be an object and all its keys must -// be equal to one of the given values. -func KeysIn(keys ...string) *KeysInValidator { - return &KeysInValidator{Keys: keys} -} diff --git a/validation/string_test.go b/validation/string_test.go index 3c30da03..32911bca 100644 --- a/validation/string_test.go +++ b/validation/string_test.go @@ -168,42 +168,3 @@ func TestDoesntStartWithValidator(t *testing.T) { }) } } - -func TestKeysInValidator(t *testing.T) { - t.Run("Constructor", func(t *testing.T) { - v := KeysIn("a", "b", "c") - assert.NotNil(t, v) - assert.Equal(t, "keys_in", v.Name()) - assert.False(t, v.IsType()) - assert.False(t, v.IsTypeDependent()) - assert.Equal(t, []string{":values", "a, b, c"}, v.MessagePlaceholders(&Context{})) - }) - - cases := []struct { - input any - value []string - want bool - }{ - {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2, "c": 3}, want: true}, - {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2, "c": 3, "d": 4}, want: false}, - {value: []string{"a", "b", "c"}, input: map[string]any{"a": 1, "b": 2}, want: true}, - {value: []string{"a", "b", "c"}, input: map[string]any{}, want: true}, - {value: []string{"a", "b", "c"}, input: "", want: false}, - {value: []string{"a", "b", "c"}, input: 'a', want: false}, - {value: []string{"a", "b", "c"}, input: 2, want: false}, - {value: []string{"a", "b", "c"}, input: 2.5, want: false}, - {value: []string{"a", "b", "c"}, input: true, want: false}, - {value: []string{"a", "b", "c"}, input: nil, want: false}, - {value: []string{"a", "b", "c"}, input: (map[string]any)(nil), want: true}, - } - - for _, tc := range cases { - tc := tc - t.Run(fmt.Sprintf("Validate_%v_%t", tc.input, tc.want), func(t *testing.T) { - v := KeysIn(tc.value...) - assert.Equal(t, tc.want, v.Validate(&Context{ - Value: tc.input, - })) - }) - } -}