1 /++
2     Validation constraints
3 
4     Implemented as UDAs.
5 
6     $(B “Batteries included!”)
7     This module provides a set of constraints
8     for the most common use cases.
9 
10 
11     ## Implementing your own constraints
12 
13     To implement a custom constraint,
14     create a new `struct` and tag it with @[constraint].
15 
16     Implement a member function `check()` that accepts a parameter of the type to validate.
17     Utilize templates or overloads to implement checks for different types.
18 
19     Implement an `errorMessage` member:
20     Either a (property) function with return type `string`
21     or a field `string`.
22  +/
23 module oceandrift.validation.constraints;
24 
25 import std.conv : to;
26 import std.traits : getSymbolsByUDA, hasUDA;
27 
28 @safe:
29 
30 // TODO: rewrite, once current preview “shortenedMethods” are enabled by default
31 
32 /++
33     UDA to mark constraint definitions
34  +/
35 struct constraint
36 {
37 }
38 
39 /++
40     Inverts a constraint
41  +/
42 @constraint struct not(OtherConstraint)
43 {
44     OtherConstraint otherConstraint;
45 
46     bool check(T)(T actual)
47     {
48         return (!otherConstraint.check(actual));
49     }
50 
51     string errorMessage()
52     {
53         // FIXME
54         static assert(
55             __traits(compiles, () { string s = otherConstraint.errorMessage; }),
56             "`@not` cannot generate an error message for faulty constraint `@"
57                 ~ O.stringof
58                 ~ '`'
59         );
60         return "must *not* comply with: " ~ otherConstraint.errorMessage;
61     }
62 }
63 
64 /// ditto
65 @constraint struct not(alias otherConstraint)
66 {
67     bool check(T)(T actual)
68     {
69         return (!otherConstraint.check(actual));
70     }
71 
72     string errorMessage()
73     {
74         // FIXME
75         static assert(
76             __traits(compiles, () { string s = otherConstraint.errorMessage; }),
77             "`@not` cannot generate an error message for faulty constraint `@"
78                 ~ O.stringof
79                 ~ '`'
80         );
81         return "must *not* comply with: " ~ otherConstraint.errorMessage;
82     }
83 }
84 
85 ///
86 unittest
87 {
88     struct Data
89     {
90         @not!notNaN // <-- must be NaN
91         float value = float.nan;
92     }
93 }
94 
95 ///
96 unittest
97 {
98     struct Data
99     {
100         @not!notNegative // <-- must *not* be non-negative
101         int value;
102     }
103 }
104 
105 /++
106     Value must not be NULL
107  +/
108 @constraint struct notNull
109 {
110     bool check(T)(T actual)
111     {
112         return (actual !is null);
113     }
114 
115     static immutable string errorMessage = "must not be NULL";
116 }
117 
118 ///
119 unittest
120 {
121     assert(!test!notNull(null));
122     assert(test!notNull(""));
123 
124     auto o = new Object();
125     assert(test!notNull(o));
126     Object o2 = null;
127     assert(!test!notNull(o2));
128 
129     int* i = null;
130     assert(!test!notNull(i));
131 
132     int* i2 = new int(10);
133     assert(test!notNull(i2));
134 }
135 
136 // string
137 
138 /++
139     Value must be longer than or as long as n units
140  +/
141 @constraint struct minLength
142 {
143     size_t n;
144 
145     bool check(T)(T actual)
146     {
147         return (actual.length >= this.n);
148     }
149 
150     string errorMessage()
151     {
152         return "length must be >= " ~ this.n.to!string;
153     }
154 }
155 
156 ///
157 unittest
158 {
159     assert(test!(minLength(2))("12"));
160     assert(test!(minLength(2))("123"));
161     assert(!test!(minLength(2))("1"));
162 
163     assert(test!(minLength(4))("1234"));
164     assert(!test!(minLength(4))("123"));
165 
166     assert(test!(minLength(1))("1"));
167     assert(!test!(minLength(1))(""));
168 
169     assert(test!(minLength(0))("1"));
170     assert(test!(minLength(0))(""));
171 
172     assert(test!(minLength(2))([99, 98, 97]));
173     assert(!test!(minLength(2))([1]));
174 }
175 
176 /++
177     Value must be shorter than or as long as N units
178  +/
179 @constraint struct maxLength
180 {
181     size_t n;
182 
183     bool check(T)(T actual)
184     {
185         return (actual.length <= this.n);
186     }
187 
188     string errorMessage()
189     {
190         return "length must be <= " ~ this.n.to!string;
191     }
192 }
193 
194 ///
195 unittest
196 {
197     assert(test!(maxLength(2))("1"));
198     assert(test!(maxLength(2))("12"));
199     assert(!test!(maxLength(2))("123"));
200 
201     assert(test!(maxLength(4))("1234"));
202     assert(!test!(maxLength(4))("12345"));
203 
204     assert(test!(maxLength(1))("1"));
205     assert(!test!(maxLength(1))("12"));
206 
207     assert(test!(maxLength(0))(""));
208     assert(!test!(maxLength(0))("1"));
209 
210     immutable string null_ = null;
211     assert(test!(maxLength(0))(null_));
212 }
213 
214 /++
215     Value must be exactly N units long
216  +/
217 @constraint struct exactLength
218 {
219     size_t n;
220 
221     bool check(T)(T actual)
222     {
223         return (actual.length == this.n);
224     }
225 
226     string errorMessage()
227     {
228         return "length must be exactly " ~ this.n.to!string;
229     }
230 }
231 
232 ///
233 unittest
234 {
235     assert(test!(exactLength(2))("12"));
236     assert(!test!(exactLength(2))("1"));
237     assert(!test!(exactLength(2))("123"));
238 
239     assert(test!(exactLength(4))("1234"));
240     assert(!test!(exactLength(4))("12345"));
241 
242     assert(test!(exactLength(1))("1"));
243     assert(!test!(exactLength(1))("12"));
244 
245     assert(test!(exactLength(0))(""));
246     assert(!test!(exactLength(0))("1"));
247 
248     immutable string null_ = null;
249     assert(test!(exactLength(0))(null_));
250 }
251 
252 /++
253     Valid Unicode (UTF-8, UTF-16 or UTF-32) constraint
254  +/
255 @constraint struct isUnicode
256 {
257     bool check(T)(T s)
258     {
259         import std.utf : validate;
260 
261         try
262             validate(s);
263         catch (Exception)
264             return false;
265 
266         return true;
267     }
268 
269     static immutable string errorMessage = "is not well-formed Unicode";
270 }
271 
272 ///
273 unittest
274 {
275     assert(test!isUnicode("abcdefghijklmnopqrstuvwxyz"));
276     assert(test!isUnicode("ABCDEFGHIUJKLMNOPQRSTUVXYZ"));
277     assert(test!isUnicode("1234567890"));
278     assert(test!isUnicode("Öl, Müll, Spaß"));
279     assert(!test!isUnicode("\xFF"));
280     assert(!test!isUnicode("\xF8\xA1\xA1\xA1\xA1"));
281 }
282 
283 unittest
284 {
285     const(char)[] s = "0000";
286     assert(test!isUnicode(s));
287 }
288 
289 /++
290     Value must contain only letters (a … z, A … Z)
291  +/
292 @constraint struct isAlpha
293 {
294     bool check(T)(T s)
295     {
296         import std.traits : isIterable;
297         import std.ascii : isAlpha;
298 
299         static if (isIterable!T)
300         {
301             foreach (c; s)
302                 if (!c.isAlpha)
303                     return false;
304 
305             return true;
306         }
307         else
308         {
309             return s.isAlpha;
310         }
311     }
312 
313     static immutable string errorMessage = "must contain alphabetic letters only";
314 }
315 
316 ///
317 unittest
318 {
319     assert(test!isAlpha("abcdefghijklmnopqrstuvwxyz"));
320     assert(test!isAlpha("ABCDEFGHIUJKLMNOPQRSTUVXYZ"));
321     assert(!test!isAlpha("a12"));
322     assert(!test!isAlpha("Müll"));
323 
324     assert(test!isAlpha('a'));
325     assert(test!isAlpha('A'));
326     assert(!test!isAlpha('9'));
327     assert(!test!isAlpha("\xFF"));
328 }
329 
330 /++
331     Value must contain only uppercase letters (A … Z)
332  +/
333 @constraint struct isUpper
334 {
335     bool check(T)(T s)
336     {
337         import std.traits : isIterable;
338         import std.ascii : isUpper;
339 
340         static if (isIterable!T)
341         {
342             foreach (c; s)
343                 if (!c.isUpper)
344                     return false;
345 
346             return true;
347         }
348         else
349         {
350             return s.isUpper;
351         }
352     }
353 
354     static immutable string errorMessage = "must contain uppercase letters only";
355 }
356 
357 ///
358 unittest
359 {
360     assert(!test!isUpper("abcdefghijklmnopqrstuvwxyz"));
361     assert(test!isUpper("ABCDEFGHIUJKLMNOPQRSTUVXYZ"));
362     assert(!test!isUpper("A12"));
363     assert(!test!isUpper("MÜLL"));
364 
365     assert(!test!isUpper('a'));
366     assert(test!isUpper('A'));
367     assert(!test!isUpper('9'));
368     assert(!test!isUpper("\xFF"));
369 }
370 
371 /++
372     Value must contain only lowercase letters (A … Z)
373  +/
374 @constraint struct isLower
375 {
376     bool check(T)(T s)
377     {
378         import std.traits : isIterable;
379         import std.ascii : isLower;
380 
381         static if (isIterable!T)
382         {
383             foreach (c; s)
384                 if (!c.isLower)
385                     return false;
386 
387             return true;
388         }
389         else
390         {
391             return s.isLower;
392         }
393     }
394 
395     static immutable string errorMessage = "must contain alphabetic lowercase letters only";
396 }
397 
398 ///
399 unittest
400 {
401     assert(test!isLower("abcdefghijklmnopqrstuvwxyz"));
402     assert(!test!isLower("ABCDEFGHIUJKLMNOPQRSTUVXYZ"));
403     assert(!test!isLower("A12"));
404     assert(!test!isLower("müll"));
405 
406     assert(test!isLower('a'));
407     assert(!test!isLower('A'));
408     assert(!test!isLower('9'));
409     assert(!test!isLower("\xFF"));
410 }
411 
412 /++
413     Value must contain only alpha-numeric characters (a … z, A … Z, 0 … 9)
414  +/
415 @constraint struct isAlphaNum
416 {
417     bool check(T)(T s)
418     {
419         import std.traits : isIterable;
420         import std.ascii : isAlphaNum;
421 
422         static if (isIterable!T)
423         {
424             foreach (c; s)
425                 if (!c.isAlphaNum)
426                     return false;
427 
428             return true;
429         }
430         else
431         {
432             return s.isAlphaNum;
433         }
434     }
435 
436     static immutable string errorMessage = "must contain alpha-numeric characters only";
437 }
438 
439 ///
440 unittest
441 {
442     assert(test!isAlphaNum("abcdefghijklmnopqrstuvwxyz"));
443     assert(test!isAlphaNum("ABCDEFGHIUJKLMNOPQRSTUVXYZ"));
444     assert(test!isAlphaNum("A12"));
445     assert(test!isAlphaNum("Az12"));
446     assert(!test!isAlphaNum("Müll"));
447 
448     assert(test!isAlphaNum('a'));
449     assert(test!isAlphaNum('A'));
450     assert(test!isAlphaNum('9'));
451     assert(!test!isAlphaNum("\xFF"));
452 }
453 
454 /++
455     Value must contain only digits (0 … 9)
456  +/
457 @constraint struct isDigit
458 {
459     bool check(T)(T s)
460     {
461         import std.traits : isIterable;
462         import std.ascii : isDigit;
463 
464         static if (isIterable!T)
465         {
466             foreach (c; s)
467                 if (!c.isDigit)
468                     return false;
469 
470             return true;
471         }
472         else
473         {
474             return s.isDigit;
475         }
476     }
477 
478     static immutable string errorMessage = "must contain digits only";
479 }
480 
481 ///
482 unittest
483 {
484     assert(test!isDigit("12998"));
485     assert(test!isDigit("1"));
486 
487     assert(!test!isDigit("abcdefghijklmnopqrstuvwxyz"));
488     assert(!test!isDigit("ABCDEFGHIUJKLMNOPQRSTUVXYZ"));
489     assert(!test!isDigit("A12"));
490     assert(!test!isDigit("Az12"));
491     assert(!test!isDigit("Müll"));
492 
493     assert(!test!isDigit('a'));
494     assert(!test!isDigit('A'));
495     assert(test!isDigit('9'));
496     assert(!test!isDigit("\xFF"));
497 }
498 
499 ///
500 enum notEmpty = minLength(1);
501 
502 ///
503 unittest
504 {
505     assert(test!notEmpty("12998"));
506     assert(test!notEmpty("0"));
507     assert(!test!notEmpty(""));
508 
509     assert(test!notEmpty([123, 456, 789,]));
510     assert(!test!notEmpty([]));
511 
512     immutable string null_ = null;
513     assert(!test!notEmpty(null_));
514 }
515 
516 // numeric
517 
518 /++
519     Value must be >= N
520 
521     Minimum value constraint
522  +/
523 @constraint struct greaterThanOrEqualTo
524 {
525     long n;
526 
527     bool check(T)(T actual)
528     {
529         return (actual >= n);
530     }
531 
532     string errorMessage()
533     {
534         return "must be >= " ~ this.n.to!string;
535     }
536 }
537 
538 ///
539 unittest
540 {
541     assert(test!(greaterThanOrEqualTo(10))(11));
542     assert(test!(greaterThanOrEqualTo(10))(10));
543     assert(!test!(greaterThanOrEqualTo(10))(9));
544     assert(!test!(greaterThanOrEqualTo(10))(-1));
545 
546     assert(test!(greaterThanOrEqualTo(-10))(1));
547     assert(test!(greaterThanOrEqualTo(-10))(-10));
548     assert(!test!(greaterThanOrEqualTo(-10))(-11));
549 }
550 
551 /++
552     Value must be <= N
553 
554     Maximum value constraint
555  +/
556 @constraint struct lessThanOrEqualTo
557 {
558     long n;
559 
560     bool check(T)(T actual)
561     {
562         return (actual <= n);
563     }
564 
565     string errorMessage()
566     {
567         return "must be <= " ~ this.n.to!string;
568     }
569 }
570 
571 ///
572 unittest
573 {
574     assert(!test!(lessThanOrEqualTo(10))(11));
575     assert(test!(lessThanOrEqualTo(10))(10));
576     assert(test!(lessThanOrEqualTo(10))(9));
577     assert(test!(lessThanOrEqualTo(10))(-1));
578 
579     assert(!test!(lessThanOrEqualTo(-10))(1));
580     assert(test!(lessThanOrEqualTo(-10))(-10));
581     assert(test!(lessThanOrEqualTo(-10))(-11));
582 }
583 
584 /++
585     Value must be < N
586  +/
587 @constraint struct lessThan
588 {
589     long n;
590 
591     bool check(T)(T actual)
592     {
593         return (actual < n);
594     }
595 
596     string errorMessage()
597     {
598         return "must be < " ~ n.to!string;
599     }
600 }
601 
602 ///
603 unittest
604 {
605     assert(!test!(lessThan(10))(11));
606     assert(!test!(lessThan(10))(10));
607     assert(test!(lessThan(10))(9));
608     assert(test!(lessThan(10))(-1));
609 
610     assert(!test!(lessThan(-10))(1));
611     assert(!test!(lessThan(-10))(-10));
612     assert(test!(lessThan(-10))(-11));
613 }
614 
615 /++
616     Value must be > N
617  +/
618 @constraint struct greaterThan
619 {
620     long n;
621 
622     bool check(T)(T actual)
623     {
624         return (actual > n);
625     }
626 
627     string errorMessage()
628     {
629         return "must be > " ~ n.to!string;
630     }
631 }
632 
633 ///
634 unittest
635 {
636     assert(test!(greaterThan(10))(11));
637     assert(!test!(greaterThan(10))(10));
638     assert(!test!(greaterThan(10))(9));
639     assert(!test!(greaterThan(10))(-1));
640 
641     assert(test!(greaterThan(-10))(1));
642     assert(!test!(greaterThan(-10))(-10));
643     assert(!test!(greaterThan(-10))(-11));
644 }
645 
646 /++
647     Value must not be equal to zero
648  +/
649 @constraint struct notZero
650 {
651     bool check(T)(T actual)
652     {
653         return (actual != 0);
654     }
655 
656     enum string errorMessage = "must not be zero";
657 }
658 
659 ///
660 unittest
661 {
662     assert(test!notZero(1));
663     assert(test!notZero(2));
664     assert(test!notZero(-1));
665     assert(!test!notZero(0));
666 }
667 
668 enum positive = greaterThan(0); ///
669 enum negative = lessThan(0); ///
670 enum notNegative = greaterThanOrEqualTo(0); ///
671 enum notPositive = lessThanOrEqualTo(0); ///
672 alias minValue = greaterThanOrEqualTo; ///
673 alias maxValue = lessThanOrEqualTo; ///
674 
675 // floating point
676 
677 ///
678 struct notNaN
679 {
680     bool check(T)(T actual)
681     {
682         import std.math : isNaN;
683 
684         return (!actual.isNaN);
685     }
686 
687     enum string errorMessage = "must not be NaN";
688 }
689 
690 ///
691 unittest
692 {
693     assert(test!notNaN(1.0f));
694     assert(test!notNaN(-1.0f));
695 
696     assert(test!notNaN(1.0));
697     assert(test!notNaN(-1.0));
698 
699     enum float nan = 0f / 0f;
700     assert(!test!notNaN(nan));
701 }
702 
703 // probably not something you’d want to use
704 alias allFactoryConstraints = getSymbolsByUDA!(oceandrift.validation.constraints, constraint);
705 
706 /++
707     Determines whether a symbol qualifies as constraint
708 
709     $(WARNING
710         Does not validate the constraint implementation.
711         Just checks whether it’s marked @[constraint].
712     )
713  +/
714 enum isConstraint(alias ConstraintCandidate) = hasUDA!(ConstraintCandidate, constraint);
715 
716 ///
717 unittest
718 {
719     @constraint static struct aConstraint
720     {
721         bool check(int)
722         {
723             return false;
724         }
725 
726         enum string errorMessage = "hi";
727     }
728 
729     static struct notAConstraint
730     {
731         bool check(int)
732         {
733             return false;
734         }
735 
736         enum string errorMessage = "hi";
737     }
738 
739     assert(isConstraint!(minLength));
740     assert(isConstraint!(aConstraint));
741     assert(!(isConstraint!(notAConstraint)));
742 }
743 
744 // unittest helper function
745 private bool test(alias constraintToTest, T)(T actual)
746 {
747     string error = constraintToTest.errorMessage;
748     assert(error !is null);
749 
750     return constraintToTest.check(actual);
751 }
752 
753 // unittest helper function
754 private bool test(ConstraintToTest, T)(T actual)
755 {
756     auto constraintToTest = ConstraintToTest.init;
757 
758     immutable string error = constraintToTest.errorMessage;
759     assert(error !is null);
760 
761     return constraintToTest.check(actual);
762 }