Test Failed
Pull Request — main (#71)
by Igor
02:01
created

it/comparison.go   F

Size/Duplication

Total Lines 830
Duplicated Lines 0 %

Test Coverage

Coverage 97.53%

Importance

Changes 0
Metric Value
cc 130
eloc 436
dl 0
loc 830
ccs 158
cts 162
cp 0.9753
crap 130.2546
rs 2
c 0
b 0
f 0
1
package it
2
3
import (
4
	"fmt"
5
	"reflect"
6
	"strconv"
7
	"time"
8
9
	"github.com/muonsoft/validation"
10
	"github.com/muonsoft/validation/code"
11
	"github.com/muonsoft/validation/is"
12
	"github.com/muonsoft/validation/message"
13
)
14
15
// NumberComparisonConstraint is used for various numeric comparisons between integer and float values.
16
type NumberComparisonConstraint[T validation.Numeric] struct {
17
	isIgnored         bool
18
	value             T
19
	groups            []string
20
	code              string
21
	messageTemplate   string
22
	messageParameters validation.TemplateParameterList
23
	comparedValue     string
24
	isValid           func(value T) bool
25
}
26
27
// IsEqualToNumber checks that the number is equal to the specified value.
28
func IsEqualToNumber[T validation.Numeric](value T) NumberComparisonConstraint[T] {
29
	return NumberComparisonConstraint[T]{
30
		code:            code.Equal,
31 1
		value:           value,
32
		messageTemplate: message.Templates[code.Equal],
33 1
		comparedValue:   fmt.Sprint(value),
34
		isValid: func(n T) bool {
35
			return n == value
36
		},
37
	}
38 1
}
39
40
// IsNotEqualToNumber checks that the number is not equal to the specified value.
41
func IsNotEqualToNumber[T validation.Numeric](value T) NumberComparisonConstraint[T] {
42
	return NumberComparisonConstraint[T]{
43
		code:            code.NotEqual,
44
		value:           value,
45
		messageTemplate: message.Templates[code.NotEqual],
46
		comparedValue:   fmt.Sprint(value),
47 1
		isValid: func(n T) bool {
48
			return n != value
49 1
		},
50
	}
51
}
52
53
// IsLessThan checks that the number is less than the specified value.
54 1
func IsLessThan[T validation.Numeric](value T) NumberComparisonConstraint[T] {
55
	return NumberComparisonConstraint[T]{
56
		code:            code.TooHigh,
57
		value:           value,
58
		messageTemplate: message.Templates[code.TooHigh],
59
		comparedValue:   fmt.Sprint(value),
60
		isValid: func(n T) bool {
61
			return n < value
62
		},
63 1
	}
64
}
65 1
66
// IsLessThanOrEqual checks that the number is less than or equal to the specified value.
67
func IsLessThanOrEqual[T validation.Numeric](value T) NumberComparisonConstraint[T] {
68
	return NumberComparisonConstraint[T]{
69
		code:            code.TooHighOrEqual,
70 1
		value:           value,
71
		messageTemplate: message.Templates[code.TooHighOrEqual],
72
		comparedValue:   fmt.Sprint(value),
73
		isValid: func(n T) bool {
74
			return n <= value
75
		},
76
	}
77
}
78
79 1
// IsGreaterThan checks that the number is greater than the specified value.
80
func IsGreaterThan[T validation.Numeric](value T) NumberComparisonConstraint[T] {
81 1
	return NumberComparisonConstraint[T]{
82
		code:            code.TooLow,
83
		value:           value,
84
		messageTemplate: message.Templates[code.TooLow],
85
		comparedValue:   fmt.Sprint(value),
86 1
		isValid: func(n T) bool {
87
			return n > value
88
		},
89
	}
90
}
91
92
// IsGreaterThanOrEqual checks that the number is greater than or equal to the specified value.
93
func IsGreaterThanOrEqual[T validation.Numeric](value T) NumberComparisonConstraint[T] {
94
	return NumberComparisonConstraint[T]{
95 1
		code:            code.TooLowOrEqual,
96
		value:           value,
97 1
		messageTemplate: message.Templates[code.TooLowOrEqual],
98
		comparedValue:   fmt.Sprint(value),
99
		isValid: func(n T) bool {
100
			return n >= value
101
		},
102 1
	}
103
}
104
105
// IsPositive checks that the value is a positive number. Zero is neither positive nor negative.
106
// If you want to allow zero use IsPositiveOrZero comparison.
107
func IsPositive[T validation.Numeric]() NumberComparisonConstraint[T] {
108
	return NumberComparisonConstraint[T]{
109
		code:            code.NotPositive,
110
		value:           0,
111 1
		messageTemplate: message.Templates[code.NotPositive],
112
		comparedValue:   "0",
113 1
		isValid: func(n T) bool {
114
			return n > 0
115
		},
116
	}
117
}
118 1
119
// IsPositiveOrZero checks that the value is a positive number or equal to zero.
120
// If you don't want to allow zero as a valid value, use IsPositive comparison.
121
func IsPositiveOrZero[T validation.Numeric]() NumberComparisonConstraint[T] {
122
	return NumberComparisonConstraint[T]{
123
		code:            code.NotPositiveOrZero,
124
		value:           0,
125
		messageTemplate: message.Templates[code.NotPositiveOrZero],
126
		comparedValue:   "0",
127 1
		isValid: func(n T) bool {
128
			return n >= 0
129 1
		},
130
	}
131
}
132
133
// IsNegative checks that the value is a negative number. Zero is neither positive nor negative.
134 1
// If you want to allow zero use IsNegativeOrZero comparison.
135
func IsNegative[T validation.Numeric]() NumberComparisonConstraint[T] {
136
	return NumberComparisonConstraint[T]{
137
		code:            code.NotNegative,
138
		value:           0,
139
		messageTemplate: message.Templates[code.NotNegative],
140
		comparedValue:   "0",
141
		isValid: func(n T) bool {
142
			return n < 0
143 1
		},
144
	}
145 1
}
146
147
// IsNegativeOrZero checks that the value is a negative number or equal to zero.
148
// If you don't want to allow zero as a valid value, use IsNegative comparison.
149
func IsNegativeOrZero[T validation.Numeric]() NumberComparisonConstraint[T] {
150 1
	return NumberComparisonConstraint[T]{
151
		code:            code.NotNegativeOrZero,
152
		value:           0,
153
		messageTemplate: message.Templates[code.NotNegativeOrZero],
154
		comparedValue:   "0",
155
		isValid: func(n T) bool {
156
			return n <= 0
157
		},
158
	}
159 1
}
160
161 1
// Code overrides default code for produced violation.
162
func (c NumberComparisonConstraint[T]) Code(code string) NumberComparisonConstraint[T] {
163
	c.code = code
164
	return c
165
}
166 1
167
// Message sets the violation message template. You can set custom template parameters
168
// for injecting its values into the final message. Also, you can use default parameters:
169
//
170
//  {{ comparedValue }} - the expected value;
171
//  {{ value }} - the current (invalid) value.
172
func (c NumberComparisonConstraint[T]) Message(
173
	template string,
174
	parameters ...validation.TemplateParameter,
175 1
) NumberComparisonConstraint[T] {
176
	c.messageTemplate = template
177 1
	c.messageParameters = parameters
178
	return c
179
}
180
181
// When enables conditional validation of this constraint. If the expression evaluates to false,
182 1
// then the constraint will be ignored.
183
func (c NumberComparisonConstraint[T]) When(condition bool) NumberComparisonConstraint[T] {
184
	c.isIgnored = !condition
185
	return c
186
}
187
188
// WhenGroups enables conditional validation of the constraint by using the validation groups.
189
func (c NumberComparisonConstraint[T]) WhenGroups(groups ...string) NumberComparisonConstraint[T] {
190
	c.groups = groups
191 1
	return c
192
}
193 1
194
func (c NumberComparisonConstraint[T]) ValidateNumber(value *T, scope validation.Scope) error {
195
	if c.isIgnored || scope.IsIgnored(c.groups...) || value == nil || c.isValid(*value) {
196
		return nil
197
	}
198 1
199
	return scope.BuildViolation(c.code, c.messageTemplate).
200
		SetParameters(
201
			c.messageParameters.Prepend(
202
				validation.TemplateParameter{Key: "{{ comparedValue }}", Value: c.comparedValue},
203
				validation.TemplateParameter{Key: "{{ value }}", Value: fmt.Sprint(*value)},
204
			)...,
205
		).
206
		CreateViolation()
207 1
}
208
209 1
// RangeConstraint is used to check that a given number value is between some minimum and maximum.
210
type RangeConstraint[T validation.Numeric] struct {
211
	isIgnored         bool
212
	groups            []string
213
	code              string
214 1
	messageTemplate   string
215
	messageParameters validation.TemplateParameterList
216
	min               T
217
	max               T
218
}
219
220
// IsBetween checks that the number is between specified minimum and maximum numeric values.
221
func IsBetween[T validation.Numeric](min, max T) RangeConstraint[T] {
222 1
	return RangeConstraint[T]{
223
		min:             min,
224 1
		max:             max,
225
		code:            code.NotInRange,
226
		messageTemplate: message.Templates[code.NotInRange],
227
	}
228
}
229 1
230
// Name is the constraint name.
231
func (c RangeConstraint[T]) Name() string {
232
	return fmt.Sprintf("RangeConstraint[%s]", reflect.TypeOf(c.min).String())
233
}
234
235
// Code overrides default code for produced violation.
236
func (c RangeConstraint[T]) Code(code string) RangeConstraint[T] {
237 1
	c.code = code
238
	return c
239 1
}
240
241
// Message sets the violation message template. You can set custom template parameters
242
// for injecting its values into the final message. Also, you can use default parameters:
243
//
244 1
//  {{ max }} - the upper limit;
245
//  {{ min }} - the lower limit;
246
//  {{ value }} - the current (invalid) value.
247
func (c RangeConstraint[T]) Message(template string, parameters ...validation.TemplateParameter) RangeConstraint[T] {
248
	c.messageTemplate = template
249
	c.messageParameters = parameters
250
	return c
251
}
252 1
253
// When enables conditional validation of this constraint. If the expression evaluates to false,
254 1
// then the constraint will be ignored.
255
func (c RangeConstraint[T]) When(condition bool) RangeConstraint[T] {
256
	c.isIgnored = !condition
257
	return c
258
}
259 1
260
// WhenGroups enables conditional validation of the constraint by using the validation groups.
261
func (c RangeConstraint[T]) WhenGroups(groups ...string) RangeConstraint[T] {
262
	c.groups = groups
263
	return c
264
}
265
266
func (c RangeConstraint[T]) ValidateNumber(value *T, scope validation.Scope) error {
267 1
	if c.min >= c.max {
268
		return scope.NewConstraintError(c.Name(), "invalid range")
269 1
	}
270
	if c.isIgnored || value == nil || scope.IsIgnored(c.groups...) {
271
		return nil
272
	}
273
	if *value < c.min || *value > c.max {
274 1
		return c.newViolation(*value, scope)
275
	}
276
277
	return nil
278
}
279
280
func (c RangeConstraint[T]) newViolation(value T, scope validation.Scope) error {
281 1
	return scope.BuildViolation(c.code, c.messageTemplate).
282
		SetParameters(
283
			c.messageParameters.Prepend(
284
				validation.TemplateParameter{Key: "{{ min }}", Value: fmt.Sprint(c.min)},
285
				validation.TemplateParameter{Key: "{{ max }}", Value: fmt.Sprint(c.max)},
286
				validation.TemplateParameter{Key: "{{ value }}", Value: fmt.Sprint(value)},
287
			)...,
288
		).
289
		CreateViolation()
290
}
291 1
292 1
// StringComparisonConstraint is used to compare strings.
293
type StringComparisonConstraint struct {
294
	isIgnored         bool
295
	groups            []string
296
	code              string
297
	messageTemplate   string
298
	messageParameters validation.TemplateParameterList
299
	comparedValue     string
300
	isValid           func(value string) bool
301
}
302
303
// IsEqualToString checks that the string value is equal to the specified string value.
304 1
func IsEqualToString(value string) StringComparisonConstraint {
305 1
	return StringComparisonConstraint{
306 1
		code:            code.Equal,
307
		messageTemplate: message.Templates[code.Equal],
308
		comparedValue:   value,
309
		isValid: func(actualValue string) bool {
310
			return value == actualValue
311
		},
312 1
	}
313 1
}
314
315
// IsNotEqualToString checks that the string value is not equal to the specified string value.
316
func IsNotEqualToString(value string) StringComparisonConstraint {
317
	return StringComparisonConstraint{
318 1
		code:            code.NotEqual,
319 1
		messageTemplate: message.Templates[code.NotEqual],
320
		comparedValue:   value,
321
		isValid: func(actualValue string) bool {
322
			return value != actualValue
323 1
		},
324 1
	}
325
}
326
327 1
// Code overrides default code for produced violation.
328
func (c StringComparisonConstraint) Code(code string) StringComparisonConstraint {
329
	c.code = code
330
	return c
331
}
332
333
// Message sets the violation message template. You can set custom template parameters
334
// for injecting its values into the final message. Also, you can use default parameters:
335
//
336
//  {{ comparedValue }} - the expected value;
337
//  {{ value }} - the current (invalid) value.
338
//
339
// All string values are quoted strings.
340
func (c StringComparisonConstraint) Message(
341
	template string,
342
	parameters ...validation.TemplateParameter,
343
) StringComparisonConstraint {
344
	c.messageTemplate = template
345
	c.messageParameters = parameters
346
	return c
347
}
348
349
// When enables conditional validation of this constraint. If the expression evaluates to false,
350
// then the constraint will be ignored.
351
func (c StringComparisonConstraint) When(condition bool) StringComparisonConstraint {
352
	c.isIgnored = !condition
353
	return c
354 1
}
355
356
// WhenGroups enables conditional validation of the constraint by using the validation groups.
357
func (c StringComparisonConstraint) WhenGroups(groups ...string) StringComparisonConstraint {
358
	c.groups = groups
359
	return c
360
}
361
362
func (c StringComparisonConstraint) ValidateString(value *string, scope validation.Scope) error {
363
	if c.isIgnored || scope.IsIgnored(c.groups...) || value == nil || c.isValid(*value) {
364
		return nil
365
	}
366 1
367
	return scope.BuildViolation(c.code, c.messageTemplate).
368
		SetParameters(
369
			c.messageParameters.Prepend(
370
				validation.TemplateParameter{Key: "{{ comparedValue }}", Value: strconv.Quote(c.comparedValue)},
371
				validation.TemplateParameter{Key: "{{ value }}", Value: strconv.Quote(*value)},
372
			)...,
373
		).
374
		CreateViolation()
375
}
376 1
377 1
// TimeComparisonConstraint is used to compare time values.
378
type TimeComparisonConstraint struct {
379
	isIgnored         bool
380 1
	groups            []string
381
	code              string
382
	messageTemplate   string
383
	messageParameters validation.TemplateParameterList
384
	comparedValue     time.Time
385 1
	layout            string
386
	isValid           func(value time.Time) bool
387
}
388
389
// IsEarlierThan checks that the given time is earlier than the specified value.
390 1
func IsEarlierThan(value time.Time) TimeComparisonConstraint {
391 1
	return TimeComparisonConstraint{
392
		code:            code.TooLate,
393
		messageTemplate: message.Templates[code.TooLate],
394
		comparedValue:   value,
395
		layout:          time.RFC3339,
396
		isValid: func(actualValue time.Time) bool {
397
			return actualValue.Before(value)
398
		},
399
	}
400
}
401 1
402 1
// IsEarlierThanOrEqual checks that the given time is earlier or equal to the specified value.
403 1
func IsEarlierThanOrEqual(value time.Time) TimeComparisonConstraint {
404
	return TimeComparisonConstraint{
405
		code:            code.TooLateOrEqual,
406
		messageTemplate: message.Templates[code.TooLateOrEqual],
407
		comparedValue:   value,
408
		layout:          time.RFC3339,
409 1
		isValid: func(actualValue time.Time) bool {
410 1
			return actualValue.Before(value) || actualValue.Equal(value)
411
		},
412
	}
413
}
414
415 1
// IsLaterThan checks that the given time is later than the specified value.
416 1
func IsLaterThan(value time.Time) TimeComparisonConstraint {
417
	return TimeComparisonConstraint{
418
		code:            code.TooEarly,
419
		messageTemplate: message.Templates[code.TooEarly],
420 1
		comparedValue:   value,
421 1
		layout:          time.RFC3339,
422
		isValid: func(actualValue time.Time) bool {
423 1
			return actualValue.After(value)
424 1
		},
425
	}
426
}
427 1
428
// IsLaterThanOrEqual checks that the given time is later or equal to the specified value.
429
func IsLaterThanOrEqual(value time.Time) TimeComparisonConstraint {
430
	return TimeComparisonConstraint{
431 1
		code:            code.TooEarlyOrEqual,
432
		messageTemplate: message.Templates[code.TooEarlyOrEqual],
433
		comparedValue:   value,
434
		layout:          time.RFC3339,
435
		isValid: func(actualValue time.Time) bool {
436
			return actualValue.After(value) || actualValue.Equal(value)
437
		},
438
	}
439
}
440
441
// Code overrides default code for produced violation.
442
func (c TimeComparisonConstraint) Code(code string) TimeComparisonConstraint {
443
	c.code = code
444
	return c
445
}
446
447
// Message sets the violation message template. You can set custom template parameters
448
// for injecting its values into the final message. Also, you can use default parameters:
449
//
450
//  {{ comparedValue }} - the expected value;
451
//  {{ value }} - the current (invalid) value.
452
//
453
// All values are formatted by the layout that can be defined by the Layout method.
454
// Default layout is time.RFC3339.
455 1
func (c TimeComparisonConstraint) Message(
456
	template string,
457
	parameters ...validation.TemplateParameter,
458
) TimeComparisonConstraint {
459
	c.messageTemplate = template
460 1
	c.messageParameters = parameters
461
	return c
462
}
463
464
// Layout can be used to set the layout that is used to format time values.
465
func (c TimeComparisonConstraint) Layout(layout string) TimeComparisonConstraint {
466
	c.layout = layout
467 1
	return c
468
}
469
470
// When enables conditional validation of this constraint. If the expression evaluates to false,
471
// then the constraint will be ignored.
472 1
func (c TimeComparisonConstraint) When(condition bool) TimeComparisonConstraint {
473
	c.isIgnored = !condition
474
	return c
475
}
476
477
// WhenGroups enables conditional validation of the constraint by using the validation groups.
478
func (c TimeComparisonConstraint) WhenGroups(groups ...string) TimeComparisonConstraint {
479 1
	c.groups = groups
480
	return c
481
}
482
483
func (c TimeComparisonConstraint) ValidateTime(value *time.Time, scope validation.Scope) error {
484
	if c.isIgnored || scope.IsIgnored(c.groups...) || value == nil || c.isValid(*value) {
485
		return nil
486
	}
487
488
	return scope.BuildViolation(c.code, c.messageTemplate).
489 1
		SetParameters(
490 1
			c.messageParameters.Prepend(
491
				validation.TemplateParameter{Key: "{{ comparedValue }}", Value: c.comparedValue.Format(c.layout)},
492
				validation.TemplateParameter{Key: "{{ value }}", Value: value.Format(c.layout)},
493
			)...,
494
		).
495
		CreateViolation()
496
}
497
498
// TimeRangeConstraint is used to check that a given time value is between some minimum and maximum.
499
type TimeRangeConstraint struct {
500
	isIgnored         bool
501
	groups            []string
502
	code              string
503
	messageTemplate   string
504 1
	messageParameters validation.TemplateParameterList
505 1
	layout            string
506 1
	min               time.Time
507
	max               time.Time
508
}
509
510
// IsBetweenTime checks that the time is between specified minimum and maximum time values.
511
func IsBetweenTime(min, max time.Time) TimeRangeConstraint {
512 1
	return TimeRangeConstraint{
513 1
		code:            code.NotInRange,
514
		messageTemplate: message.Templates[code.NotInRange],
515
		layout:          time.RFC3339,
516
		min:             min,
517
		max:             max,
518 1
	}
519 1
}
520
521
// Code overrides default code for produced violation.
522
func (c TimeRangeConstraint) Code(code string) TimeRangeConstraint {
523 1
	c.code = code
524 1
	return c
525
}
526
527 1
// Message sets the violation message template. You can set custom template parameters
528
// for injecting its values into the final message. Also, you can use default parameters:
529
//
530
//  {{ max }} - the upper limit;
531
//  {{ min }} - the lower limit;
532
//  {{ value }} - the current (invalid) value.
533
//
534
// All values are formatted by the layout that can be defined by the Layout method.
535
// Default layout is time.RFC3339.
536
func (c TimeRangeConstraint) Message(template string, parameters ...validation.TemplateParameter) TimeRangeConstraint {
537
	c.messageTemplate = template
538
	c.messageParameters = parameters
539
	return c
540
}
541
542
// When enables conditional validation of this constraint. If the expression evaluates to false,
543
// then the constraint will be ignored.
544
func (c TimeRangeConstraint) When(condition bool) TimeRangeConstraint {
545
	c.isIgnored = !condition
546
	return c
547
}
548
549
// WhenGroups enables conditional validation of the constraint by using the validation groups.
550
func (c TimeRangeConstraint) WhenGroups(groups ...string) TimeRangeConstraint {
551 1
	c.groups = groups
552
	return c
553
}
554
555
// Layout can be used to set the layout that is used to format time values.
556
func (c TimeRangeConstraint) Layout(layout string) TimeRangeConstraint {
557 1
	c.layout = layout
558
	return c
559
}
560
561
func (c TimeRangeConstraint) ValidateTime(value *time.Time, scope validation.Scope) error {
562
	if c.min.After(c.max) || c.min.Equal(c.max) {
563
		return scope.NewConstraintError("TimeRangeConstraint", "invalid range")
564 1
	}
565
	if c.isIgnored || scope.IsIgnored(c.groups...) || value == nil {
566
		return nil
567
	}
568
	if value.Before(c.min) || value.After(c.max) {
569
		return c.newViolation(value, scope)
570 1
	}
571
572
	return nil
573
}
574
575
func (c TimeRangeConstraint) newViolation(value *time.Time, scope validation.Scope) validation.Violation {
576
	return scope.BuildViolation(c.code, c.messageTemplate).
577 1
		SetParameters(
578
			c.messageParameters.Prepend(
579
				validation.TemplateParameter{Key: "{{ min }}", Value: c.min.Format(c.layout)},
580
				validation.TemplateParameter{Key: "{{ max }}", Value: c.max.Format(c.layout)},
581
				validation.TemplateParameter{Key: "{{ value }}", Value: value.Format(c.layout)},
582
			)...,
583 1
		).
584
		CreateViolation()
585
}
586
587
// UniqueConstraint is used to check that all elements of the given collection are unique.
588
type UniqueConstraint[T comparable] struct {
589
	isIgnored         bool
590 1
	groups            []string
591
	code              string
592
	messageTemplate   string
593
	messageParameters validation.TemplateParameterList
594
}
595
596 1
// HasUniqueValues checks that all elements of the given collection are unique
597
// (none of them is present more than once).
598
func HasUniqueValues[T comparable]() UniqueConstraint[T] {
599
	return UniqueConstraint[T]{
600
		code:            code.NotUnique,
601
		messageTemplate: message.Templates[code.NotUnique],
602
	}
603 1
}
604
605
// Code overrides default code for produced violation.
606
func (c UniqueConstraint[T]) Code(code string) UniqueConstraint[T] {
607
	c.code = code
608
	return c
609
}
610
611
// Message sets the violation message template. You can set custom template parameters
612
// for injecting its values into the final message.
613 1
func (c UniqueConstraint[T]) Message(template string, parameters ...validation.TemplateParameter) UniqueConstraint[T] {
614 1
	c.messageTemplate = template
615
	c.messageParameters = parameters
616
	return c
617
}
618
619
// When enables conditional validation of this constraint. If the expression evaluates to false,
620
// then the constraint will be ignored.
621
func (c UniqueConstraint[T]) When(condition bool) UniqueConstraint[T] {
622
	c.isIgnored = !condition
623
	return c
624
}
625
626
// WhenGroups enables conditional validation of the constraint by using the validation groups.
627
func (c UniqueConstraint[T]) WhenGroups(groups ...string) UniqueConstraint[T] {
628
	c.groups = groups
629 1
	return c
630 1
}
631 1
632
func (c UniqueConstraint[T]) ValidateComparables(values []T, scope validation.Scope) error {
633
	if c.isIgnored || scope.IsIgnored(c.groups...) || is.Unique(values) {
634
		return nil
635
	}
636 1
637 1
	return scope.BuildViolation(c.code, c.messageTemplate).
638
		SetParameters(c.messageParameters...).
639
		CreateViolation()
640
}
641