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 }