Passed
Pull Request — main (#55)
by Igor
04:23 queued 02:12
created

validation.validateIterableOfValidatables   A

Complexity

Conditions 4

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.0072

Importance

Changes 0
Metric Value
cc 4
eloc 15
dl 0
loc 23
ccs 12
cts 13
cp 0.9231
crap 4.0072
rs 9.65
c 0
b 0
f 0
nop 2
1
// Copyright 2021 Igor Lazarev. All rights reserved.
2
// Use of this source code is governed by a MIT-style
3
// license that can be found in the LICENSE file.
4
5
// Package validation provides tools for data validation.
6
// It is designed to create complex validation rules with abilities to hook into the validation process.
7
package validation
8
9
import (
10
	"reflect"
11
	"time"
12
13
	"github.com/muonsoft/validation/generic"
14
)
15
16
// Validatable is interface for creating validatable types on the client side.
17
// By using it you can build complex validation rules on a set of objects used in other objects.
18
//
19
// Example
20
//  type Book struct {
21
//      Title    string
22
//      Author   string
23
//      Keywords []string
24
//  }
25
//
26
//  func (b Book) Validate(validator *validation.Validator) error {
27
//      return validator.Validate(
28
//          validation.StringProperty("title", &b.Title, it.IsNotBlank()),
29
//          validation.StringProperty("author", &b.Author, it.IsNotBlank()),
30
//          validation.CountableProperty("keywords", len(b.Keywords), it.HasCountBetween(1, 10)),
31
//          validation.EachStringProperty("keywords", b.Keywords, it.IsNotBlank()),
32
//      )
33
//  }
34
type Validatable interface {
35
	Validate(validator *Validator) error
36
}
37
38
// Filter is used for processing the list of errors to return a single ViolationList.
39
// If there is at least one non-violation error it will return it instead.
40
func Filter(violations ...error) error {
41 1
	list := &ViolationList{}
42
43 1
	for _, violation := range violations {
44 1
		err := list.AppendFromError(violation)
45 1
		if err != nil {
46 1
			return err
47
		}
48
	}
49
50 1
	return list.AsError()
51
}
52
53
// ValidateByConstraintFunc is used for building validation functions for the values of specific types.
54
type ValidateByConstraintFunc func(constraint Constraint, scope Scope) error
55
56
type validateFunc func(scope Scope) (*ViolationList, error)
57
58
var validatableType = reflect.TypeOf((*Validatable)(nil)).Elem()
59
60
func newValueValidator(value interface{}, options []Option) (validateFunc, error) {
61 1
	switch v := value.(type) {
62
	case Validatable:
63 1
		return newValidValidator(v, options), nil
64
	case time.Time:
65 1
		return newTimeValidator(&v, options), nil
66
	case *time.Time:
67 1
		return newTimeValidator(v, options), nil
68
	}
69
70 1
	v := reflect.ValueOf(value)
71
72 1
	switch v.Kind() {
73
	case reflect.Ptr:
74 1
		return newValuePointerValidator(v, options)
75
	case reflect.Bool:
76 1
		b := v.Bool()
77 1
		return newBoolValidator(&b, options), nil
78
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
79
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
80
		reflect.Float32, reflect.Float64:
81 1
		n, err := generic.NewNumber(value)
82 1
		if err != nil {
83
			return nil, err
84
		}
85
86 1
		return newNumberValidator(*n, options), nil
87
	case reflect.String:
88 1
		s := v.String()
89 1
		return newStringValidator(&s, options), nil
90
	case reflect.Array, reflect.Slice, reflect.Map:
91 1
		i, err := generic.NewIterable(value)
92 1
		if err != nil {
93
			return nil, err
94
		}
95
96 1
		return newIterableValidator(i, options), nil
97
	}
98
99 1
	return nil, &NotValidatableError{Value: v}
100
}
101
102
func newValuePointerValidator(value reflect.Value, options []Option) (validateFunc, error) {
103 1
	p := value.Elem()
104 1
	if value.IsNil() {
105 1
		return newNilValidator(options), nil
106
	}
107
108 1
	switch p.Kind() {
109
	case reflect.Bool:
110 1
		b := p.Bool()
111 1
		return newBoolValidator(&b, options), nil
112
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
113
		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
114
		reflect.Float32, reflect.Float64:
115 1
		n, err := generic.NewNumber(p.Interface())
116 1
		if err != nil {
117
			return nil, err
118
		}
119
120 1
		return newNumberValidator(*n, options), nil
121
	case reflect.String:
122 1
		s := p.String()
123 1
		return newStringValidator(&s, options), nil
124
	case reflect.Array, reflect.Slice, reflect.Map:
125 1
		i, err := generic.NewIterable(p.Interface())
126 1
		if err != nil {
127
			return nil, err
128
		}
129
130 1
		return newIterableValidator(i, options), nil
131
	}
132
133
	return nil, &NotValidatableError{Value: value}
134
}
135
136
func newNilValidator(options []Option) validateFunc {
137 1
	return newValidator(options, func(constraint Constraint, scope Scope) error {
138 1
		if constraintValidator, ok := constraint.(NilConstraint); ok {
139 1
			return constraintValidator.ValidateNil(scope)
140
		}
141
142
		return nil
143
	})
144
}
145
146
func newBoolValidator(value *bool, options []Option) validateFunc {
147 1
	return newValidator(options, func(constraint Constraint, scope Scope) error {
148 1
		if c, ok := constraint.(BoolConstraint); ok {
149 1
			return c.ValidateBool(value, scope)
150
		}
151
152 1
		return NewInapplicableConstraintError(constraint, "bool")
153
	})
154
}
155
156
func newNumberValidator(value generic.Number, options []Option) validateFunc {
157 1
	return newValidator(options, func(constraint Constraint, scope Scope) error {
158 1
		if c, ok := constraint.(NumberConstraint); ok {
159 1
			return c.ValidateNumber(value, scope)
160
		}
161
162 1
		return NewInapplicableConstraintError(constraint, "number")
163
	})
164
}
165
166
func newStringValidator(value *string, options []Option) validateFunc {
167 1
	return newValidator(options, func(constraint Constraint, scope Scope) error {
168 1
		if c, ok := constraint.(StringConstraint); ok {
169 1
			return c.ValidateString(value, scope)
170
		}
171
172 1
		return NewInapplicableConstraintError(constraint, "string")
173
	})
174
}
175
176
func newStringsValidator(values []string, options []Option) validateFunc {
177 1
	return newValidator(options, func(constraint Constraint, scope Scope) error {
178 1
		if c, ok := constraint.(StringsConstraint); ok {
179 1
			return c.ValidateStrings(values, scope)
180
		}
181
182
		return NewInapplicableConstraintError(constraint, "strings")
183
	})
184
}
185
186
func newIterableValidator(iterable generic.Iterable, options []Option) validateFunc {
187 1
	return func(scope Scope) (*ViolationList, error) {
188 1
		err := scope.applyOptions(options...)
189 1
		if err != nil {
190
			return nil, err
191
		}
192
193 1
		violations, err := validateOnScope(scope, options, func(constraint Constraint, scope Scope) error {
194 1
			if c, ok := constraint.(IterableConstraint); ok {
195 1
				return c.ValidateIterable(iterable, scope)
196
			}
197
198 1
			return NewInapplicableConstraintError(constraint, "iterable")
199
		})
200 1
		if err != nil {
201 1
			return nil, err
202
		}
203
204 1
		if iterable.IsElementImplements(validatableType) {
205 1
			vs, err := validateIterableOfValidatables(scope, iterable)
206 1
			if err != nil {
207
				return nil, err
208
			}
209 1
			violations.Join(vs)
210
		}
211
212 1
		return violations, nil
213
	}
214
}
215
216
func newCountableValidator(count int, options []Option) validateFunc {
217 1
	return newValidator(options, func(constraint Constraint, scope Scope) error {
218 1
		if c, ok := constraint.(CountableConstraint); ok {
219 1
			return c.ValidateCountable(count, scope)
220
		}
221
222 1
		return NewInapplicableConstraintError(constraint, "countable")
223
	})
224
}
225
226
func newTimeValidator(value *time.Time, options []Option) validateFunc {
227 1
	return newValidator(options, func(constraint Constraint, scope Scope) error {
228 1
		if c, ok := constraint.(TimeConstraint); ok {
229 1
			return c.ValidateTime(value, scope)
230
		}
231
232 1
		return NewInapplicableConstraintError(constraint, "time")
233
	})
234
}
235
236
func newEachValidator(iterable generic.Iterable, options []Option) validateFunc {
237 1
	return func(scope Scope) (*ViolationList, error) {
238 1
		violations := &ViolationList{}
239
240 1
		err := iterable.Iterate(func(key generic.Key, value interface{}) error {
241 1
			opts := options
242 1
			if key.IsIndex() {
243 1
				opts = append(opts, ArrayIndex(key.Index()))
244
			} else {
245 1
				opts = append(opts, PropertyName(key.String()))
246
			}
247
248 1
			validate, err := newValueValidator(value, opts)
249 1
			if err != nil {
250
				return err
251
			}
252
253 1
			vs, err := validate(scope)
254 1
			if err != nil {
255
				return err
256
			}
257 1
			violations.Join(vs)
258
259 1
			return nil
260
		})
261
262 1
		return violations, err
263
	}
264
}
265
266
func newEachStringValidator(values []string, options []Option) validateFunc {
267 1
	return func(scope Scope) (*ViolationList, error) {
268 1
		violations := &ViolationList{}
269
270 1
		for i := range values {
271 1
			opts := append(options, ArrayIndex(i))
272 1
			validate := newStringValidator(&values[i], opts)
273 1
			vs, err := validate(scope)
274 1
			if err != nil {
275
				return nil, err
276
			}
277 1
			violations.Join(vs)
278
		}
279
280 1
		return violations, nil
281
	}
282
}
283
284
func newValidValidator(value Validatable, options []Option) validateFunc {
285 1
	return func(scope Scope) (*ViolationList, error) {
286 1
		err := scope.applyOptions(options...)
287 1
		if err != nil {
288
			return nil, err
289
		}
290
291 1
		err = value.Validate(newScopedValidator(scope))
292 1
		violations, ok := UnwrapViolationList(err)
293 1
		if ok {
294 1
			return violations, nil
295
		}
296
297 1
		return nil, err
298
	}
299
}
300
301
func newValidator(options []Option, validate ValidateByConstraintFunc) validateFunc {
302 1
	return func(scope Scope) (*ViolationList, error) {
303 1
		err := scope.applyOptions(options...)
304 1
		if err != nil {
305 1
			return nil, err
306
		}
307
308 1
		return validateOnScope(scope, options, validate)
309
	}
310
}
311
312
func validateOnScope(scope Scope, options []Option, validate ValidateByConstraintFunc) (*ViolationList, error) {
313 1
	violations := &ViolationList{}
314
315 1
	for _, option := range options {
316 1
		if constraint, ok := option.(controlConstraint); ok {
317 1
			vs, err := constraint.validate(scope, validate)
318 1
			if err != nil {
319
				return nil, err
320
			}
321
322 1
			violations.Join(vs)
323 1
		} else if constraint, ok := option.(Constraint); ok {
324 1
			err := violations.AppendFromError(validate(constraint, scope))
325 1
			if err != nil {
326 1
				return nil, err
327
			}
328
		}
329
	}
330
331 1
	return violations, nil
332
}
333
334
func validateIterableOfValidatables(scope Scope, iterable generic.Iterable) (*ViolationList, error) {
335 1
	violations := &ViolationList{}
336
337 1
	err := iterable.Iterate(func(key generic.Key, value interface{}) error {
338 1
		s := scope
339 1
		if key.IsIndex() {
340 1
			s = s.atIndex(key.Index())
341
		} else {
342 1
			s = s.atProperty(key.String())
343
		}
344
345 1
		validate := newValidValidator(value.(Validatable), nil)
346 1
		vs, err := validate(s)
347 1
		if err != nil {
348
			return err
349
		}
350
351 1
		violations.Join(vs)
352
353 1
		return nil
354
	})
355
356 1
	return violations, err
357
}
358