Skip to content

Commit 1092010

Browse files
authored
Add EachWithOptions for Validating Arrays of Objects (#12)
* adding possibility to validate array of objects * fixed a type i test validate
1 parent 48ca92d commit 1092010

File tree

4 files changed

+178
-2
lines changed

4 files changed

+178
-2
lines changed

validator/range.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,34 @@ func Each(validatorFunc ValidatorFunc) ValidatorFunc {
109109
return nil
110110
}
111111
}
112+
113+
// EachWithOptions applies a set of validation options to each element in a slice or array, returning the first error
114+
func EachWithOptions(options []ValidationOption) ValidatorFunc {
115+
return func(value interface{}) error {
116+
if value == nil {
117+
return fmt.Errorf("value must be a non-nil slice or array")
118+
}
119+
v := reflect.ValueOf(value)
120+
if v.Kind() != reflect.Slice && v.Kind() != reflect.Array {
121+
return fmt.Errorf("value must be a slice or array, got %T", value)
122+
}
123+
if v.Len() == 0 {
124+
return nil
125+
}
126+
for i := 0; i < v.Len(); i++ {
127+
elem := v.Index(i).Interface()
128+
nestedBody, ok := elem.(map[string]interface{})
129+
if !ok {
130+
if reflect.TypeOf(elem).Kind() == reflect.Struct {
131+
nestedBody = StructToMap(elem)
132+
} else {
133+
return fmt.Errorf("element at index %d must be an object, got %T", i, elem)
134+
}
135+
}
136+
if err := Validate(nestedBody, options); err != nil {
137+
return fmt.Errorf("element at index %d: %v", i, err)
138+
}
139+
}
140+
return nil
141+
}
142+
}

validator/range_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,114 @@ func TestEach(t *testing.T) {
144144
require.EqualError(t, err, "value must be a slice or array")
145145
})
146146
}
147+
148+
func TestEachWithOptions(t *testing.T) {
149+
nestedOptions := []ValidationOption{
150+
{
151+
Key: "name",
152+
IsOptional: false,
153+
Validators: []Validator{
154+
CreateValidator(IsNotEmpty, "name is required"),
155+
CreateValidator(IsString, "value must be a string"),
156+
},
157+
},
158+
{
159+
Key: "value",
160+
IsOptional: false,
161+
Validators: []Validator{
162+
CreateValidator(IsNotEmpty, "value is required"),
163+
},
164+
},
165+
}
166+
167+
tests := []struct {
168+
name string
169+
input interface{}
170+
expected error
171+
}{
172+
{
173+
name: "valid array of maps",
174+
input: []interface{}{
175+
map[string]interface{}{"name": "color", "value": "red"},
176+
map[string]interface{}{"name": "size", "value": "M"},
177+
},
178+
expected: nil,
179+
},
180+
{
181+
name: "invalid array - missing required field",
182+
input: []interface{}{
183+
map[string]interface{}{"name": "color", "value": "red"},
184+
map[string]interface{}{"value": "M"},
185+
},
186+
expected: errors.New("element at index 1: name is required"),
187+
},
188+
{
189+
name: "invalid array - wrong type",
190+
input: []interface{}{
191+
map[string]interface{}{"name": "color", "value": "red"},
192+
map[string]interface{}{"name": 123, "value": "M"},
193+
},
194+
expected: errors.New("element at index 1: value must be a string"),
195+
},
196+
{
197+
name: "empty array",
198+
input: []interface{}{},
199+
expected: nil,
200+
},
201+
{
202+
name: "nil array",
203+
input: nil,
204+
expected: errors.New("value must be a non-nil slice or array"),
205+
},
206+
{
207+
name: "not an array",
208+
input: "not an array",
209+
expected: errors.New("value must be a slice or array, got string"),
210+
},
211+
{
212+
name: "valid array of structs",
213+
input: []interface{}{
214+
struct {
215+
Name string `json:"name"`
216+
Value string `json:"value"`
217+
}{Name: "color", Value: "red"},
218+
struct {
219+
Name string `json:"name"`
220+
Value string `json:"value"`
221+
}{Name: "size", Value: "M"},
222+
},
223+
expected: nil,
224+
},
225+
{
226+
name: "invalid element type",
227+
input: []interface{}{
228+
"not a map or struct",
229+
},
230+
expected: errors.New("element at index 0 must be an object, got string"),
231+
},
232+
{
233+
name: "json with mixed case keys",
234+
input: []interface{}{
235+
map[string]interface{}{
236+
"name": "ahmed",
237+
"Name": 123,
238+
"value": "test",
239+
},
240+
},
241+
expected: nil,
242+
},
243+
}
244+
245+
for _, tt := range tests {
246+
t.Run(tt.name, func(t *testing.T) {
247+
validator := EachWithOptions(nestedOptions)
248+
err := validator(tt.input)
249+
if tt.expected == nil {
250+
require.NoError(t, err, "expected no error")
251+
} else {
252+
require.Error(t, err, "expected an error")
253+
require.Equal(t, tt.expected.Error(), err.Error(), "error message mismatch")
254+
}
255+
})
256+
}
257+
}

validator/validator.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package validator
22

3-
import "fmt"
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
)
48

59
// ValidatorFunc is a function that validates a field and returns an error if validation fails.
610
type ValidatorFunc func(value interface{}) error
@@ -33,6 +37,11 @@ func Validate(body map[string]interface{}, options []ValidationOption) error {
3337
continue
3438
}
3539

40+
// Check if the field is required but missing *before* running validators
41+
if !option.IsOptional && !exists {
42+
return fmt.Errorf("%s is required", option.Key)
43+
}
44+
3645
// Apply transformations
3746
if exists {
3847
for _, transformer := range option.Transformers {
@@ -75,3 +84,28 @@ func CreateValidator(fn ValidatorFunc, message string) Validator {
7584
Message: message,
7685
}
7786
}
87+
88+
// StructToMap converts a struct to a map[string]interface{} for validation
89+
func StructToMap(obj interface{}) map[string]interface{} {
90+
result := make(map[string]interface{})
91+
v := reflect.ValueOf(obj)
92+
t := reflect.TypeOf(obj)
93+
94+
for i := range v.NumField() {
95+
field := t.Field(i)
96+
// Use the json tag if present, otherwise fall back to field name
97+
jsonTag := field.Tag.Get("json")
98+
key := field.Name
99+
if jsonTag != "" {
100+
// Split on "," to handle options like "omitempty"
101+
if parts := strings.Split(jsonTag, ","); len(parts) > 0 {
102+
key = parts[0]
103+
}
104+
}
105+
// Only process exported fields
106+
if field.PkgPath == "" { // PkgPath is empty for exported fields
107+
result[key] = v.Field(i).Interface()
108+
}
109+
}
110+
return result
111+
}

validator/validator_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ func TestValidate(t *testing.T) {
7878
"username": "user123",
7979
"password": "password123",
8080
},
81-
errors.New("Email is required"),
81+
errors.New("email is required"),
8282
},
8383
{
8484
"invalid email",

0 commit comments

Comments
 (0)