├── .benchmark ├── README.md ├── benchmark.svg ├── comparison.bench.ts └── stubs │ ├── user.ts │ ├── validate_ajv.ts │ ├── validate_safen.ts │ ├── validate_typebox.ts │ └── validate_zod.ts ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── ast ├── ast.ts ├── desugar.test.ts ├── desugar.ts ├── estimate_type.test.ts ├── estimate_type.ts └── utils.ts ├── decorator ├── decorator.test.ts └── decorator.ts ├── decorators.test.ts ├── decorators.ts ├── decorators ├── alpha.ts ├── alphanum.ts ├── ascii.ts ├── base64.ts ├── between.ts ├── ceil.ts ├── creditcard.ts ├── dateformat.ts ├── email.ts ├── empty_to_null.ts ├── floor.ts ├── hexcolor.ts ├── ip.ts ├── json.ts ├── length.ts ├── length_between.ts ├── length_max.ts ├── length_min.ts ├── lowercase.ts ├── macaddress.ts ├── max.ts ├── min.ts ├── port.ts ├── re.ts ├── round.ts ├── stringify.ts ├── to_lower.ts ├── to_upper.ts ├── trim.ts ├── uppercase.ts ├── url.ts └── uuid.ts ├── deno.json ├── mod.ts ├── parser └── syntax_error.ts ├── scripts └── build_npm.ts ├── short.test.ts ├── short.ts └── validator ├── create_sanitize.test.ts ├── create_sanitize.ts ├── create_validate.test.ts ├── create_validate.ts └── invalid_value_error.ts /.benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Safen Benchmark 2 | 3 | ![benchmark](./benchmark.svg) 4 | -------------------------------------------------------------------------------- /.benchmark/benchmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | c 14 | p 15 | u 16 | : 17 | A 18 | p 19 | p 20 | l 21 | e 22 | M 23 | 1 24 | r 25 | u 26 | n 27 | t 28 | i 29 | m 30 | e 31 | : 32 | d 33 | e 34 | n 35 | o 36 | 1 37 | . 38 | 3 39 | 1 40 | . 41 | 3 42 | ( 43 | a 44 | a 45 | r 46 | c 47 | h 48 | 6 49 | 4 50 | - 51 | a 52 | p 53 | p 54 | l 55 | e 56 | - 57 | d 58 | a 59 | r 60 | w 61 | i 62 | n 63 | ) 64 | f 65 | i 66 | l 67 | e 68 | : 69 | / 70 | / 71 | / 72 | U 73 | s 74 | e 75 | r 76 | s 77 | / 78 | w 79 | a 80 | n 81 | 2 82 | l 83 | a 84 | n 85 | d 86 | / 87 | W 88 | o 89 | r 90 | k 91 | s 92 | p 93 | a 94 | c 95 | e 96 | / 97 | @ 98 | d 99 | e 100 | n 101 | o 102 | s 103 | t 104 | a 105 | c 106 | k 107 | / 108 | s 109 | a 110 | f 111 | e 112 | n 113 | / 114 | . 115 | b 116 | e 117 | n 118 | c 119 | h 120 | m 121 | a 122 | r 123 | k 124 | / 125 | c 126 | o 127 | m 128 | p 129 | a 130 | r 131 | i 132 | s 133 | o 134 | n 135 | . 136 | b 137 | e 138 | n 139 | c 140 | h 141 | . 142 | t 143 | s 144 | b 145 | e 146 | n 147 | c 148 | h 149 | m 150 | a 151 | r 152 | k 153 | t 154 | i 155 | m 156 | e 157 | ( 158 | a 159 | v 160 | g 161 | ) 162 | ( 163 | m 164 | i 165 | n 166 | 167 | m 168 | a 169 | x 170 | ) 171 | p 172 | 7 173 | 5 174 | p 175 | 9 176 | 9 177 | p 178 | 9 179 | 9 180 | 5 181 | - 182 | - 183 | - 184 | - 185 | - 186 | - 187 | - 188 | - 189 | - 190 | - 191 | - 192 | - 193 | - 194 | - 195 | - 196 | - 197 | - 198 | - 199 | - 200 | - 201 | - 202 | - 203 | - 204 | - 205 | - 206 | - 207 | - 208 | - 209 | - 210 | - 211 | - 212 | - 213 | - 214 | - 215 | - 216 | - 217 | - 218 | - 219 | - 220 | - 221 | - 222 | - 223 | - 224 | - 225 | - 226 | - 227 | - 228 | - 229 | - 230 | - 231 | - 232 | - 233 | - 234 | - 235 | - 236 | - 237 | - 238 | - 239 | - 240 | - 241 | - 242 | - 243 | - 244 | - 245 | - 246 | - 247 | - 248 | - 249 | - 250 | - 251 | - 252 | - 253 | - 254 | - 255 | - 256 | - 257 | - 258 | - 259 | - 260 | - 261 | - 262 | - 263 | - 264 | - 265 | - 266 | - 267 | - 268 | - 269 | - 270 | - 271 | - 272 | - 273 | - 274 | - 275 | - 276 | - 277 | - 278 | - 279 | - 280 | - 281 | - 282 | - 283 | - 284 | - 285 | - 286 | S 287 | a 288 | f 289 | e 290 | n 291 | O 292 | b 293 | j 294 | e 295 | c 296 | t 297 | 4 298 | 5 299 | 4 300 | . 301 | 9 302 | 8 303 | n 304 | s 305 | / 306 | i 307 | t 308 | e 309 | r 310 | ( 311 | 4 312 | 3 313 | 8 314 | . 315 | 9 316 | n 317 | s 318 | 319 | 4 320 | 6 321 | 8 322 | . 323 | 3 324 | 2 325 | n 326 | s 327 | ) 328 | 4 329 | 5 330 | 7 331 | . 332 | 0 333 | 3 334 | n 335 | s 336 | 4 337 | 6 338 | 6 339 | . 340 | 3 341 | 9 342 | n 343 | s 344 | 4 345 | 6 346 | 8 347 | . 348 | 3 349 | 2 350 | n 351 | s 352 | S 353 | a 354 | f 355 | e 356 | n 357 | ( 358 | w 359 | / 360 | V 361 | a 362 | l 363 | i 364 | d 365 | a 366 | t 367 | e 368 | C 369 | r 370 | e 371 | a 372 | t 373 | i 374 | o 375 | n 376 | ) 377 | O 378 | b 379 | j 380 | e 381 | c 382 | t 383 | 1 384 | 5 385 | . 386 | 3 387 | 2 388 | µ 389 | s 390 | / 391 | i 392 | t 393 | e 394 | r 395 | ( 396 | 1 397 | 2 398 | . 399 | 6 400 | 7 401 | µ 402 | s 403 | 404 | 1 405 | . 406 | 3 407 | 3 408 | m 409 | s 410 | ) 411 | 1 412 | 3 413 | . 414 | 9 415 | 2 416 | µ 417 | s 418 | 2 419 | 4 420 | . 421 | 1 422 | 7 423 | µ 424 | s 425 | 3 426 | 0 427 | . 428 | 3 429 | 8 430 | µ 431 | s 432 | Z 433 | o 434 | d 435 | O 436 | b 437 | j 438 | e 439 | c 440 | t 441 | 9 442 | 4 443 | . 444 | 4 445 | 2 446 | µ 447 | s 448 | / 449 | i 450 | t 451 | e 452 | r 453 | ( 454 | 9 455 | 0 456 | . 457 | 9 458 | 6 459 | µ 460 | s 461 | 462 | 6 463 | 3 464 | 8 465 | . 466 | 7 467 | 5 468 | µ 469 | s 470 | ) 471 | 9 472 | 3 473 | . 474 | 1 475 | 7 476 | µ 477 | s 478 | 1 479 | 1 480 | 5 481 | . 482 | 5 483 | 8 484 | µ 485 | s 486 | 2 487 | 5 488 | 7 489 | . 490 | 2 491 | 9 492 | µ 493 | s 494 | A 495 | j 496 | v 497 | O 498 | b 499 | j 500 | e 501 | c 502 | t 503 | 3 504 | . 505 | 1 506 | 7 507 | µ 508 | s 509 | / 510 | i 511 | t 512 | e 513 | r 514 | ( 515 | 3 516 | µ 517 | s 518 | 519 | 5 520 | 3 521 | . 522 | 3 523 | 8 524 | µ 525 | s 526 | ) 527 | 3 528 | . 529 | 1 530 | 7 531 | µ 532 | s 533 | 3 534 | . 535 | 3 536 | 8 537 | µ 538 | s 539 | 3 540 | . 541 | 4 542 | 6 543 | µ 544 | s 545 | A 546 | j 547 | v 548 | ( 549 | w 550 | / 551 | V 552 | a 553 | l 554 | i 555 | d 556 | a 557 | t 558 | e 559 | C 560 | r 561 | e 562 | a 563 | t 564 | i 565 | o 566 | n 567 | ) 568 | O 569 | b 570 | j 571 | e 572 | c 573 | t 574 | 2 575 | . 576 | 3 577 | 1 578 | m 579 | s 580 | / 581 | i 582 | t 583 | e 584 | r 585 | ( 586 | 2 587 | . 588 | 1 589 | 4 590 | m 591 | s 592 | 593 | 7 594 | . 595 | 2 596 | 9 597 | m 598 | s 599 | ) 600 | 2 601 | . 602 | 3 603 | 1 604 | m 605 | s 606 | 3 607 | . 608 | 1 609 | 5 610 | m 611 | s 612 | 4 613 | . 614 | 1 615 | 6 616 | m 617 | s 618 | T 619 | y 620 | p 621 | e 622 | B 623 | o 624 | x 625 | O 626 | b 627 | j 628 | e 629 | c 630 | t 631 | 7 632 | 3 633 | 4 634 | . 635 | 6 636 | n 637 | s 638 | / 639 | i 640 | t 641 | e 642 | r 643 | ( 644 | 7 645 | 2 646 | 3 647 | . 648 | 7 649 | 1 650 | n 651 | s 652 | 653 | 8 654 | 8 655 | 1 656 | . 657 | 4 658 | 5 659 | n 660 | s 661 | ) 662 | 7 663 | 4 664 | 0 665 | . 666 | 1 667 | 6 668 | n 669 | s 670 | 8 671 | 8 672 | 1 673 | . 674 | 4 675 | 5 676 | n 677 | s 678 | 8 679 | 8 680 | 1 681 | . 682 | 4 683 | 5 684 | n 685 | s 686 | T 687 | y 688 | p 689 | e 690 | B 691 | o 692 | x 693 | ( 694 | w 695 | / 696 | V 697 | a 698 | l 699 | i 700 | d 701 | a 702 | t 703 | e 704 | C 705 | r 706 | e 707 | a 708 | t 709 | i 710 | o 711 | n 712 | ) 713 | O 714 | b 715 | j 716 | e 717 | c 718 | t 719 | 2 720 | 3 721 | . 722 | 7 723 | 2 724 | µ 725 | s 726 | / 727 | i 728 | t 729 | e 730 | r 731 | ( 732 | 2 733 | 2 734 | . 735 | 1 736 | 2 737 | µ 738 | s 739 | 740 | 2 741 | . 742 | 1 743 | 4 744 | m 745 | s 746 | ) 747 | 2 748 | 2 749 | . 750 | 9 751 | 2 752 | µ 753 | s 754 | 3 755 | 7 756 | . 757 | 5 758 | 4 759 | µ 760 | s 761 | 4 762 | 2 763 | . 764 | 5 765 | 4 766 | µ 767 | s 768 | s 769 | u 770 | m 771 | m 772 | a 773 | r 774 | y 775 | S 776 | a 777 | f 778 | e 779 | n 780 | O 781 | b 782 | j 783 | e 784 | c 785 | t 786 | 1 787 | . 788 | 6 789 | 1 790 | x 791 | f 792 | a 793 | s 794 | t 795 | e 796 | r 797 | t 798 | h 799 | a 800 | n 801 | T 802 | y 803 | p 804 | e 805 | B 806 | o 807 | x 808 | O 809 | b 810 | j 811 | e 812 | c 813 | t 814 | 6 815 | . 816 | 9 817 | 6 818 | x 819 | f 820 | a 821 | s 822 | t 823 | e 824 | r 825 | t 826 | h 827 | a 828 | n 829 | A 830 | j 831 | v 832 | O 833 | b 834 | j 835 | e 836 | c 837 | t 838 | 3 839 | 3 840 | . 841 | 6 842 | 8 843 | x 844 | f 845 | a 846 | s 847 | t 848 | e 849 | r 850 | t 851 | h 852 | a 853 | n 854 | S 855 | a 856 | f 857 | e 858 | n 859 | ( 860 | w 861 | / 862 | V 863 | a 864 | l 865 | i 866 | d 867 | a 868 | t 869 | e 870 | C 871 | r 872 | e 873 | a 874 | t 875 | i 876 | o 877 | n 878 | ) 879 | O 880 | b 881 | j 882 | e 883 | c 884 | t 885 | 5 886 | 2 887 | . 888 | 1 889 | 2 890 | x 891 | f 892 | a 893 | s 894 | t 895 | e 896 | r 897 | t 898 | h 899 | a 900 | n 901 | T 902 | y 903 | p 904 | e 905 | B 906 | o 907 | x 908 | ( 909 | w 910 | / 911 | V 912 | a 913 | l 914 | i 915 | d 916 | a 917 | t 918 | e 919 | C 920 | r 921 | e 922 | a 923 | t 924 | i 925 | o 926 | n 927 | ) 928 | O 929 | b 930 | j 931 | e 932 | c 933 | t 934 | 2 935 | 0 936 | 7 937 | . 938 | 5 939 | 1 940 | x 941 | f 942 | a 943 | s 944 | t 945 | e 946 | r 947 | t 948 | h 949 | a 950 | n 951 | Z 952 | o 953 | d 954 | O 955 | b 956 | j 957 | e 958 | c 959 | t 960 | 5 961 | 0 962 | 8 963 | 1 964 | . 965 | 3 966 | 4 967 | x 968 | f 969 | a 970 | s 971 | t 972 | e 973 | r 974 | t 975 | h 976 | a 977 | n 978 | A 979 | j 980 | v 981 | ( 982 | w 983 | / 984 | V 985 | a 986 | l 987 | i 988 | d 989 | a 990 | t 991 | e 992 | C 993 | r 994 | e 995 | a 996 | t 997 | i 998 | o 999 | n 1000 | ) 1001 | O 1002 | b 1003 | j 1004 | e 1005 | c 1006 | t 1007 | 1008 | -------------------------------------------------------------------------------- /.benchmark/comparison.bench.ts: -------------------------------------------------------------------------------- 1 | import { generateRandomUser } from "./stubs/user.ts"; 2 | import * as zod from "./stubs/validate_zod.ts"; 3 | import * as safen from "./stubs/validate_safen.ts"; 4 | import * as typebox from "./stubs/validate_typebox.ts"; 5 | import * as ajv from "./stubs/validate_ajv.ts"; 6 | 7 | const user = generateRandomUser(); 8 | 9 | Deno.bench({ 10 | name: "Safen Object", 11 | group: "validate", 12 | ignore: !safen.isUser(user), 13 | baseline: true, 14 | }, () => { 15 | safen.isUser(user); 16 | }); 17 | 18 | Deno.bench({ 19 | name: "Safen(w/ Validate Creation) Object", 20 | group: "validate", 21 | ignore: !safen.generateAndIsUser(user), 22 | }, () => { 23 | safen.generateAndIsUser(user); 24 | }); 25 | 26 | Deno.bench({ 27 | name: "Zod Object", 28 | group: "validate", 29 | ignore: !zod.isUser(user), 30 | }, () => { 31 | zod.isUser(user); 32 | }); 33 | 34 | Deno.bench({ 35 | name: "Ajv Object", 36 | group: "validate", 37 | ignore: !ajv.isUser(user), 38 | }, () => { 39 | ajv.isUser(user); 40 | }); 41 | 42 | Deno.bench({ 43 | name: "Ajv(w/ Validate Creation) Object", 44 | group: "validate", 45 | ignore: !ajv.generateAndIsUser(user), 46 | }, () => { 47 | ajv.generateAndIsUser(user); 48 | }); 49 | 50 | Deno.bench({ 51 | name: "TypeBox Object", 52 | group: "validate", 53 | ignore: !typebox.isUser(user), 54 | }, () => { 55 | typebox.isUser(user); 56 | }); 57 | 58 | Deno.bench({ 59 | name: "TypeBox(w/ Validate Creation) Object", 60 | group: "validate", 61 | ignore: !typebox.generateAndIsUser(user), 62 | }, () => { 63 | typebox.generateAndIsUser(user); 64 | }); 65 | -------------------------------------------------------------------------------- /.benchmark/stubs/user.ts: -------------------------------------------------------------------------------- 1 | function repeatArray(n: number, fn: (idx: number) => T): T[] { 2 | return Array.from({ length: n }, (_, idx) => fn(idx)); 3 | } 4 | 5 | function random(min: number, max: number): number { 6 | return Math.floor(Math.random() * (max - min + 1)) + min; 7 | } 8 | 9 | export function generateRandomUser() { 10 | return { 11 | id: random(1, 100000), 12 | email: "wan2land@gmail.com", 13 | name: "wan2land", 14 | articles: repeatArray(20, (i) => ({ 15 | id: i, 16 | title: `title ${i}`, 17 | content: `content ${i}`, 18 | comments: repeatArray(3, (i) => ({ 19 | id: i, 20 | contents: `contents ${i}`, 21 | createdAt: { 22 | timestamp: 1671926400000, 23 | offset: 0, 24 | }, 25 | })), 26 | updatedAt: { 27 | timestamp: 1671926400000, 28 | offset: 0, 29 | }, 30 | createdAt: { 31 | timestamp: 1671926400000, 32 | offset: 0, 33 | }, 34 | })), 35 | comments: repeatArray(3, (i) => ({ 36 | id: i, 37 | contents: `contents ${i}`, 38 | createdAt: { 39 | timestamp: 1671926400000, 40 | offset: 0, 41 | }, 42 | })), 43 | location: "Seoul", 44 | createdAt: { 45 | timestamp: 1671926400000, 46 | offset: 0, 47 | }, 48 | }; 49 | } 50 | -------------------------------------------------------------------------------- /.benchmark/stubs/validate_ajv.ts: -------------------------------------------------------------------------------- 1 | import Ajv from "https://esm.sh/ajv@8"; 2 | 3 | const DateType = { 4 | type: "object", 5 | properties: { 6 | timestamp: { type: "integer" }, 7 | offset: { type: "integer" }, 8 | }, 9 | required: ["timestamp", "offset"], 10 | }; 11 | 12 | const CommentType = { 13 | type: "object", 14 | properties: { 15 | id: { 16 | anyOf: [ 17 | { type: "integer" }, 18 | { type: "string" }, 19 | ], 20 | }, 21 | contents: { type: "string" }, 22 | createdAt: DateType, 23 | }, 24 | required: ["id", "contents", "createdAt"], 25 | }; 26 | 27 | const ArticleType = { 28 | type: "object", 29 | properties: { 30 | id: { type: "integer" }, 31 | title: { type: "string" }, 32 | content: { type: "string" }, 33 | comments: { 34 | type: "array", 35 | items: CommentType, 36 | }, 37 | updatedAt: DateType, 38 | createdAt: DateType, 39 | }, 40 | required: ["id", "title", "content", "comments", "updatedAt", "createdAt"], 41 | }; 42 | 43 | const UserType = { 44 | type: "object", 45 | properties: { 46 | id: { 47 | anyOf: [ 48 | { type: "integer" }, 49 | { type: "string" }, 50 | ], 51 | }, 52 | email: { type: "string" }, 53 | name: { type: "string" }, 54 | articles: { 55 | type: "array", 56 | items: ArticleType, 57 | }, 58 | comments: { 59 | type: "array", 60 | items: CommentType, 61 | }, 62 | location: { type: "string" }, 63 | createdAt: DateType, 64 | }, 65 | required: [ 66 | "id", 67 | "email", 68 | "name", 69 | "articles", 70 | "comments", 71 | "location", 72 | "createdAt", 73 | ], 74 | }; 75 | 76 | const ajv1 = new Ajv(); 77 | const validate = ajv1.compile(UserType); 78 | export function isUser(input: unknown) { 79 | return validate(input); 80 | } 81 | 82 | const ajv2 = new Ajv(); 83 | export function generateAndIsUser(input: unknown) { 84 | ajv2.removeSchema(); // clear cache 85 | const validate = ajv2.compile(UserType); 86 | return validate(input); 87 | } 88 | -------------------------------------------------------------------------------- /.benchmark/stubs/validate_safen.ts: -------------------------------------------------------------------------------- 1 | import { v } from "../../mod.ts"; 2 | 3 | const DateType = { 4 | timestamp: Number, 5 | offset: Number, 6 | }; 7 | 8 | const CommentType = { 9 | id: v.union([Number, String]), 10 | contents: String, 11 | createdAt: DateType, 12 | }; 13 | 14 | const ArticleType = { 15 | id: v.union([Number, String]), 16 | title: String, 17 | content: String, 18 | comments: v.array(CommentType), 19 | updatedAt: DateType, 20 | createdAt: DateType, 21 | }; 22 | 23 | const UserType = { 24 | id: v.union([Number, String]), 25 | email: v.decorate(String, (d) => d.email()), 26 | name: String, 27 | articles: v.array(ArticleType), 28 | comments: v.array(CommentType), 29 | location: String, 30 | createdAt: DateType, 31 | }; 32 | 33 | const validate = v(UserType); 34 | export function isUser(user: unknown) { 35 | return validate(user); 36 | } 37 | 38 | export function generateAndIsUser(user: unknown) { 39 | return v(UserType)(user); 40 | } 41 | -------------------------------------------------------------------------------- /.benchmark/stubs/validate_typebox.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "npm:@sinclair/typebox"; 2 | import { TypeCompiler } from "npm:@sinclair/typebox/compiler"; 3 | 4 | const DateType = Type.Object({ 5 | timestamp: Type.Integer(), 6 | offset: Type.Integer(), 7 | }); 8 | 9 | const CommentType = Type.Object({ 10 | id: Type.Union([Type.Integer(), Type.String()]), 11 | contents: Type.String(), 12 | createdAt: DateType, 13 | }); 14 | 15 | const ArticleType = Type.Object({ 16 | id: Type.Union([Type.Integer(), Type.String()]), 17 | title: Type.String(), 18 | content: Type.String(), 19 | comments: Type.Array(CommentType), 20 | updatedAt: DateType, 21 | createdAt: DateType, 22 | }); 23 | 24 | const UserType = Type.Object({ 25 | id: Type.Union([Type.Integer(), Type.String()]), 26 | email: Type.String(), // email? 27 | name: Type.String(), 28 | articles: Type.Array(ArticleType), 29 | comments: Type.Array(CommentType), 30 | location: Type.String(), 31 | createdAt: DateType, 32 | }); 33 | 34 | const compiled = TypeCompiler.Compile(UserType); 35 | export function isUser(input: unknown) { 36 | return compiled.Check(input); 37 | } 38 | 39 | export function generateAndIsUser(input: unknown) { 40 | const compiled = TypeCompiler.Compile(UserType); 41 | return compiled.Check(input); 42 | } 43 | -------------------------------------------------------------------------------- /.benchmark/stubs/validate_zod.ts: -------------------------------------------------------------------------------- 1 | import { z } from "https://deno.land/x/zod@v3.16.1/mod.ts"; 2 | 3 | const DateType = z.object({ 4 | timestamp: z.number().int(), 5 | offset: z.number().int(), 6 | }); 7 | 8 | const CommentType = z.object({ 9 | id: z.number().int().or(z.string()), 10 | contents: z.string(), 11 | createdAt: DateType, 12 | }); 13 | 14 | const ArticleType = z.object({ 15 | id: z.number().int().or(z.string()), 16 | title: z.string(), 17 | content: z.string(), 18 | comments: z.array(CommentType), 19 | updatedAt: DateType, 20 | createdAt: DateType, 21 | }); 22 | 23 | const UserType = z.object({ 24 | id: z.number().int().or(z.string()), 25 | email: z.string(), 26 | name: z.string(), 27 | articles: z.array(ArticleType), 28 | comments: z.array(CommentType), 29 | location: z.string(), 30 | createdAt: DateType, 31 | }); 32 | 33 | export function isUser(input: unknown) { 34 | return UserType.safeParse(input).success; 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | deno-version: [v1.x] 15 | steps: 16 | - name: Git Checkout Deno Module 17 | uses: actions/checkout@v2 18 | - name: Use Deno Version ${{ matrix.deno-version }} 19 | uses: denoland/setup-deno@v1 20 | with: 21 | deno-version: ${{ matrix.deno-version }} 22 | - name: Format 23 | run: deno fmt --check 24 | - name: Lint 25 | run: deno lint 26 | - name: Unit Test 27 | run: deno test --coverage=coverage 28 | - name: Create coverage report 29 | run: deno coverage ./coverage --lcov > coverage.lcov 30 | - name: Collect coverage 31 | uses: codecov/codecov-action@v1.0.10 32 | with: 33 | file: ./coverage.lcov 34 | - name: Build Module 35 | run: deno task build:npm 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .npm 2 | 3 | deno.lock 4 | 5 | examples/**/package-lock.json 6 | examples/**/node_modules 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "denoland.vscode-deno" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "denoland.vscode-deno", 6 | "cSpell.words": [ 7 | "alphanum", 8 | "creditcard", 9 | "hexcolor", 10 | "macaddress", 11 | "safen", 12 | "typebox" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Safen 2 | 3 |

4 | Build 5 | Coverage 6 | License 7 | Language Typescript 8 |
9 | deno.land/x/safen 10 | Version 11 | Downloads 12 |

13 | 14 | Safen is a high-performance validation and sanitization library with easy type 15 | inference. Its syntax is similar to TypeScript interface, making it easy to 16 | create validation rules. 17 | 18 | https://user-images.githubusercontent.com/4086535/203831205-8b3481cb-bb8d-4f3c-9876-e41adb6855fd.mp4 19 | 20 | ## Installation 21 | 22 | **Node** 23 | 24 | ```bash 25 | npm install safen 26 | ``` 27 | 28 | **Deno** 29 | 30 | ```ts 31 | import { 32 | s, // create sanitize, 33 | v, // create validate, 34 | } from "https://deno.land/x/safen/mod.ts"; 35 | ``` 36 | 37 | ## Basic Usage 38 | 39 | **Create Validate Fn** 40 | 41 | ```ts 42 | import { v } from "https://deno.land/x/safen/mod.ts"; 43 | 44 | const validate = v(String); // now, validate: (data: unknown) => data is string 45 | 46 | const input = {} as unknown; 47 | if (validate(input)) { 48 | // now input is string! 49 | } 50 | ``` 51 | 52 | **Create Sanitize Fn** 53 | 54 | ```ts 55 | import { s } from "https://deno.land/x/safen/mod.ts"; 56 | 57 | const sanitize = s(String); // now, sanitize: (data: unknown) => string 58 | 59 | const input = {} as unknown; // some unknown value 60 | 61 | sanitize("something" as unknown); // return "something" 62 | sanitize(null as unknown); // throw InvalidValueError 63 | ``` 64 | 65 | ## Types 66 | 67 | ```ts 68 | // Primitive Types 69 | const validate = v(String); // (data: unknown) => data is string 70 | const validate = v(Number); // (data: unknown) => data is number 71 | const validate = v(Boolean); // (data: unknown) => data is boolean 72 | const validate = v(BigInt); // (data: unknown) => data is bigint 73 | const validate = v(Symbol); // (data: unknown) => data is symbol 74 | 75 | // Literal Types 76 | const validate = v("foo"); // (data: unknown) => data is "foo" 77 | const validate = v(1024); // (data: unknown) => data is 1024 78 | const validate = v(true); // (data: unknown) => data is true 79 | const validate = v(2048n); // (data: unknown) => data is 2048n 80 | const validate = v(null); // (data: unknown) => data is null 81 | const validate = v(undefined); // (data: unknown) => data is undefined 82 | 83 | // Special 84 | const validate = v(v.any()); // (data: unknown) => data is any 85 | const validate = v(Array); // (data: unknown) => data is any[] 86 | 87 | // Object 88 | const Point = { x: Number, y: Number }; 89 | const validate = v({ p1: Point, p2: Point }); // (data: unknown) => data is { p1: { x: number, y: number }, p2: { x: number, y: number } } 90 | 91 | // Union 92 | const validate = v(v.union([String, Number])); // (data: unknown) => data is string | number 93 | 94 | // Array 95 | const validate = v([String]); // (data: unknown) => data is string[] 96 | const validate = v([v.union([String, Number])]); // (data: unknown) => data is (string | number)[] 97 | ``` 98 | 99 | ## Decorator 100 | 101 | Decorators do not affect type inference, but do affect additional validation and 102 | data transformation. 103 | 104 | **Step1. Basic Sanitize** 105 | 106 | ```ts 107 | const sanitize = s(s.union([ 108 | String, 109 | null, 110 | ])); 111 | 112 | sanitize("hello world!"); // return "hello world!" 113 | sanitize(" hello world! "); // return " hello world! " 114 | sanitize(" "); // return " " 115 | sanitize(null); // return null 116 | ``` 117 | 118 | **Step2. Add trim decorator** 119 | 120 | ```ts 121 | const sanitize = s(s.union([ 122 | s.decorate(String, (d) => d.trim()), 123 | null, 124 | ])); 125 | 126 | sanitize("hello world!"); // return "hello world!" 127 | sanitize(" hello world! "); // return "hello world!" 128 | sanitize(" "); // return "" 129 | sanitize(null); // return null 130 | ``` 131 | 132 | **Step3. Add emptyToNull decorator** 133 | 134 | ```ts 135 | const sanitize = s( 136 | s.decorate( 137 | s.union([ 138 | s.decorate(String, (d) => d.trim()), 139 | null, 140 | ]), 141 | (d) => d.emptyToNull(), 142 | ), 143 | ); 144 | 145 | sanitize("hello world!"); // return "hello world!" 146 | sanitize(" hello world! "); // return "hello world!" 147 | sanitize(" "); // return null 148 | sanitize(null); // return null 149 | ``` 150 | 151 | ### Defined Decorators 152 | 153 | | Decorator | Validate | Transform | Type | Description | 154 | | ------------------------- | -------- | --------- | ------------------ | ----------------------------------------------------------------------------------- | 155 | | `alpha` | ✅ | | `string` | contains only letters([a-zA-Z]). | 156 | | `alphanum` | ✅ | | `string` | contains only letters and numbers([a-zA-Z0-9]) | 157 | | `ascii` | ✅ | | `string` | contains only ascii characters. | 158 | | `base64` | ✅ | | `string` | Base64. | 159 | | `between(min, max)` | ✅ | | `string`, `number` | value is between `{min}` and `{max}`. (ex) `between("aaa","zzz")`, `between(1,100)` | 160 | | `ceil` | | ✅ | `number` | Math.ceil. (ref. `floor`, `round`) | 161 | | `creditcard` | ✅ | | `string` | valid Credit Card number. cf. `0000-0000-0000-0000` | 162 | | `dateformat` | ✅ | | `string` | valid Date string(RFC2822, ISO8601). cf. `2018-12-25`, `12/25/2018`, `Dec 25, 2018` | 163 | | `email` | ✅ | | `string` | valid E-mail string. | 164 | | `emptyToNull` | | ✅ | `string or null` | empty string(`""`) to null | 165 | | `floor` | | ✅ | `number` | Math.floor. (ref. `ceil`, `round`) | 166 | | `hexcolor` | ✅ | | `string` | valid Hex Color string. cf. `#ffffff` | 167 | | `ip(version = null)` | ✅ | | `string` | valid UUID.
version is one of `null`(both, default), `v4`, and `v6`. | 168 | | `json` | ✅ | | `string` | valid JSON. | 169 | | `length(size)` | ✅ | | `string`, `any[]` | length is `{size}`. | 170 | | `lengthBetween(min, max)` | ✅ | | `string`, `any[]` | length is between `{min}` and `{max}`. | 171 | | `lengthMax(max)` | ✅ | | `string`, `any[]` | length is less than `{max}`. | 172 | | `lengthMin(min)` | ✅ | | `string`, `any[]` | length is greater than `{min}`. | 173 | | `lowercase` | ✅ | | `string` | lowercase. | 174 | | `macaddress` | ✅ | | `string` | valid Mac Address. | 175 | | `max(max)` | ✅ | | `string`, `number` | value is less than `{min}`. | 176 | | `min(min)` | ✅ | | `string`, `number` | value is greater than `{max}`. | 177 | | `port` | ✅ | | `number` | valid PORT(0-65535). | 178 | | `re` | ✅ | | `string` | match RegExp. | 179 | | `round` | | ✅ | `number` | Math.round. (ref. `ceil`, `floor`) | 180 | | `stringify` | | ✅ | `string` | cast to string | 181 | | `toLower` | | ✅ | `string` | change to lower case. | 182 | | `toUpper` | | ✅ | `string` | change to upper case. | 183 | | `trim` | | ✅ | `string` | trim. | 184 | | `uppercase` | ✅ | | `string` | uppercase. | 185 | | `url` | ✅ | | `string` | valid URL. | 186 | | `uuid(version = null)` | ✅ | | `string` | valid UUID.
version is one of `null`(default), `v3`, `v4`, and `v5`. | 187 | 188 | ## Custom Decorator 189 | 190 | ```mermaid 191 | graph LR; 192 | A[input] -->|type = unknown| B{cast}; 193 | B -->|type = T| C{validate}; 194 | C -->|true| D{transform}; 195 | C -->|false| E[error]; 196 | D --> F[output]; 197 | ``` 198 | 199 | ```ts 200 | interface Decorator { 201 | name: string; 202 | cast?(v: unknown): T; 203 | validate?(v: T): boolean; 204 | transform?(v: T): T; 205 | } 206 | ``` 207 | 208 | The `cast` function is invoked at the beginning of the data processing pipeline, 209 | before the `validate` and `transform` functions. The purpose of the `cast` 210 | function is to ensure that the data is in the right type before being processed 211 | further. 212 | 213 | This is an example of a cast-only function: 214 | 215 | ```ts 216 | const decorator: Decorator = { 217 | name: "json_string", 218 | cast: (v) => JSON.stringify(v), 219 | }; 220 | ``` 221 | 222 | Once the data has been casted, the `validate` function is called to verify the 223 | content and format of the data. This function ensures that the data is valid and 224 | meets the specified criteria before being processed further. 225 | 226 | The `transform` function, on the other hand, is invoked only after the 227 | validation function returns a `true` result. The `transform` function then 228 | processes the data according to the specified rules and criteria. 229 | 230 | Therefore, the `cast`, `validate`, and `transform` functions work together to 231 | ensure that the data is in the right format, is valid, and is properly 232 | processed. 233 | 234 | ## Benchmark 235 | 236 | Please see [benchmark results](.benchmark). 237 | 238 | ## Old Version Docs 239 | 240 | - [1.x](https://github.com/denostack/safen/tree/1.x) 241 | - [2.x](https://github.com/denostack/safen/tree/1.x) 242 | -------------------------------------------------------------------------------- /ast/ast.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | 3 | import { Decorator } from "../decorator/decorator.ts"; 4 | 5 | export enum Kind { 6 | Primitive = 1, 7 | Literal = 2, 8 | 9 | Array = 11, 10 | Object = 12, 11 | 12 | Union = 21, 13 | 14 | Decorator = 31, 15 | } 16 | 17 | export enum PrimitiveType { 18 | Any = 0, 19 | Null = 1, 20 | Undefined = 2, 21 | String = 3, 22 | Number = 4, 23 | Boolean = 5, 24 | BigInt = 6, 25 | Symbol = 7, 26 | } 27 | 28 | export type Ast = 29 | | AstSugarPrimitive 30 | | AstSugarLiteral 31 | | AstSugarArray 32 | | AstSugarAnyArray 33 | | AstSugarObject 34 | | AstStrict; 35 | 36 | export type AstSugarPrimitive = 37 | | null 38 | | undefined 39 | | StringConstructor 40 | | NumberConstructor 41 | | BooleanConstructor 42 | | BigIntConstructor 43 | | SymbolConstructor; 44 | 45 | export type AstSugarLiteral = string | number | boolean | bigint; 46 | export type AstSugarArray = [Ast]; 47 | export type AstSugarAnyArray = ArrayConstructor; // map to [array, [primitive, any]]; 48 | export interface AstSugarObject { 49 | [key: string]: Ast; 50 | } 51 | 52 | // Strict 53 | export type AstStrict = 54 | | AstPrimitive 55 | | AstLiteral 56 | | AstArray 57 | | AstObject 58 | | AstUnion 59 | | AstDecorator; 60 | 61 | export type AstPrimitive = [kind: Kind.Primitive, type: PrimitiveType]; 62 | export type AstLiteral = [ 63 | kind: Kind.Literal, 64 | type: string | number | boolean | bigint, 65 | ]; 66 | export type AstArray = [kind: Kind.Array, of: T]; 67 | export type AstObject = [ 68 | kind: Kind.Object, 69 | obj: { [key: string]: T }, 70 | ]; 71 | export type AstUnion = [kind: Kind.Union, types: T[]]; 72 | export type AstDecorator = [ 73 | kind: Kind.Decorator, 74 | of: T, 75 | decorators: Decorator[], 76 | ]; 77 | -------------------------------------------------------------------------------- /ast/desugar.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "assert/mod.ts"; 2 | import { desugar } from "./desugar.ts"; 3 | import { Kind, PrimitiveType } from "./ast.ts"; 4 | 5 | Deno.test("ast/desugar, desugar primitive", () => { 6 | assertEquals(desugar(null), [Kind.Primitive, PrimitiveType.Null]); 7 | assertEquals(desugar(undefined), [Kind.Primitive, PrimitiveType.Undefined]); 8 | assertEquals(desugar(String), [Kind.Primitive, PrimitiveType.String]); 9 | assertEquals(desugar(Number), [Kind.Primitive, PrimitiveType.Number]); 10 | assertEquals(desugar(Boolean), [Kind.Primitive, PrimitiveType.Boolean]); 11 | assertEquals(desugar(BigInt), [Kind.Primitive, PrimitiveType.BigInt]); 12 | assertEquals(desugar(Symbol), [Kind.Primitive, PrimitiveType.Symbol]); 13 | }); 14 | 15 | Deno.test("ast/desugar, desugar literal", () => { 16 | assertEquals(desugar("foo"), [Kind.Literal, "foo"]); 17 | assertEquals(desugar(30), [Kind.Literal, 30]); 18 | assertEquals(desugar(true), [Kind.Literal, true]); 19 | assertEquals(desugar(false), [Kind.Literal, false]); 20 | assertEquals(desugar(10n), [Kind.Literal, 10n]); 21 | }); 22 | 23 | Deno.test("ast/desugar, desugar array", () => { 24 | assertEquals( 25 | desugar(Array), 26 | [Kind.Array, [Kind.Primitive, PrimitiveType.Any]], 27 | ); 28 | assertEquals( 29 | desugar([String]), 30 | [Kind.Array, [Kind.Primitive, PrimitiveType.String]], 31 | ); 32 | assertEquals( 33 | desugar([Kind.Array, String]), 34 | [Kind.Array, [Kind.Primitive, PrimitiveType.String]], 35 | ); 36 | }); 37 | 38 | Deno.test("ast/desugar, desugar object", () => { 39 | assertEquals( 40 | desugar({ 41 | name: String, 42 | age: Number, 43 | }), 44 | [Kind.Object, { 45 | name: [Kind.Primitive, PrimitiveType.String], 46 | age: [Kind.Primitive, PrimitiveType.Number], 47 | }], 48 | ); 49 | assertEquals( 50 | desugar([Kind.Object, { 51 | name: String, 52 | age: Number, 53 | }]), 54 | [Kind.Object, { 55 | name: [Kind.Primitive, PrimitiveType.String], 56 | age: [Kind.Primitive, PrimitiveType.Number], 57 | }], 58 | ); 59 | }); 60 | -------------------------------------------------------------------------------- /ast/desugar.ts: -------------------------------------------------------------------------------- 1 | import { Ast, AstStrict, Kind, PrimitiveType } from "./ast.ts"; 2 | 3 | export function desugar(ast: Ast): AstStrict { 4 | switch (ast) { 5 | case null: 6 | return [Kind.Primitive, PrimitiveType.Null]; 7 | case undefined: 8 | return [Kind.Primitive, PrimitiveType.Undefined]; 9 | case String: 10 | return [Kind.Primitive, PrimitiveType.String]; 11 | case Number: 12 | return [Kind.Primitive, PrimitiveType.Number]; 13 | case Boolean: 14 | return [Kind.Primitive, PrimitiveType.Boolean]; 15 | case BigInt: 16 | return [Kind.Primitive, PrimitiveType.BigInt]; 17 | case Symbol: 18 | return [Kind.Primitive, PrimitiveType.Symbol]; 19 | case Array: 20 | return [Kind.Array, [Kind.Primitive, PrimitiveType.Any]]; 21 | } 22 | switch (typeof ast) { 23 | case "string": 24 | return [Kind.Literal, ast]; 25 | case "number": 26 | return [Kind.Literal, ast]; 27 | case "boolean": 28 | return [Kind.Literal, ast]; 29 | case "bigint": 30 | return [Kind.Literal, ast]; 31 | } 32 | if (Array.isArray(ast)) { 33 | if (ast.length === 1) { 34 | return [Kind.Array, desugar(ast[0])]; 35 | } 36 | switch (ast[0]) { 37 | case Kind.Array: 38 | return [Kind.Array, desugar(ast[1])]; 39 | case Kind.Object: 40 | return desugar(ast[1]); 41 | case Kind.Union: 42 | return [Kind.Union, ast[1].map(desugar)]; 43 | case Kind.Decorator: 44 | return [Kind.Decorator, desugar(ast[1]), ast[2]]; 45 | } 46 | return ast; 47 | } 48 | if (typeof ast === "object") { 49 | const obj: Record = {}; 50 | for (const key in ast) { 51 | obj[key] = desugar(ast[key]); 52 | } 53 | return [Kind.Object, obj]; 54 | } 55 | throw new Error(".."); 56 | } 57 | -------------------------------------------------------------------------------- /ast/estimate_type.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "assert/mod.ts"; 2 | import type { Equal, Expect } from "@type-challenges/utils"; 3 | import { type EstimateType } from "./estimate_type.ts"; 4 | import { Kind } from "./ast.ts"; 5 | 6 | type TestPrimitiveTypes = [ 7 | Expect, string>>, 8 | Expect, number>>, 9 | Expect, boolean>>, 10 | Expect, bigint>>, 11 | Expect, symbol>>, 12 | Expect, null>>, 13 | Expect, undefined>>, 14 | ]; 15 | 16 | type TestScalarValueTypes = [ 17 | Expect, "something">>, 18 | Expect, 30>>, 19 | Expect, true>>, 20 | Expect, false>>, 21 | Expect, 1n>>, 22 | ]; 23 | 24 | type Point = { x: typeof Number; y: typeof Number }; 25 | type TestArray = [ 26 | Expect< 27 | Equal< 28 | EstimateType, 29 | // deno-lint-ignore no-explicit-any 30 | any[] 31 | > 32 | >, 33 | Expect< 34 | Equal< 35 | EstimateType<[Kind.Array, typeof String]>, 36 | string[] 37 | > 38 | >, 39 | ]; 40 | type TestObject = [ 41 | Expect, { foo: string }>>, 42 | Expect< 43 | Equal< 44 | EstimateType<{ start: Point; end: Point }>, 45 | { start: { x: number; y: number }; end: { x: number; y: number } } 46 | > 47 | >, 48 | ]; 49 | 50 | type TestUnion = [ 51 | Expect< 52 | Equal< 53 | EstimateType<[Kind.Union, (typeof String | typeof Number)[]]>, 54 | string | number 55 | > 56 | >, 57 | Expect< 58 | Equal< 59 | EstimateType< 60 | { hello: [Kind.Union, (typeof String | typeof Number | Point)[]] } 61 | >, 62 | { hello: string | number | { x: number; y: number } } 63 | > 64 | >, 65 | ]; 66 | 67 | type TestDecorator = [ 68 | // with scalar 69 | Expect< 70 | Equal, string> 71 | >, 72 | // with object 73 | Expect< 74 | Equal< 75 | EstimateType<[Kind.Decorator, { foo: typeof String }, []]>, 76 | { foo: string } 77 | > 78 | >, 79 | // with array 80 | Expect< 81 | Equal< 82 | EstimateType<[Kind.Decorator, [Kind.Array, typeof String], []]>, 83 | string[] 84 | > 85 | >, 86 | // with or 87 | Expect< 88 | Equal< 89 | EstimateType< 90 | [ 91 | Kind.Decorator, 92 | { hello: [Kind.Union, (typeof String | typeof Number | Point)[]] }, 93 | [], 94 | ] 95 | >, 96 | { hello: string | number | { x: number; y: number } } 97 | > 98 | >, 99 | ]; 100 | 101 | Deno.test("ast/estimate_type", () => { 102 | assertEquals(true, true); 103 | }); 104 | -------------------------------------------------------------------------------- /ast/estimate_type.ts: -------------------------------------------------------------------------------- 1 | // deno-lint-ignore-file no-explicit-any 2 | 3 | import { 4 | Ast, 5 | AstArray, 6 | AstDecorator, 7 | AstLiteral, 8 | AstObject, 9 | AstPrimitive, 10 | AstSugarArray, 11 | AstSugarLiteral, 12 | AstSugarObject, 13 | AstUnion, 14 | PrimitiveType, 15 | } from "./ast.ts"; 16 | 17 | export interface PrimitiveTypeMap { 18 | [PrimitiveType.Any]: any; 19 | [PrimitiveType.Null]: null; 20 | [PrimitiveType.Undefined]: undefined; 21 | [PrimitiveType.String]: string; 22 | [PrimitiveType.Number]: number; 23 | [PrimitiveType.Boolean]: boolean; 24 | [PrimitiveType.BigInt]: bigint; 25 | [PrimitiveType.Symbol]: symbol; 26 | } 27 | 28 | export type EstimateType = T extends StringConstructor ? string 29 | : T extends NumberConstructor ? number 30 | : T extends BooleanConstructor ? boolean 31 | : T extends BigIntConstructor ? bigint 32 | : T extends SymbolConstructor ? symbol 33 | : T extends ArrayConstructor ? any[] // anyarray 34 | : T extends infer U extends (null | undefined) ? U 35 | : T extends infer U extends AstSugarLiteral ? U 36 | : T extends AstSugarObject ? { [K in keyof T]: EstimateType } 37 | : T extends AstSugarArray ? EstimateType[] 38 | : T extends AstPrimitive ? PrimitiveTypeMap[T[1]] 39 | : T extends AstLiteral ? T[1] 40 | : T extends AstArray ? EstimateType[] 41 | : T extends AstObject ? { [K in keyof T[1]]: EstimateType } 42 | : T extends AstUnion ? EstimateType 43 | : T extends AstDecorator ? EstimateType 44 | : never; 45 | -------------------------------------------------------------------------------- /ast/utils.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | import { 3 | Ast, 4 | AstArray, 5 | AstDecorator, 6 | AstPrimitive, 7 | AstUnion, 8 | Kind, 9 | PrimitiveType, 10 | } from "./ast.ts"; 11 | import { EstimateType } from "./estimate_type.ts"; 12 | 13 | export function any(): AstPrimitive { 14 | return [Kind.Primitive, PrimitiveType.Any]; 15 | } 16 | 17 | export function or(types: T[]): AstUnion { 18 | return [Kind.Union, types]; 19 | } 20 | 21 | export function union(types: T[]): AstUnion { 22 | return [Kind.Union, types]; 23 | } 24 | 25 | export function array(of: T): AstArray { 26 | return [Kind.Array, of]; 27 | } 28 | 29 | export function decorate( 30 | of: T, 31 | decorator: Decorator>, 32 | ): AstDecorator; 33 | export function decorate( 34 | of: T, 35 | decorators: Decorator>[], 36 | ): AstDecorator; 37 | export function decorate( 38 | of: T, 39 | decorator: Decorator> | Decorator< 40 | EstimateType 41 | >[], 42 | ): AstDecorator { 43 | return [ 44 | Kind.Decorator, 45 | of, 46 | Array.isArray(decorator) ? decorator : [decorator], 47 | ]; 48 | } 49 | 50 | export function optional( 51 | of: T, 52 | ): AstUnion { 53 | return [Kind.Union, [undefined, of]]; 54 | } 55 | -------------------------------------------------------------------------------- /decorator/decorator.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals } from "assert/mod.ts"; 2 | 3 | import { Decorator } from "./decorator.ts"; 4 | 5 | type IsSubset = T extends F ? true : false; 6 | 7 | function is() { 8 | return true; 9 | } 10 | 11 | function isNot() { 12 | return true; 13 | } 14 | 15 | Deno.test("decorator/decorator, type check", () => { 16 | const v1 = { name: "custom" }; 17 | 18 | assertEquals(is>>(), true); 19 | 20 | const v2 = () => {}; 21 | assertEquals(isNot>>(), true); 22 | 23 | const v3 = "string"; 24 | assertEquals(isNot>>(), true); 25 | }); 26 | -------------------------------------------------------------------------------- /decorator/decorator.ts: -------------------------------------------------------------------------------- 1 | export interface Decorator { 2 | name: string; 3 | cast?(v: unknown): T; 4 | validate?(v: T): boolean; 5 | transform?(v: T): T; 6 | apply?: never; 7 | call?: never; 8 | bind?: never; 9 | } 10 | -------------------------------------------------------------------------------- /decorators.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrows } from "assert/mod.ts"; 2 | import { d } from "./decorators.ts"; 3 | import { decorate, union } from "./ast/utils.ts"; 4 | import { createSanitize } from "./validator/create_sanitize.ts"; 5 | import { InvalidValueError } from "./validator/invalid_value_error.ts"; 6 | 7 | const { 8 | alpha, 9 | alphanum, 10 | ascii, 11 | base64, 12 | between, 13 | ceil, 14 | creditcard, 15 | dateformat, 16 | email, 17 | emptyToNull, 18 | floor, 19 | hexcolor, 20 | ip, 21 | json, 22 | length, 23 | lengthBetween, 24 | lengthMax, 25 | lengthMin, 26 | lowercase, 27 | macaddress, 28 | max, 29 | min, 30 | port, 31 | re, 32 | round, 33 | stringify, 34 | toLower, 35 | toUpper, 36 | trim, 37 | uppercase, 38 | url, 39 | uuid, 40 | } = d; 41 | 42 | Deno.test("decorators, alpha", () => { 43 | const s = createSanitize(decorate(String, alpha())); 44 | 45 | assertEquals(s("abcdefghijklmnopqrstuvwxyz"), "abcdefghijklmnopqrstuvwxyz"); 46 | 47 | const e = assertThrows( 48 | () => { 49 | s("1"); 50 | }, 51 | InvalidValueError, 52 | "This is an invalid value from decorator.", 53 | ); 54 | assertEquals(e.reason, "#alpha"); 55 | }); 56 | 57 | Deno.test("decorators, alphanum", () => { 58 | const s = createSanitize(decorate(String, alphanum())); 59 | 60 | assertEquals(s("abcdefghijklmnopqrstuvwxyz1"), "abcdefghijklmnopqrstuvwxyz1"); 61 | assertEquals(s("1"), "1"); 62 | 63 | const e = assertThrows( 64 | () => { 65 | s("äbc1"); 66 | }, 67 | InvalidValueError, 68 | "This is an invalid value from decorator.", 69 | ); 70 | assertEquals(e.reason, "#alphanum"); 71 | }); 72 | 73 | Deno.test("decorators, ascii", () => { 74 | const s = createSanitize(decorate(String, ascii())); 75 | 76 | assertEquals(s("abcdefghijklmnopqrstuvwxyz"), "abcdefghijklmnopqrstuvwxyz"); 77 | assertEquals(s("0123456789"), "0123456789"); 78 | assertEquals(s("!@#$%^&*()"), "!@#$%^&*()"); 79 | 80 | const e = assertThrows( 81 | () => { 82 | s("äbc"); 83 | }, 84 | InvalidValueError, 85 | "This is an invalid value from decorator.", 86 | ); 87 | assertEquals(e.reason, "#ascii"); 88 | }); 89 | 90 | Deno.test("decorators, base64", () => { 91 | const s = createSanitize(decorate(String, base64())); 92 | 93 | assertEquals(s("Zg=="), "Zg=="); 94 | 95 | const e = assertThrows( 96 | () => { 97 | s("Zg="); 98 | }, 99 | InvalidValueError, 100 | "This is an invalid value from decorator.", 101 | ); 102 | assertEquals(e.reason, "#base64"); 103 | }); 104 | 105 | Deno.test("decorators, between", () => { 106 | const s1 = createSanitize(decorate(Number, between(2, 3))); 107 | assertEquals(s1(2), 2); 108 | assertEquals(s1(3), 3); 109 | 110 | { 111 | const e = assertThrows( 112 | () => { 113 | s1(1); 114 | }, 115 | InvalidValueError, 116 | "This is an invalid value from decorator.", 117 | ); 118 | assertEquals(e.reason, "#between"); 119 | assertThrows( 120 | () => { 121 | s1(4); 122 | }, 123 | InvalidValueError, 124 | "This is an invalid value from decorator.", 125 | ); 126 | } 127 | }); 128 | 129 | Deno.test("decorators, ceil", () => { 130 | const s1 = createSanitize(decorate(Number, ceil())); 131 | 132 | assertEquals(s1(2.1), 3); 133 | assertEquals(s1(2.9), 3); 134 | 135 | assertEquals(s1(-2.1), -2); 136 | assertEquals(s1(-2.9), -2); 137 | }); 138 | 139 | Deno.test("decorators, creditcard", () => { 140 | const s = createSanitize(decorate(String, creditcard())); 141 | 142 | assertEquals(s("4716-2210-5188-5662"), "4716-2210-5188-5662"); 143 | assertEquals(s("4929 7226 5379 7141"), "4929 7226 5379 7141"); 144 | 145 | const e = assertThrows( 146 | () => { 147 | s("5398228707871528"); 148 | }, 149 | InvalidValueError, 150 | "This is an invalid value from decorator.", 151 | ); 152 | assertEquals(e.reason, "#creditcard"); 153 | }); 154 | 155 | Deno.test("decorators, dateformat", () => { 156 | const s = createSanitize(decorate(String, dateformat())); 157 | 158 | assertEquals(s("2018-12-25"), "2018-12-25"); 159 | assertEquals(s("12/25/2018"), "12/25/2018"); 160 | assertEquals(s("Dec 25, 2018"), "Dec 25, 2018"); 161 | 162 | const e = assertThrows( 163 | () => { 164 | s("1539043200000"); 165 | }, 166 | InvalidValueError, 167 | "This is an invalid value from decorator.", 168 | ); 169 | assertEquals(e.reason, "#dateformat"); 170 | }); 171 | 172 | Deno.test("decorators, email", () => { 173 | const s = createSanitize(decorate(String, email())); 174 | 175 | assertEquals(s("wan2land+en@gmail.com"), "wan2land+en@gmail.com"); 176 | const e = assertThrows( 177 | () => { 178 | s("unknown"); 179 | }, 180 | InvalidValueError, 181 | "This is an invalid value from decorator.", 182 | ); 183 | assertEquals(e.reason, "#email"); 184 | }); 185 | 186 | Deno.test("decorators, emptyToNull", () => { 187 | const s = createSanitize(decorate(union([String, null]), emptyToNull())); 188 | 189 | assertEquals(s("empty"), "empty"); 190 | assertEquals(s(""), null); 191 | assertEquals(s(null), null); 192 | }); 193 | 194 | Deno.test("decorators, floor", () => { 195 | const s1 = createSanitize(decorate(Number, floor())); 196 | 197 | assertEquals(s1(2.1), 2); 198 | assertEquals(s1(2.9), 2); 199 | 200 | assertEquals(s1(-2.1), -3); 201 | assertEquals(s1(-2.9), -3); 202 | }); 203 | 204 | Deno.test("decorators, hexcolor", () => { 205 | const s = createSanitize(decorate(String, hexcolor())); 206 | 207 | assertEquals(s("#CCCCCC"), "#CCCCCC"); 208 | const e = assertThrows( 209 | () => { 210 | s("#ff"); 211 | }, 212 | InvalidValueError, 213 | "This is an invalid value from decorator.", 214 | ); 215 | assertEquals(e.reason, "#hexcolor"); 216 | }); 217 | 218 | Deno.test("decorators, ip", () => { 219 | const s1 = createSanitize(decorate(String, ip())); 220 | 221 | assertEquals(s1("127.0.0.1"), "127.0.0.1"); 222 | assertEquals(s1("2001:db8:0000:1:1:1:1:1"), "2001:db8:0000:1:1:1:1:1"); 223 | { 224 | const e = assertThrows( 225 | () => { 226 | s1("256.0.0.0"); 227 | }, 228 | InvalidValueError, 229 | "This is an invalid value from decorator.", 230 | ); 231 | assertEquals(e.reason, "#ip"); 232 | } 233 | 234 | const s2 = createSanitize(decorate(String, ip("v4"))); 235 | 236 | assertEquals(s2("127.0.0.1"), "127.0.0.1"); 237 | { 238 | const e = assertThrows( 239 | () => { 240 | s2("256.0.0.0"); 241 | }, 242 | InvalidValueError, 243 | "This is an invalid value from decorator.", 244 | ); 245 | assertEquals(e.reason, "#ip"); 246 | assertThrows( 247 | () => { 248 | s2("2001:db8:0000:1:1:1:1:1"); 249 | }, 250 | InvalidValueError, 251 | "This is an invalid value from decorator.", 252 | ); 253 | } 254 | 255 | const s3 = createSanitize(decorate(String, ip("v6"))); 256 | 257 | assertEquals(s3("2001:db8:0000:1:1:1:1:1"), "2001:db8:0000:1:1:1:1:1"); 258 | { 259 | const e = assertThrows( 260 | () => { 261 | s3("256.0.0.0"); 262 | }, 263 | InvalidValueError, 264 | "This is an invalid value from decorator.", 265 | ); 266 | assertEquals(e.reason, "#ip"); 267 | assertThrows( 268 | () => { 269 | s3("127.0.0.1"); 270 | }, 271 | InvalidValueError, 272 | "This is an invalid value from decorator.", 273 | ); 274 | } 275 | }); 276 | 277 | Deno.test("decorators, json", () => { 278 | const s = createSanitize(decorate(String, json())); 279 | 280 | assertEquals(s("{}"), "{}"); 281 | const e = assertThrows( 282 | () => { 283 | s("a"); 284 | }, 285 | InvalidValueError, 286 | "This is an invalid value from decorator.", 287 | ); 288 | assertEquals(e.reason, "#json"); 289 | }); 290 | 291 | Deno.test("decorators, lengthBetween", () => { 292 | const s1 = createSanitize(decorate(String, lengthBetween(2, 3))); 293 | assertEquals(s1("ab"), "ab"); 294 | assertEquals(s1("abc"), "abc"); 295 | 296 | { 297 | const e = assertThrows( 298 | () => { 299 | s1("a"); 300 | }, 301 | InvalidValueError, 302 | "This is an invalid value from decorator.", 303 | ); 304 | assertEquals(e.reason, "#lengthBetween"); 305 | assertThrows( 306 | () => { 307 | s1("abcd"); 308 | }, 309 | InvalidValueError, 310 | "This is an invalid value from decorator.", 311 | ); 312 | } 313 | 314 | const s2 = createSanitize(decorate(Array, lengthBetween(2, 3))); 315 | assertEquals(s2([1, 2]), [1, 2]); 316 | assertEquals(s2([1, 2, 3]), [1, 2, 3]); 317 | 318 | { 319 | const e = assertThrows( 320 | () => { 321 | s2([1]); 322 | }, 323 | InvalidValueError, 324 | "This is an invalid value from decorator.", 325 | ); 326 | assertEquals(e.reason, "#lengthBetween"); 327 | assertThrows( 328 | () => { 329 | s2([1, 2, 3, 4]); 330 | }, 331 | InvalidValueError, 332 | "This is an invalid value from decorator.", 333 | ); 334 | } 335 | }); 336 | 337 | Deno.test("decorators, lengthMax", () => { 338 | const s = createSanitize(decorate(String, lengthMax(3))); 339 | assertEquals(s("a"), "a"); 340 | assertEquals(s("ab"), "ab"); 341 | assertEquals(s("abc"), "abc"); 342 | 343 | const e = assertThrows( 344 | () => { 345 | s("abcd"); 346 | }, 347 | InvalidValueError, 348 | "This is an invalid value from decorator.", 349 | ); 350 | assertEquals(e.reason, "#lengthMax"); 351 | }); 352 | 353 | Deno.test("decorators, lengthMin", () => { 354 | const s = createSanitize(decorate(String, lengthMin(2))); 355 | assertEquals(s("ab"), "ab"); 356 | assertEquals(s("abc"), "abc"); 357 | 358 | const e = assertThrows( 359 | () => { 360 | s("a"); 361 | }, 362 | InvalidValueError, 363 | "This is an invalid value from decorator.", 364 | ); 365 | assertEquals(e.reason, "#lengthMin"); 366 | }); 367 | 368 | Deno.test("decorators, length", () => { 369 | const s = createSanitize(decorate(String, length(2))); 370 | assertEquals(s("ab"), "ab"); 371 | 372 | const e = assertThrows( 373 | () => { 374 | s("a"); 375 | }, 376 | InvalidValueError, 377 | "This is an invalid value from decorator.", 378 | ); 379 | assertEquals(e.reason, "#length"); 380 | assertThrows( 381 | () => { 382 | s("abcd"); 383 | }, 384 | InvalidValueError, 385 | "This is an invalid value from decorator.", 386 | ); 387 | }); 388 | 389 | Deno.test("decorators, lowercase", () => { 390 | const s = createSanitize(decorate(String, lowercase())); 391 | 392 | assertEquals(s("abcd"), "abcd"); 393 | 394 | const e = assertThrows( 395 | () => { 396 | s("ABCD"); 397 | }, 398 | InvalidValueError, 399 | "This is an invalid value from decorator.", 400 | ); 401 | assertEquals(e.reason, "#lowercase"); 402 | }); 403 | 404 | Deno.test("decorators, macaddress", () => { 405 | const s = createSanitize(decorate(String, macaddress())); 406 | 407 | assertEquals(s("ab:ab:ab:ab:ab:ab"), "ab:ab:ab:ab:ab:ab"); 408 | 409 | const e = assertThrows( 410 | () => { 411 | s("01:02:03:04:05"); 412 | }, 413 | InvalidValueError, 414 | "This is an invalid value from decorator.", 415 | ); 416 | assertEquals(e.reason, "#macaddress"); 417 | }); 418 | 419 | Deno.test("decorators, max", () => { 420 | const s = createSanitize(decorate(Number, max(2))); 421 | 422 | assertEquals(s(2), 2); 423 | 424 | const e = assertThrows( 425 | () => { 426 | s(3); 427 | }, 428 | InvalidValueError, 429 | "This is an invalid value from decorator.", 430 | ); 431 | assertEquals(e.reason, "#max"); 432 | }); 433 | 434 | Deno.test("decorators, min", () => { 435 | const s = createSanitize(decorate(Number, min(2))); 436 | 437 | assertEquals(s(2), 2); 438 | 439 | const e = assertThrows( 440 | () => { 441 | s(1); 442 | }, 443 | InvalidValueError, 444 | "This is an invalid value from decorator.", 445 | ); 446 | assertEquals(e.reason, "#min"); 447 | }); 448 | 449 | Deno.test("decorators, port", () => { 450 | const s = createSanitize(decorate(Number, port())); 451 | 452 | assertEquals(s(0), 0); 453 | assertEquals(s(1), 1); 454 | assertEquals(s(65534), 65534); 455 | assertEquals(s(65535), 65535); 456 | 457 | const e = assertThrows( 458 | () => { 459 | s(65536); 460 | }, 461 | InvalidValueError, 462 | "This is an invalid value from decorator.", 463 | ); 464 | assertEquals(e.reason, "#port"); 465 | }); 466 | 467 | Deno.test("decorators, re", () => { 468 | const s = createSanitize(decorate(String, re(/^abc?$/i))); 469 | 470 | assertEquals(s("abc"), "abc"); 471 | 472 | const e = assertThrows( 473 | () => { 474 | s("github"); 475 | }, 476 | InvalidValueError, 477 | "This is an invalid value from decorator.", 478 | ); 479 | assertEquals(e.reason, "#re"); 480 | }); 481 | 482 | Deno.test("decorators, round", () => { 483 | const s1 = createSanitize(decorate(Number, round())); 484 | 485 | assertEquals(s1(2.1), 2); 486 | assertEquals(s1(2.9), 3); 487 | 488 | assertEquals(s1(-2.1), -2); 489 | assertEquals(s1(-2.9), -3); 490 | }); 491 | 492 | Deno.test("decorators, stringify", () => { 493 | const s = createSanitize(decorate(String, stringify())); 494 | 495 | assertEquals(s("abcdef"), "abcdef"); 496 | assertEquals(s(3030), "3030"); 497 | assertEquals(s(123n), "123"); 498 | assertEquals(s(true), "true"); 499 | assertEquals(s(false), "false"); 500 | 501 | // object, JSON.stringify 502 | assertEquals(s({ foo: "wow" }), '{"foo":"wow"}'); 503 | assertEquals(s([{ foo: "wow" }]), '[{"foo":"wow"}]'); 504 | }); 505 | 506 | Deno.test("decorators, toLower", () => { 507 | const s = createSanitize(decorate(String, toLower())); 508 | 509 | assertEquals(s("aBcDeF"), "abcdef"); 510 | }); 511 | 512 | Deno.test("decorators, toUpper", () => { 513 | const s = createSanitize(decorate(String, toUpper())); 514 | 515 | assertEquals(s("aBcDeF"), "ABCDEF"); 516 | }); 517 | 518 | Deno.test("decorators, trim", () => { 519 | const s = createSanitize(decorate(String, trim())); 520 | 521 | assertEquals(s(" abcd\n\n\r\t"), "abcd"); 522 | }); 523 | 524 | Deno.test("decorators, uppercase", () => { 525 | const s = createSanitize(decorate(String, uppercase())); 526 | 527 | assertEquals(s("ABCD"), "ABCD"); 528 | 529 | const e = assertThrows( 530 | () => { 531 | s("abcd"); 532 | }, 533 | InvalidValueError, 534 | "This is an invalid value from decorator.", 535 | ); 536 | assertEquals(e.reason, "#uppercase"); 537 | }); 538 | 539 | Deno.test("decorators, url", () => { 540 | const s = createSanitize(decorate(String, url())); 541 | 542 | assertEquals( 543 | s("http://github.com/corgidisco"), 544 | "http://github.com/corgidisco", 545 | ); 546 | assertEquals(s("https://github.com"), "https://github.com"); 547 | 548 | const e = assertThrows( 549 | () => { 550 | s("github"); 551 | }, 552 | InvalidValueError, 553 | "This is an invalid value from decorator.", 554 | ); 555 | assertEquals(e.reason, "#url"); 556 | }); 557 | 558 | Deno.test("decorators, uuid", () => { 559 | const s = createSanitize(decorate(String, uuid())); 560 | 561 | assertEquals( 562 | s("A987FBC9-4BED-3078-CF07-9141BA07C9F3"), 563 | "A987FBC9-4BED-3078-CF07-9141BA07C9F3", 564 | ); 565 | const e = assertThrows( 566 | () => { 567 | s("xxxA987FBC9-4BED-3078-CF07-9141BA07C9F3"); 568 | }, 569 | InvalidValueError, 570 | "This is an invalid value from decorator.", 571 | ); 572 | assertEquals(e.reason, "#uuid"); 573 | }); 574 | -------------------------------------------------------------------------------- /decorators.ts: -------------------------------------------------------------------------------- 1 | import { alpha } from "./decorators/alpha.ts"; 2 | import { alphanum } from "./decorators/alphanum.ts"; 3 | import { ascii } from "./decorators/ascii.ts"; 4 | import { base64 } from "./decorators/base64.ts"; 5 | import { between } from "./decorators/between.ts"; 6 | import { ceil } from "./decorators/ceil.ts"; 7 | import { creditcard } from "./decorators/creditcard.ts"; 8 | import { dateformat } from "./decorators/dateformat.ts"; 9 | import { email } from "./decorators/email.ts"; 10 | import { emptyToNull } from "./decorators/empty_to_null.ts"; 11 | import { floor } from "./decorators/floor.ts"; 12 | import { hexcolor } from "./decorators/hexcolor.ts"; 13 | import { ip } from "./decorators/ip.ts"; 14 | import { json } from "./decorators/json.ts"; 15 | import { lengthBetween } from "./decorators/length_between.ts"; 16 | import { lengthMax } from "./decorators/length_max.ts"; 17 | import { lengthMin } from "./decorators/length_min.ts"; 18 | import { length } from "./decorators/length.ts"; 19 | import { lowercase } from "./decorators/lowercase.ts"; 20 | import { macaddress } from "./decorators/macaddress.ts"; 21 | import { min } from "./decorators/min.ts"; 22 | import { max } from "./decorators/max.ts"; 23 | import { port } from "./decorators/port.ts"; 24 | import { re } from "./decorators/re.ts"; 25 | import { round } from "./decorators/round.ts"; 26 | import { stringify } from "./decorators/stringify.ts"; 27 | import { toLower } from "./decorators/to_lower.ts"; 28 | import { toUpper } from "./decorators/to_upper.ts"; 29 | import { trim } from "./decorators/trim.ts"; 30 | import { uppercase } from "./decorators/uppercase.ts"; 31 | import { url } from "./decorators/url.ts"; 32 | import { uuid } from "./decorators/uuid.ts"; 33 | 34 | export const d = { 35 | alpha, 36 | alphanum, 37 | ascii, 38 | base64, 39 | between, 40 | ceil, 41 | creditcard, 42 | dateformat, 43 | email, 44 | emptyToNull, 45 | floor, 46 | hexcolor, 47 | ip, 48 | json, 49 | lengthBetween, 50 | lengthMax, 51 | lengthMin, 52 | length, 53 | lowercase, 54 | macaddress, 55 | min, 56 | max, 57 | port, 58 | re, 59 | round, 60 | stringify, 61 | toLower, 62 | toUpper, 63 | trim, 64 | uppercase, 65 | url, 66 | uuid, 67 | }; 68 | 69 | export type PredefinedDecorators = typeof d; 70 | -------------------------------------------------------------------------------- /decorators/alpha.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const re = /^[a-z]+$/i; 4 | const decorator: Decorator = { 5 | name: "alpha", 6 | validate(v) { 7 | return re.test(v); 8 | }, 9 | }; 10 | 11 | export function alpha(): Decorator { 12 | return decorator; 13 | } 14 | -------------------------------------------------------------------------------- /decorators/alphanum.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const re = /^[a-z0-9]+$/i; 4 | const decorator: Decorator = { 5 | name: "alphanum", 6 | validate(v) { 7 | return re.test(v); 8 | }, 9 | }; 10 | 11 | export function alphanum(): Decorator { 12 | return decorator; 13 | } 14 | -------------------------------------------------------------------------------- /decorators/ascii.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | // deno-lint-ignore no-control-regex 4 | const re = /^[\x00-\x7F]+$/; 5 | const decorator: Decorator = { 6 | name: "ascii", 7 | validate(v) { 8 | return re.test(v); 9 | }, 10 | }; 11 | 12 | export function ascii(): Decorator { 13 | return decorator; 14 | } 15 | -------------------------------------------------------------------------------- /decorators/base64.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "base64", 5 | validate(v) { 6 | const l = v.length; 7 | if (!l || l % 4 !== 0 || /[^A-Z0-9+\\/=]/i.test(v)) return false; 8 | const index = v.indexOf("="); 9 | return index === -1 || index === l - 1 || 10 | (index === l - 2 && v[l - 1] === "="); 11 | }, 12 | }; 13 | 14 | export function base64(): Decorator { 15 | return decorator; 16 | } 17 | -------------------------------------------------------------------------------- /decorators/between.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export function between(min: number, max: number): Decorator; 4 | export function between(min: string, max: string): Decorator; 5 | export function between( 6 | min: number | string, 7 | max: number | string, 8 | ): Decorator { 9 | return { 10 | name: "between", 11 | validate(v) { 12 | return v >= min && v <= max; 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /decorators/ceil.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "ceil", 5 | transform(v) { 6 | return Math.ceil(v); 7 | }, 8 | }; 9 | export function ceil(): Decorator { 10 | return decorator; 11 | } 12 | -------------------------------------------------------------------------------- /decorators/creditcard.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const re = 4 | /^(?:4\d{12}(?:\d{3})?|5[1-5]\d{14}|(222[1-9]|22[3-9]\d|2[3-6]\d{2}|27[01]\d|2720)\d{12}|6(?:011|5\d\d)\d{12}|3[47]\d{13}|3(?:0[0-5]|[68]\d)\d{11}|(?:2131|1800|35\d{3})\d{11}|6[27]\d{14})$/; 5 | 6 | const decorator: Decorator = { 7 | name: "creditcard", 8 | validate(v) { 9 | v = v.replace(/\D+/g, ""); 10 | if (!re.test(v)) return false; 11 | let sum = 0; 12 | let check = false; 13 | for (let i = v.length - 1; i >= 0; i--) { 14 | let tmp = parseInt(v.charAt(i), 10); 15 | if (check) { 16 | tmp *= 2; 17 | if (tmp >= 10) sum += (tmp % 10) + 1; 18 | else sum += tmp; 19 | } else { 20 | sum += tmp; 21 | } 22 | check = !check; 23 | } 24 | return !!((sum % 10) === 0 ? v : false); 25 | }, 26 | }; 27 | 28 | export function creditcard(): Decorator { 29 | return decorator; 30 | } 31 | -------------------------------------------------------------------------------- /decorators/dateformat.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "dateformat", 5 | validate(v) { 6 | return !Number.isNaN(Date.parse(v)); 7 | }, 8 | }; 9 | 10 | export function dateformat(): Decorator { 11 | return decorator; 12 | } 13 | -------------------------------------------------------------------------------- /decorators/email.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | /** 4 | * RFC 5322 5 | * @ref https://emailregex.com/ 6 | */ 7 | const re = 8 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 9 | const decorator: Decorator = { 10 | name: "email", 11 | validate(v: string) { 12 | return re.test(v); 13 | }, 14 | }; 15 | 16 | export function email(): Decorator { 17 | return decorator; 18 | } 19 | -------------------------------------------------------------------------------- /decorators/empty_to_null.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "emptyToNull", 5 | transform: (v) => v ? v : null, 6 | }; 7 | export function emptyToNull(): Decorator { 8 | return decorator; 9 | } 10 | -------------------------------------------------------------------------------- /decorators/floor.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "floor", 5 | transform(v) { 6 | return Math.floor(v); 7 | }, 8 | }; 9 | export function floor(): Decorator { 10 | return decorator; 11 | } 12 | -------------------------------------------------------------------------------- /decorators/hexcolor.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "hexcolor", 5 | validate(v) { 6 | return /^#?([0-9A-F]{3}|[0-9A-F]{6})$/i.test(v); 7 | }, 8 | }; 9 | 10 | export function hexcolor(): Decorator { 11 | return decorator; 12 | } 13 | -------------------------------------------------------------------------------- /decorators/ip.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export const re4 = 4 | /^((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3,3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)$/; 5 | export const re6 = 6 | /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3,3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3,3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d))$/; 7 | 8 | export function ip(version?: "v4" | "v6"): Decorator { 9 | if (version === "v4") { 10 | return { 11 | name: "ip", 12 | validate(v) { 13 | return re4.test(v); 14 | }, 15 | }; 16 | } 17 | if (version === "v6") { 18 | return { 19 | name: "ip", 20 | validate(v) { 21 | return re6.test(v); 22 | }, 23 | }; 24 | } 25 | return { 26 | name: "ip", 27 | validate(v) { 28 | return re4.test(v) || re6.test(v); 29 | }, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /decorators/json.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "json", 5 | validate(v) { 6 | try { 7 | JSON.parse(v); 8 | return true; 9 | } catch { 10 | return false; 11 | } 12 | }, 13 | }; 14 | 15 | export function json(): Decorator { 16 | return decorator; 17 | } 18 | -------------------------------------------------------------------------------- /decorators/length.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export function length(n: number): Decorator { 4 | return { 5 | name: "length", 6 | validate(v) { 7 | return v.length === n; 8 | }, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /decorators/length_between.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export function lengthBetween( 4 | min: number, 5 | max: number, 6 | ): Decorator { 7 | return { 8 | name: "lengthBetween", 9 | validate(v) { 10 | return typeof v.length === "number" && 11 | v.length >= min && 12 | v.length <= max; 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /decorators/length_max.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export function lengthMax(max: number): Decorator { 4 | return { 5 | name: "lengthMax", 6 | validate(v) { 7 | return typeof v.length === "number" && 8 | v.length <= max; 9 | }, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /decorators/length_min.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export function lengthMin(min: number): Decorator { 4 | return { 5 | name: "lengthMin", 6 | validate(v) { 7 | return typeof v.length === "number" && 8 | v.length >= min; 9 | }, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /decorators/lowercase.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export function lowercase(): Decorator { 4 | return { 5 | name: "lowercase", 6 | validate(v) { 7 | return v.toLowerCase() === v; 8 | }, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /decorators/macaddress.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const re = /^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$/; 4 | const decorator: Decorator = { 5 | name: "macaddress", 6 | validate(v) { 7 | return re.test(v); 8 | }, 9 | }; 10 | 11 | export function macaddress(): Decorator { 12 | return decorator; 13 | } 14 | -------------------------------------------------------------------------------- /decorators/max.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export function max(max: number): Decorator; 4 | export function max(max: string): Decorator; 5 | export function max(max: number | string): Decorator { 6 | return { 7 | name: "max", 8 | validate(v) { 9 | return v <= max; 10 | }, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /decorators/min.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export function min(min: number): Decorator; 4 | export function min(min: string): Decorator; 5 | export function min(min: number | string): Decorator { 6 | return { 7 | name: "min", 8 | validate(v) { 9 | return v >= min; 10 | }, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /decorators/port.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "port", 5 | validate(v) { 6 | return v >= 0 && v <= 65535; 7 | }, 8 | }; 9 | export function port(): Decorator { 10 | return decorator; 11 | } 12 | -------------------------------------------------------------------------------- /decorators/re.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export function re(re: RegExp): Decorator { 4 | return { 5 | name: "re", 6 | validate(v) { 7 | return re.test(v); 8 | }, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /decorators/round.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "round", 5 | transform(v) { 6 | return Math.round(v); 7 | }, 8 | }; 9 | export function round(): Decorator { 10 | return decorator; 11 | } 12 | -------------------------------------------------------------------------------- /decorators/stringify.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "stringify", 5 | cast: (v) => 6 | v == null ? "" : typeof v === "object" ? JSON.stringify(v) : `${v}`, 7 | }; 8 | 9 | export function stringify(): Decorator { 10 | return decorator; 11 | } 12 | -------------------------------------------------------------------------------- /decorators/to_lower.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "toLower", 5 | transform(v) { 6 | return v.toLowerCase(); 7 | }, 8 | }; 9 | export function toLower(): Decorator { 10 | return decorator; 11 | } 12 | -------------------------------------------------------------------------------- /decorators/to_upper.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "toUpper", 5 | transform(v) { 6 | return v.toUpperCase(); 7 | }, 8 | }; 9 | export function toUpper(): Decorator { 10 | return decorator; 11 | } 12 | -------------------------------------------------------------------------------- /decorators/trim.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "trim", 5 | transform: (v) => v.trim(), 6 | }; 7 | export function trim(): Decorator { 8 | return decorator; 9 | } 10 | -------------------------------------------------------------------------------- /decorators/uppercase.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "uppercase", 5 | validate(v) { 6 | return v.toUpperCase() === v; 7 | }, 8 | }; 9 | export function uppercase(): Decorator { 10 | return decorator; 11 | } 12 | -------------------------------------------------------------------------------- /decorators/url.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | const decorator: Decorator = { 4 | name: "url", 5 | validate(v) { 6 | try { 7 | new URL(v); 8 | return true; 9 | } catch { 10 | return false; 11 | } 12 | }, 13 | }; 14 | export function url(): Decorator { 15 | return decorator; 16 | } 17 | -------------------------------------------------------------------------------- /decorators/uuid.ts: -------------------------------------------------------------------------------- 1 | import { Decorator } from "../decorator/decorator.ts"; 2 | 3 | export const re = 4 | /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i; 5 | export const re3 = 6 | /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i; 7 | export const re4 = 8 | /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; 9 | export const re5 = 10 | /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i; 11 | 12 | // ref. https://github.com/chriso/validator.js/blob/master/src/lib/isUUID.js 13 | export function uuid(version?: "v3" | "v4" | "v5"): Decorator { 14 | if (version === "v3") { 15 | return { 16 | name: "uuid(v3)", 17 | validate(v) { 18 | return re3.test(v); 19 | }, 20 | }; 21 | } 22 | if (version === "v4") { 23 | return { 24 | name: "uuid(v4)", 25 | validate(v) { 26 | return re4.test(v); 27 | }, 28 | }; 29 | } 30 | if (version === "v5") { 31 | return { 32 | name: "uuid(v5)", 33 | validate(v) { 34 | return re5.test(v); 35 | }, 36 | }; 37 | } 38 | return { 39 | name: "uuid", 40 | validate(v) { 41 | return re.test(v); 42 | }, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "dnt/": "https://deno.land/x/dnt@0.40.0/", 4 | "assert/": "https://deno.land/std@0.220.1/assert/", 5 | "@type-challenges/utils": "npm:@type-challenges/utils@0.1.1/index.d.ts" 6 | }, 7 | "tasks": { 8 | "test": "deno task test:unit && deno task test:lint && deno task test:format && deno task test:types", 9 | "test:unit": "deno test -A --unstable", 10 | "test:lint": "deno lint --ignore=.npm", 11 | "test:format": "deno fmt --check --ignore=.npm", 12 | "test:types": "find . -name '*.ts' -not -path './.npm/*' | xargs deno check", 13 | "build:npm": "deno run -A scripts/build_npm.ts" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | export { any, array, decorate, optional, or, union } from "./ast/utils.ts"; 2 | export type { 3 | Ast, 4 | AstArray, 5 | AstDecorator, 6 | AstLiteral, 7 | AstObject, 8 | AstPrimitive, 9 | AstStrict, 10 | AstSugarAnyArray, 11 | AstSugarArray, 12 | AstSugarLiteral, 13 | AstSugarObject, 14 | AstSugarPrimitive, 15 | AstUnion, 16 | Kind, 17 | PrimitiveType, 18 | } from "./ast/ast.ts"; 19 | export type { EstimateType } from "./ast/estimate_type.ts"; 20 | export type { Decorator } from "./decorator/decorator.ts"; 21 | 22 | export { createSanitize } from "./validator/create_sanitize.ts"; 23 | export { createValidate } from "./validator/create_validate.ts"; 24 | export { InvalidValueError } from "./validator/invalid_value_error.ts"; 25 | 26 | export { d, type PredefinedDecorators } from "./decorators.ts"; 27 | export { type DecoratorFactory, s, v } from "./short.ts"; 28 | -------------------------------------------------------------------------------- /parser/syntax_error.ts: -------------------------------------------------------------------------------- 1 | function padStart(text: string, length: number): string { 2 | if (text.length > length) { 3 | return text; 4 | } 5 | length = length - text.length; 6 | return " ".repeat(length).slice(0, length) + text; 7 | } 8 | 9 | export class SyntaxError extends Error { 10 | public readonly code = "SYNTAX_ERROR"; 11 | 12 | public constructor( 13 | source: string, 14 | expected: string, 15 | received: string, 16 | public position: number, 17 | public line: number, 18 | public column: number, 19 | ) { 20 | super( 21 | `Syntax Error: ${ 22 | expected ? `expected ${expected}, ` : "" 23 | }unexpected token "${received}" (${line}:${column}) 24 | ${line}: ${source.split("\n")[line - 1]} 25 | ${padStart("^", column + 2 + line.toString().length)}`, 26 | ); 27 | this.name = "SyntaxError"; 28 | Object.setPrototypeOf(this, SyntaxError.prototype); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /scripts/build_npm.ts: -------------------------------------------------------------------------------- 1 | import { build, emptyDir } from "dnt/mod.ts"; 2 | 3 | const cmd = new Deno.Command(Deno.execPath(), { 4 | args: ["git", "describe", "--tags"], 5 | stdout: "piped", 6 | }); 7 | const { stdout } = await cmd.output(); 8 | const version = new TextDecoder().decode(stdout).trim(); 9 | 10 | await emptyDir("./.npm"); 11 | 12 | await build({ 13 | entryPoints: ["./mod.ts"], 14 | outDir: "./.npm", 15 | shims: { 16 | deno: false, 17 | }, 18 | test: false, 19 | compilerOptions: { 20 | lib: ["ES2021", "DOM"], 21 | }, 22 | package: { 23 | name: "safen", 24 | version, 25 | description: "Super Fast Object Validator for Javascript(& Typescript).", 26 | keywords: [ 27 | "validation", 28 | "validator", 29 | "validate", 30 | "sanitizer", 31 | "sanitize", 32 | "assert", 33 | "check", 34 | "type", 35 | "schema", 36 | "jsonschema", 37 | "joi", 38 | "ajv", 39 | "typescript", 40 | ], 41 | license: "MIT", 42 | repository: { 43 | type: "git", 44 | url: "git+https://github.com/denostack/safen.git", 45 | }, 46 | bugs: { 47 | url: "https://github.com/denostack/safen/issues", 48 | }, 49 | }, 50 | }); 51 | 52 | // post build steps 53 | Deno.copyFileSync("README.md", ".npm/README.md"); 54 | -------------------------------------------------------------------------------- /short.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, assertFalse } from "assert/mod.ts"; 2 | import { v } from "./short.ts"; 3 | 4 | Deno.test("short, validation, decorate", () => { 5 | const validate = v({ 6 | decorate: v.decorate(String, (d) => [d.email()]), 7 | }); 8 | 9 | assert(validate({ 10 | decorate: "wan2land@gmail.com", 11 | })); 12 | assertFalse(validate({ 13 | decorate: "wan2land", 14 | })); 15 | }); 16 | 17 | Deno.test("short, validation, union", () => { 18 | const validate = v({ 19 | union: v.union([String, Number]), 20 | }); 21 | 22 | assert(validate({ 23 | union: "string", 24 | })); 25 | assert(validate({ 26 | union: 30, 27 | })); 28 | assertFalse(validate({ 29 | union: false, 30 | })); 31 | }); 32 | 33 | Deno.test("short, validation, array", () => { 34 | const validate = v({ 35 | array: v.array(String), 36 | }); 37 | 38 | assert(validate({ 39 | array: ["string"], 40 | })); 41 | assertFalse(validate({ 42 | array: {}, 43 | })); 44 | }); 45 | 46 | Deno.test("short, validation, any", () => { 47 | const validate = v({ 48 | any: v.any(), 49 | }); 50 | 51 | assert(validate({ 52 | any: "string", 53 | })); 54 | assert(validate({ 55 | any: null, 56 | })); 57 | assert(validate({ 58 | any: undefined, 59 | })); 60 | assert(validate({})); 61 | }); 62 | 63 | Deno.test("short, validation, optional", () => { 64 | const validate = v({ 65 | optional: v.optional(String), 66 | }); 67 | 68 | assert(validate({ 69 | optional: "string", 70 | })); 71 | assert(validate({ 72 | optional: undefined, 73 | })); 74 | assert(validate({})); 75 | 76 | assertFalse(validate({ 77 | optional: 30, 78 | })); 79 | }); 80 | -------------------------------------------------------------------------------- /short.ts: -------------------------------------------------------------------------------- 1 | import { Ast } from "./ast/ast.ts"; 2 | import { any, array, decorate, optional, union } from "./ast/utils.ts"; 3 | import { EstimateType } from "./ast/estimate_type.ts"; 4 | import { createValidate } from "./validator/create_validate.ts"; 5 | import { createSanitize } from "./validator/create_sanitize.ts"; 6 | import { Decorator } from "./decorator/decorator.ts"; 7 | import { d, PredefinedDecorators } from "./decorators.ts"; 8 | 9 | export type DecoratorFactory = ( 10 | d: PredefinedDecorators, 11 | ) => Decorator> | Decorator>[]; 12 | 13 | const helpers = { 14 | any() { 15 | return any(); 16 | }, 17 | union(types: T[]) { 18 | return union(types); 19 | }, 20 | array(of: T) { 21 | return array(of); 22 | }, 23 | decorate(of: T, by: DecoratorFactory) { 24 | return decorate(of, by(d) as Decorator>); 25 | }, 26 | optional(of: T) { 27 | return optional(of); 28 | }, 29 | }; 30 | 31 | export const v = Object.assign(function (ast: T) { 32 | return createValidate(ast); 33 | }, helpers); 34 | 35 | export const s = Object.assign(function (ast: T) { 36 | return createSanitize(ast); 37 | }, helpers); 38 | -------------------------------------------------------------------------------- /validator/create_sanitize.test.ts: -------------------------------------------------------------------------------- 1 | import { assertEquals, assertThrows } from "assert/mod.ts"; 2 | import { between } from "../decorators/between.ts"; 3 | import { email } from "../decorators/email.ts"; 4 | import { ip } from "../decorators/ip.ts"; 5 | import { lengthBetween } from "../decorators/length_between.ts"; 6 | import { trim } from "../decorators/trim.ts"; 7 | import { any, array, decorate, optional, or, union } from "../ast/utils.ts"; 8 | import { createSanitize } from "./create_sanitize.ts"; 9 | import { InvalidValueError } from "./invalid_value_error.ts"; 10 | import { emptyToNull } from "../decorators/empty_to_null.ts"; 11 | 12 | Deno.test("validator/create_sanitize, createSanitize string", () => { 13 | const s = createSanitize(String); 14 | 15 | assertEquals(s("1"), "1"); 16 | assertEquals(s(""), ""); 17 | 18 | const e = assertThrows( 19 | () => s(30), 20 | InvalidValueError, 21 | "It must be a string.", 22 | ); 23 | assertEquals(e.path, ""); 24 | assertEquals(e.reason, "string"); 25 | }); 26 | 27 | Deno.test("validator/create_sanitize, createSanitize number", () => { 28 | const s = createSanitize(Number); 29 | 30 | assertEquals(s(30), 30); 31 | assertEquals(s(3.5), 3.5); 32 | 33 | const e = assertThrows( 34 | () => s(true), 35 | InvalidValueError, 36 | "It must be a number.", 37 | ); 38 | assertEquals(e.path, ""); 39 | assertEquals(e.reason, "number"); 40 | }); 41 | 42 | Deno.test("validator/create_sanitize, createSanitize boolean", () => { 43 | const s = createSanitize(Boolean); 44 | 45 | assertEquals(s(true), true); 46 | assertEquals(s(false), false); 47 | 48 | const e = assertThrows( 49 | () => s(30), 50 | InvalidValueError, 51 | "It must be a boolean.", 52 | ); 53 | assertEquals(e.path, ""); 54 | assertEquals(e.reason, "boolean"); 55 | }); 56 | 57 | Deno.test("validator/create_sanitize, createSanitize bigint", () => { 58 | const s = createSanitize(BigInt); 59 | 60 | assertEquals(s(1n), 1n); 61 | 62 | const e = assertThrows( 63 | () => s(30), 64 | InvalidValueError, 65 | "It must be a bigint.", 66 | ); 67 | assertEquals(e.path, ""); 68 | assertEquals(e.reason, "bigint"); 69 | }); 70 | 71 | Deno.test("validator/create_sanitize, createSanitize symbol", () => { 72 | const s = createSanitize(Symbol); 73 | 74 | const sym = Symbol(30); 75 | assertEquals(s(sym), sym); 76 | 77 | const e = assertThrows( 78 | () => s(30), 79 | InvalidValueError, 80 | "It must be a symbol.", 81 | ); 82 | assertEquals(e.path, ""); 83 | assertEquals(e.reason, "symbol"); 84 | }); 85 | 86 | Deno.test("validator/create_sanitize, createSanitize string value", () => { 87 | const s = createSanitize("something"); 88 | 89 | assertEquals(s("something"), "something"); 90 | 91 | const e = assertThrows( 92 | () => s(30), 93 | InvalidValueError, 94 | 'It must be a "something".', 95 | ); 96 | assertEquals(e.path, ""); 97 | assertEquals(e.reason, '"something"'); 98 | }); 99 | 100 | Deno.test("validator/create_sanitize, createSanitize number value", () => { 101 | const s = createSanitize(1); 102 | 103 | assertEquals(s(1), 1); 104 | 105 | const e = assertThrows( 106 | () => s(2), 107 | InvalidValueError, 108 | "It must be a 1.", 109 | ); 110 | assertEquals(e.path, ""); 111 | assertEquals(e.reason, "1"); 112 | }); 113 | 114 | Deno.test("validator/create_sanitize, createSanitize boolean value", () => { 115 | const s = createSanitize(true); 116 | 117 | assertEquals(s(true), true); 118 | 119 | const e = assertThrows( 120 | () => s(false), 121 | InvalidValueError, 122 | "It must be a true.", 123 | ); 124 | assertEquals(e.path, ""); 125 | assertEquals(e.reason, "true"); 126 | }); 127 | 128 | Deno.test("validator/create_sanitize, createSanitize bigint value", () => { 129 | const s = createSanitize(1n); 130 | 131 | assertEquals(s(1n), 1n); 132 | 133 | const e = assertThrows( 134 | () => s(1), 135 | InvalidValueError, 136 | "It must be a 1n.", 137 | ); 138 | assertEquals(e.path, ""); 139 | assertEquals(e.reason, "1n"); 140 | }); 141 | 142 | Deno.test("validator/create_sanitize, createSanitize null", () => { 143 | const s = createSanitize(null); 144 | 145 | assertEquals(s(null), null); 146 | 147 | const e = assertThrows( 148 | () => s(undefined), 149 | InvalidValueError, 150 | "It must be a null.", 151 | ); 152 | assertEquals(e.path, ""); 153 | assertEquals(e.reason, "null"); 154 | }); 155 | 156 | Deno.test("validator/create_sanitize, createSanitize undefined", () => { 157 | const s = createSanitize(undefined); 158 | 159 | assertEquals(s(undefined), undefined); 160 | 161 | const e = assertThrows( 162 | () => s(null), 163 | InvalidValueError, 164 | "It must be a undefined.", 165 | ); 166 | assertEquals(e.path, ""); 167 | assertEquals(e.reason, "undefined"); 168 | }); 169 | 170 | Deno.test("validator/create_sanitize, createSanitize any", () => { 171 | const s = createSanitize(any()); 172 | 173 | assertEquals(s(undefined), undefined); 174 | assertEquals(s(1), 1); 175 | assertEquals(s("1"), "1"); 176 | assertEquals(s(true), true); 177 | assertEquals(s({}), {}); 178 | }); 179 | 180 | Deno.test("validator/create_sanitize, createSanitize object", () => { 181 | const Point = { 182 | x: Number, 183 | y: Number, 184 | }; 185 | const s = createSanitize({ 186 | start: Point, 187 | end: Point, 188 | empty: {}, 189 | }); 190 | 191 | assertEquals(s({ start: { x: 1, y: 2 }, end: { x: 3, y: 4 }, empty: {} }), { 192 | start: { x: 1, y: 2 }, 193 | end: { x: 3, y: 4 }, 194 | empty: {}, 195 | }); 196 | 197 | { 198 | const e = assertThrows( 199 | () => s(null), 200 | InvalidValueError, 201 | "It must be a object.", 202 | ); 203 | assertEquals(e.path, ""); 204 | assertEquals(e.reason, "object"); 205 | } 206 | { 207 | const e = assertThrows( 208 | () => 209 | s({ 210 | start: { x: 1, y: 2 }, 211 | end: { x: 3 }, 212 | }), 213 | InvalidValueError, 214 | "It must be a number.", 215 | ); 216 | assertEquals(e.path, "end.y"); 217 | assertEquals(e.reason, "number"); 218 | } 219 | }); 220 | 221 | Deno.test("validator/create_sanitize, createSanitize union", () => { 222 | const s = createSanitize({ 223 | id: union([String, { "#": String }, Number, BigInt, { _: Number }]), 224 | }); 225 | 226 | assertEquals(s({ id: "1" }), { id: "1" }); 227 | assertEquals(s({ id: { "#": "0x1" } }), { id: { "#": "0x1" } }); 228 | assertEquals(s({ id: 1 }), { id: 1 }); 229 | assertEquals(s({ id: 1n }), { id: 1n }); 230 | assertEquals(s({ id: { _: 10 } }), { id: { _: 10 } }); 231 | 232 | { 233 | const e = assertThrows( 234 | () => s({ id: true }), 235 | InvalidValueError, 236 | "It must be one of the types.", 237 | ); 238 | assertEquals(e.path, "id"); 239 | assertEquals(e.reason, "union"); 240 | } 241 | }); 242 | 243 | Deno.test("validator/create_sanitize, createSanitize array of any", () => { 244 | const s = createSanitize(Array); 245 | 246 | assertEquals(s([]), []); 247 | assertEquals(s(["1", 1, 1n]), ["1", 1, 1n]); 248 | 249 | assertEquals(s(["1"]), ["1"]); 250 | assertEquals(s([1]), [1]); 251 | assertEquals(s([1n]), [1n]); 252 | assertEquals(s([true]), [true]); 253 | assertEquals(s([null]), [null]); 254 | assertEquals(s([undefined]), [undefined]); 255 | 256 | const e = assertThrows( 257 | () => s({ id: true }), 258 | InvalidValueError, 259 | "It must be a array.", 260 | ); 261 | assertEquals(e.path, ""); 262 | assertEquals(e.reason, "array"); 263 | }); 264 | 265 | Deno.test("validator/create_sanitize, createSanitize sugar array", () => { 266 | const s = createSanitize({ ids: [Number] }); 267 | 268 | assertEquals(s({ ids: [] }), { ids: [] }); 269 | assertEquals(s({ ids: [1, 2, 3] }), { ids: [1, 2, 3] }); 270 | 271 | const e = assertThrows( 272 | () => s({ ids: [1, 2, "3"] }), 273 | InvalidValueError, 274 | "It must be a number.", 275 | ); 276 | assertEquals(e.path, "ids[2]"); 277 | assertEquals(e.reason, "number"); 278 | }); 279 | 280 | Deno.test("validator/create_sanitize, createSanitize array of primitive", () => { 281 | const s = createSanitize({ ids: array(Number) }); 282 | 283 | assertEquals(s({ ids: [] }), { ids: [] }); 284 | assertEquals(s({ ids: [1, 2, 3] }), { ids: [1, 2, 3] }); 285 | 286 | const e = assertThrows( 287 | () => s({ ids: [1, 2, "3"] }), 288 | InvalidValueError, 289 | "It must be a number.", 290 | ); 291 | assertEquals(e.path, "ids[2]"); 292 | assertEquals(e.reason, "number"); 293 | }); 294 | 295 | Deno.test("validator/create_sanitize, createSanitize array of union", () => { 296 | const s = createSanitize(array(or([String, Number, BigInt]))); 297 | 298 | assertEquals(s([]), []); 299 | assertEquals(s(["1", 1, 1n]), ["1", 1, 1n]); 300 | 301 | assertEquals(s(["1"]), ["1"]); 302 | assertEquals(s([1]), [1]); 303 | assertEquals(s([1n]), [1n]); 304 | 305 | const e = assertThrows( 306 | () => s([1, 2, true]), 307 | InvalidValueError, 308 | "It must be one of the types.", 309 | ); 310 | assertEquals(e.path, "[2]"); 311 | assertEquals(e.reason, "union"); 312 | }); 313 | 314 | Deno.test("validator/create_sanitize, createSanitize decorate", () => { 315 | const s = createSanitize(decorate(String, [trim(), ip("v4")])); 316 | 317 | assertEquals(s("127.0.0.1"), "127.0.0.1"); 318 | assertEquals(s(" 127.0.0.1 "), "127.0.0.1"); 319 | 320 | const e = assertThrows( 321 | () => s("128.0.0.1.1"), 322 | InvalidValueError, 323 | "This is an invalid value from decorator.", 324 | ); 325 | assertEquals(e.path, ""); 326 | assertEquals(e.reason, "#ip"); 327 | }); 328 | 329 | Deno.test("validator/create_sanitize, createSanitize decorate complex", () => { 330 | const s = createSanitize( 331 | decorate( 332 | union([decorate(String, trim()), null]), 333 | emptyToNull(), 334 | ), 335 | ); 336 | 337 | assertEquals(s(" 127.0.0.1 "), "127.0.0.1"); 338 | assertEquals(s(" "), null); 339 | assertEquals(s(null), null); 340 | }); 341 | 342 | Deno.test("validator/create_sanitize, createSanitize complex", () => { 343 | const typeLat = decorate(Number, between(-90, 90)); 344 | const typeLng = decorate(Number, between(-180, 180)); 345 | const s = createSanitize({ 346 | id: Number, 347 | email: decorate(String, [trim(), email()]), 348 | name: optional(String), 349 | password: decorate(String, lengthBetween(8, 20)), 350 | areas: [{ 351 | lat: typeLat, 352 | lng: typeLng, 353 | }], 354 | env: { 355 | ip: decorate(String, ip("v4")), 356 | os: { 357 | name: or([ 358 | "window" as const, 359 | "osx" as const, 360 | "android" as const, 361 | "iphone" as const, 362 | ]), 363 | version: String, 364 | }, 365 | browser: { 366 | name: or([ 367 | "chrome" as const, 368 | "firefox" as const, 369 | "edge" as const, 370 | "ie" as const, 371 | ]), 372 | version: String, 373 | }, 374 | }, 375 | }); 376 | 377 | assertEquals( 378 | s({ 379 | id: 30, 380 | email: " wan2land@gmail.com ", 381 | name: "wan2land", 382 | password: "12345678", 383 | areas: [ 384 | { lat: 0, lng: 0 }, 385 | ], 386 | env: { 387 | ip: "127.0.0.1", 388 | os: { 389 | name: "osx", 390 | version: "10.13.1", 391 | }, 392 | browser: { 393 | name: "chrome", 394 | version: "62.0.3202.94", 395 | }, 396 | }, 397 | }), 398 | { 399 | id: 30, 400 | email: "wan2land@gmail.com", // trimmed! 401 | name: "wan2land", 402 | password: "12345678", 403 | areas: [ 404 | { lat: 0, lng: 0 }, 405 | ], 406 | env: { 407 | ip: "127.0.0.1", 408 | os: { 409 | name: "osx", 410 | version: "10.13.1", 411 | }, 412 | browser: { 413 | name: "chrome", 414 | version: "62.0.3202.94", 415 | }, 416 | }, 417 | }, 418 | ); 419 | }); 420 | -------------------------------------------------------------------------------- /validator/create_sanitize.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Ast, 3 | AstLiteral, 4 | AstPrimitive, 5 | AstStrict, 6 | Kind, 7 | PrimitiveType, 8 | } from "../ast/ast.ts"; 9 | import { desugar } from "../ast/desugar.ts"; 10 | import { EstimateType } from "../ast/estimate_type.ts"; 11 | import { Decorator } from "../decorator/decorator.ts"; 12 | import { 13 | condLiteral, 14 | condPrimitive, 15 | stringifyLiteral, 16 | } from "./create_validate.ts"; 17 | import { InvalidValueError } from "./invalid_value_error.ts"; 18 | 19 | const astToIndex = new Map(); 20 | let fns: string[] = []; 21 | const decoratorToIdx = new Map, number>(); 22 | let decorators: Decorator[] = []; 23 | 24 | type InternalError = 25 | | InternalUnionError 26 | | InternalTypeError 27 | | InternalDecoratorError; 28 | 29 | interface InternalUnionError { 30 | type: "union"; 31 | path: string; 32 | } 33 | 34 | interface InternalTypeError { 35 | type: "type"; 36 | reason: string; 37 | path: string; 38 | } 39 | 40 | interface InternalDecoratorError { 41 | type: "decorator"; 42 | reason: string; 43 | path: string; 44 | } 45 | 46 | function throwUnionError(path = "p") { 47 | return `throw{type:"union",path:${path}}`; 48 | } 49 | 50 | function throwTypeError(type: string, path = "p") { 51 | return `throw{type:"type",reason:${JSON.stringify(type)},path:${path}}`; 52 | } 53 | 54 | function throwDecoratorError(reason: string, path = "p") { 55 | return `throw{type:"decorator",reason:${ 56 | JSON.stringify(reason) 57 | },path:${path}}`; 58 | } 59 | 60 | function invalidPrimitive(ast: AstPrimitive, value = "v", path = "p"): string { 61 | switch (ast[1]) { 62 | case PrimitiveType.Any: { 63 | return ""; 64 | } 65 | case PrimitiveType.Null: { 66 | return `if(${value}!==null)${throwTypeError("null", path)};`; 67 | } 68 | case PrimitiveType.Undefined: { 69 | return `if(typeof ${value}!=="undefined")${ 70 | throwTypeError("undefined", path) 71 | };`; 72 | } 73 | case PrimitiveType.String: { 74 | return `if(typeof ${value}!=="string")${throwTypeError("string", path)};`; 75 | } 76 | case PrimitiveType.Number: { 77 | return `if(typeof ${value}!=="number")${throwTypeError("number", path)};`; 78 | } 79 | case PrimitiveType.Boolean: { 80 | return `if(typeof ${value}!=="boolean")${ 81 | throwTypeError("boolean", path) 82 | };`; 83 | } 84 | case PrimitiveType.BigInt: { 85 | return `if(typeof ${value}!=="bigint")${throwTypeError("bigint", path)};`; 86 | } 87 | case PrimitiveType.Symbol: { 88 | return `if(typeof ${value}!=="symbol")${throwTypeError("symbol", path)};`; 89 | } 90 | } 91 | throw new Error("unsupported primitive type"); 92 | } 93 | 94 | function invalidLiteral(ast: AstLiteral, value: string, path = "p"): string { 95 | const literalValue = stringifyLiteral(ast[1]); 96 | return `if(${value}!==${literalValue})${throwTypeError(literalValue, path)};`; 97 | } 98 | 99 | function invalidAst(ast: AstStrict, value: string, path = "p"): string { 100 | switch (ast[0]) { 101 | case Kind.Primitive: { 102 | return invalidPrimitive(ast, value, path); 103 | } 104 | case Kind.Literal: { 105 | return invalidLiteral(ast, value, path); 106 | } 107 | } 108 | if (!astToIndex.has(ast)) { 109 | traverse(ast); 110 | } 111 | const idx = astToIndex.get(ast)!; 112 | return `${value}=_${idx}(${value},${path});`; 113 | } 114 | 115 | function traverse(ast: AstStrict) { 116 | const idx = fns.length; 117 | const name = `_${idx}`; 118 | 119 | astToIndex.set(ast, idx); 120 | fns.push(""); 121 | 122 | switch (ast[0]) { 123 | case Kind.Primitive: { 124 | fns[idx] = `function ${name}(v,p){${invalidPrimitive(ast, "v")}return v}`; 125 | return; 126 | } 127 | case Kind.Literal: { 128 | fns[idx] = `function ${name}(v,p){${invalidLiteral(ast, "v")}return v}`; 129 | return; 130 | } 131 | case Kind.Array: { 132 | let result = `function ${name}(v,p){`; 133 | result += `if(!Array.isArray(v))${throwTypeError("array")};`; 134 | result += `for(let i=0;i] => { 187 | if (!decoratorToIdx.has(decorator)) { 188 | decoratorToIdx.set(decorator, decorators.length); 189 | decorators.push(decorator); 190 | } 191 | return [decoratorToIdx.get(decorator)!, decorator]; 192 | }); 193 | for (const [dId, decorator] of pairs) { 194 | if (decorator.cast) { 195 | result += `v=_d[${dId}].cast(v);`; 196 | } 197 | } 198 | result += invalidAst(ast[1], "v"); 199 | for (const [dId, decorator] of pairs) { 200 | if (decorator.validate) { 201 | result += `if(!_d[${dId}].validate(v))${ 202 | throwDecoratorError(decorator.name) 203 | };`; 204 | } 205 | if (decorator.transform) { 206 | result += `v=_d[${dId}].transform(v);`; 207 | } 208 | } 209 | result += `return v}`; 210 | fns[idx] = result; 211 | return; 212 | } 213 | } 214 | throw new Error("Invalid ast"); 215 | } 216 | 217 | export function createSanitizeSource(ast: AstStrict) { 218 | fns = []; 219 | decorators = []; 220 | astToIndex.clear(); 221 | decoratorToIdx.clear(); 222 | traverse(ast); 223 | return { 224 | source: fns.join("\n"), 225 | decorators, 226 | }; 227 | } 228 | 229 | function mapCreateError(error: InternalError) { 230 | if (error.type === "decorator") { 231 | const path = error.path.replace(/^\.+/, ""); 232 | return new InvalidValueError( 233 | "This is an invalid value from decorator.", 234 | `#${error.reason}`, 235 | path, 236 | ); 237 | } 238 | if (error.type === "type") { 239 | const path = error.path.replace(/^\.+/, ""); 240 | return new InvalidValueError( 241 | `It must be a ${error.reason}.`, 242 | `${error.reason}`, 243 | path, 244 | ); 245 | } 246 | if (error.type === "union") { 247 | const path = error.path.replace(/^\.+/, ""); 248 | return new InvalidValueError( 249 | `It must be one of the types.`, 250 | "union", 251 | path, 252 | ); 253 | } 254 | throw new Error("Invalid error type"); 255 | } 256 | 257 | export function createSanitize( 258 | ast: T, 259 | ): (data: unknown) => EstimateType { 260 | const { source, decorators } = createSanitizeSource(desugar(ast)); 261 | return new Function( 262 | "_d", 263 | "_e", 264 | `${source}\nreturn function(d){try{return _0(d,'')}catch(e){throw _e(e)}}`, 265 | )(decorators, mapCreateError); 266 | } 267 | -------------------------------------------------------------------------------- /validator/create_validate.test.ts: -------------------------------------------------------------------------------- 1 | import { between } from "../decorators/between.ts"; 2 | import { email } from "../decorators/email.ts"; 3 | import { lengthBetween } from "../decorators/length_between.ts"; 4 | import { assert, assertFalse } from "assert/mod.ts"; 5 | import { any, array, decorate, optional, or, union } from "../ast/utils.ts"; 6 | import { emptyToNull } from "../decorators/empty_to_null.ts"; 7 | import { ip } from "../decorators/ip.ts"; 8 | import { trim } from "../decorators/trim.ts"; 9 | import { createValidate } from "./create_validate.ts"; 10 | 11 | Deno.test("validator/create_validate, createValidate string", () => { 12 | const v = createValidate(String); 13 | 14 | assert(v("1")); 15 | assert(v("")); 16 | 17 | assertFalse(v(1)); 18 | assertFalse(v(1n)); 19 | assertFalse(v(true)); 20 | assertFalse(v(null)); 21 | assertFalse(v(undefined)); 22 | }); 23 | 24 | Deno.test("validator/create_validate, createValidate number", () => { 25 | const v = createValidate(Number); 26 | 27 | assert(v(30)); 28 | assert(v(3.5)); 29 | 30 | assertFalse(v("1")); 31 | assertFalse(v(1n)); 32 | assertFalse(v(true)); 33 | assertFalse(v(null)); 34 | assertFalse(v(undefined)); 35 | }); 36 | 37 | Deno.test("validator/create_validate, createValidate boolean", () => { 38 | const v = createValidate(Boolean); 39 | 40 | assert(v(true)); 41 | assert(v(false)); 42 | 43 | assertFalse(v("1")); 44 | assertFalse(v(1)); 45 | assertFalse(v(1n)); 46 | assertFalse(v(null)); 47 | assertFalse(v(undefined)); 48 | }); 49 | 50 | Deno.test("validator/create_validate, createValidate bigint", () => { 51 | const v = createValidate(BigInt); 52 | 53 | assert(v(1n)); 54 | 55 | assertFalse(v("1")); 56 | assertFalse(v(1)); 57 | assertFalse(v(true)); 58 | assertFalse(v(null)); 59 | assertFalse(v(undefined)); 60 | }); 61 | 62 | Deno.test("validator/create_validate, createValidate symbol", () => { 63 | const v = createValidate(Symbol); 64 | 65 | assert(v(Symbol(1))); 66 | 67 | assertFalse(v("1")); 68 | assertFalse(v(1)); 69 | assertFalse(v(true)); 70 | assertFalse(v(null)); 71 | assertFalse(v(undefined)); 72 | }); 73 | 74 | Deno.test("validator/create_validate, createValidate string literal", () => { 75 | const v = createValidate("something"); 76 | 77 | assert(v("something")); 78 | assertFalse(v("unknown")); 79 | }); 80 | 81 | Deno.test("validator/create_validate, createValidate number literal", () => { 82 | const v = createValidate(1); 83 | 84 | assert(v(1)); 85 | assertFalse(v(0)); 86 | }); 87 | 88 | Deno.test("validator/create_validate, createValidate boolean literal", () => { 89 | { 90 | const v = createValidate(true); 91 | 92 | assert(v(true)); 93 | assertFalse(v(false)); 94 | } 95 | { 96 | const v = createValidate(false); 97 | 98 | assert(v(false)); 99 | assertFalse(v(true)); 100 | } 101 | }); 102 | 103 | Deno.test("validator/create_validate, createValidate bigint literal", () => { 104 | const v = createValidate(1n); 105 | 106 | assert(v(1n)); 107 | assertFalse(v(0n)); 108 | }); 109 | 110 | Deno.test("validator/create_validate, createValidate null", () => { 111 | const v = createValidate(null); 112 | 113 | assert(v(null)); 114 | 115 | assertFalse(v("1")); 116 | assertFalse(v(1)); 117 | assertFalse(v(1n)); 118 | assertFalse(v(true)); 119 | assertFalse(v(undefined)); 120 | }); 121 | 122 | Deno.test("validator/create_validate, createValidate undefined", () => { 123 | const v = createValidate(undefined); 124 | 125 | assert(v(undefined)); 126 | 127 | assertFalse(v("1")); 128 | assertFalse(v(1)); 129 | assertFalse(v(1n)); 130 | assertFalse(v(true)); 131 | assertFalse(v(null)); 132 | }); 133 | 134 | Deno.test("validator/create_validate, createValidate any", () => { 135 | const v = createValidate(any()); 136 | 137 | assert(v(undefined)); 138 | assert(v("1")); 139 | assert(v(1)); 140 | assert(v(1n)); 141 | assert(v(true)); 142 | assert(v(null)); 143 | }); 144 | 145 | Deno.test("validator/create_validate, createValidate object", () => { 146 | const Point = { 147 | x: Number, 148 | y: Number, 149 | }; 150 | const v = createValidate({ 151 | start: Point, 152 | end: Point, 153 | empty: {}, 154 | }); 155 | 156 | assert(v({ start: { x: 1, y: 2 }, end: { x: 3, y: 4 }, empty: {} })); 157 | }); 158 | 159 | Deno.test("validator/create_validate, createValidate union", () => { 160 | const v = createValidate(union([String, Number, BigInt])); 161 | 162 | assert(v("1")); 163 | assert(v(1)); 164 | assert(v(1n)); 165 | 166 | assertFalse(v(true)); 167 | assertFalse(v(null)); 168 | assertFalse(v(undefined)); 169 | }); 170 | 171 | Deno.test("validator/create_validate, createValidate array of any", () => { 172 | const v = createValidate(Array); 173 | 174 | assert(v([])); 175 | assert(v(["1", 1, 1n])); 176 | 177 | assert(v(["1"])); 178 | assert(v([1])); 179 | assert(v([1n])); 180 | assert(v([true])); 181 | assert(v([null])); 182 | assert(v([undefined])); 183 | 184 | assertFalse(v(1)); 185 | assertFalse(v(1n)); 186 | assertFalse(v(true)); 187 | assertFalse(v(null)); 188 | assertFalse(v(undefined)); 189 | }); 190 | 191 | Deno.test("validator/create_validate, createValidate array", () => { 192 | const v = createValidate([or([String, Number, BigInt])]); 193 | 194 | assert(v([])); 195 | assert(v(["1", 1, 1n])); 196 | 197 | assert(v(["1"])); 198 | assert(v([1])); 199 | assert(v([1n])); 200 | assertFalse(v([true])); 201 | assertFalse(v([null])); 202 | assertFalse(v([undefined])); 203 | 204 | assertFalse(v(1)); 205 | assertFalse(v(1n)); 206 | assertFalse(v(true)); 207 | assertFalse(v(null)); 208 | assertFalse(v(undefined)); 209 | }); 210 | 211 | Deno.test("validator/create_validate, createValidate array by util", () => { 212 | const v = createValidate(array(or([String, Number, BigInt]))); 213 | 214 | assert(v([])); 215 | assert(v(["1", 1, 1n])); 216 | 217 | assert(v(["1"])); 218 | assert(v([1])); 219 | assert(v([1n])); 220 | assertFalse(v([true])); 221 | assertFalse(v([null])); 222 | assertFalse(v([undefined])); 223 | 224 | assertFalse(v(1)); 225 | assertFalse(v(1n)); 226 | assertFalse(v(true)); 227 | assertFalse(v(null)); 228 | assertFalse(v(undefined)); 229 | }); 230 | 231 | Deno.test("validator/create_validate, createValidate array", () => { 232 | const v = createValidate(array(or([String, Number, BigInt]))); 233 | 234 | assert(v([])); 235 | assert(v(["1", 1, 1n])); 236 | 237 | assert(v(["1"])); 238 | assert(v([1])); 239 | assert(v([1n])); 240 | assertFalse(v([true])); 241 | assertFalse(v([null])); 242 | assertFalse(v([undefined])); 243 | 244 | assertFalse(v(1)); 245 | assertFalse(v(1n)); 246 | assertFalse(v(true)); 247 | assertFalse(v(null)); 248 | assertFalse(v(undefined)); 249 | }); 250 | 251 | Deno.test("validator/create_validate, createValidate decorate", () => { 252 | const v = createValidate(decorate(String, ip("v4"))); 253 | 254 | assert(v("127.0.0.1")); 255 | assertFalse(v("1")); 256 | }); 257 | 258 | Deno.test("validator/create_validate, createValidate decorate complex", () => { 259 | const v = createValidate( 260 | decorate( 261 | union([decorate(String, trim()), null]), 262 | emptyToNull(), 263 | ), 264 | ); 265 | 266 | assert(v(" 127.0.0.1 ")); 267 | assert(v(" ")); 268 | assert(v(null)); 269 | }); 270 | 271 | Deno.test("validator/create_validate, createValidate complex", () => { 272 | const typeLat = decorate(Number, between(-90, 90)); 273 | const typeLng = decorate(Number, between(-180, 180)); 274 | const v = createValidate({ 275 | id: Number, 276 | email: decorate(String, [trim(), email()]), 277 | name: optional(String), 278 | password: decorate(String, lengthBetween(8, 20)), 279 | areas: [{ 280 | lat: typeLat, 281 | lng: typeLng, 282 | }], 283 | env: { 284 | ip: decorate(String, ip("v4")), 285 | os: { 286 | name: or([ 287 | "window" as const, 288 | "osx" as const, 289 | "android" as const, 290 | "iphone" as const, 291 | ]), 292 | version: String, 293 | }, 294 | browser: { 295 | name: or([ 296 | "chrome" as const, 297 | "firefox" as const, 298 | "edge" as const, 299 | "ie" as const, 300 | ]), 301 | version: String, 302 | }, 303 | }, 304 | }); 305 | 306 | assert( 307 | v({ 308 | id: 30, 309 | email: " wan2land@gmail.com ", 310 | name: "wan2land", 311 | password: "12345678", 312 | areas: [ 313 | { lat: 0, lng: 0 }, 314 | ], 315 | env: { 316 | ip: "127.0.0.1", 317 | os: { 318 | name: "osx", 319 | version: "10.13.1", 320 | }, 321 | browser: { 322 | name: "chrome", 323 | version: "62.0.3202.94", 324 | }, 325 | }, 326 | }), 327 | ); 328 | }); 329 | -------------------------------------------------------------------------------- /validator/create_validate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Ast, 3 | AstLiteral, 4 | AstPrimitive, 5 | AstStrict, 6 | Kind, 7 | PrimitiveType, 8 | } from "../ast/ast.ts"; 9 | import { desugar } from "../ast/desugar.ts"; 10 | import { EstimateType } from "../ast/estimate_type.ts"; 11 | import { Decorator } from "../decorator/decorator.ts"; 12 | 13 | const astToIndex = new Map(); 14 | let fns: string[] = []; 15 | const decoratorToIdx = new Map, number>(); 16 | let decorators: Decorator[] = []; 17 | 18 | export function stringifyLiteral( 19 | value: string | number | boolean | bigint, 20 | ): string { 21 | switch (typeof value) { 22 | case "string": 23 | case "number": 24 | case "boolean": { 25 | return JSON.stringify(value); 26 | } 27 | case "bigint": { 28 | return `${value.toString()}n`; 29 | } 30 | } 31 | throw new Error("Unknown literal type"); 32 | } 33 | 34 | const comparators = ["!==", "==="]; 35 | export function condPrimitive( 36 | ast: AstPrimitive, 37 | is: 1 | 0, 38 | value: string, 39 | ): string { 40 | switch (ast[1]) { 41 | case PrimitiveType.Any: { 42 | return is ? "true" : "false"; 43 | } 44 | case PrimitiveType.Null: { 45 | return `${value}${comparators[is]}null`; 46 | } 47 | case PrimitiveType.Undefined: { 48 | return `typeof ${value}${comparators[is]}"undefined"`; 49 | } 50 | case PrimitiveType.String: { 51 | return `typeof ${value}${comparators[is]}"string"`; 52 | } 53 | case PrimitiveType.Number: { 54 | return `typeof ${value}${comparators[is]}"number"`; 55 | } 56 | case PrimitiveType.Boolean: { 57 | return `typeof ${value}${comparators[is]}"boolean"`; 58 | } 59 | case PrimitiveType.BigInt: { 60 | return `typeof ${value}${comparators[is]}"bigint"`; 61 | } 62 | case PrimitiveType.Symbol: { 63 | return `typeof ${value}${comparators[is]}"symbol"`; 64 | } 65 | } 66 | throw new Error("unsupported primitive type"); 67 | } 68 | 69 | export function condLiteral(ast: AstLiteral, is: 1 | 0, value: string): string { 70 | const literalValue = stringifyLiteral(ast[1]); 71 | return `${value}${comparators[is]}${literalValue}`; 72 | } 73 | 74 | function condRoot(ast: AstStrict, is: 1 | 0, value: string): string { 75 | switch (ast[0]) { 76 | case Kind.Primitive: { 77 | return condPrimitive(ast, is, value); 78 | } 79 | case Kind.Literal: { 80 | return condLiteral(ast, is, value); 81 | } 82 | } 83 | if (!astToIndex.has(ast)) { 84 | traverse(ast); 85 | } 86 | const idx = astToIndex.get(ast)!; 87 | return is ? `_${idx}(${value})` : `!_${idx}(${value})`; 88 | } 89 | 90 | function traverse(ast: AstStrict) { 91 | const idx = fns.length; 92 | const name = `_${idx}`; 93 | 94 | astToIndex.set(ast, idx); 95 | fns.push(""); 96 | 97 | switch (ast[0]) { 98 | case Kind.Primitive: { 99 | fns[idx] = `function ${name}(v){return ${condPrimitive(ast, 1, "v")}}`; 100 | return; 101 | } 102 | case Kind.Literal: { 103 | fns[idx] = `function ${name}(v){return ${condLiteral(ast, 1, "v")}}`; 104 | return; 105 | } 106 | case Kind.Array: { 107 | let result = `function ${name}(v){`; 108 | result += `if(!Array.isArray(v))return false;`; 109 | result += `for(let i=0;i 127 | condRoot(child, 1, `v[${JSON.stringify(key)}]`) 128 | ).join("&&"); 129 | result += `}`; // end fn 130 | fns[idx] = result; 131 | return; 132 | } 133 | case Kind.Union: { 134 | const [_, children] = ast; 135 | if (children.length === 0) { 136 | throw new Error("Union must have at least one subtype"); 137 | } 138 | fns[idx] = `function ${name}(v){return ${ 139 | children.map((child) => condRoot(child, 1, "v")).join("||") 140 | }}`; 141 | return; 142 | } 143 | case Kind.Decorator: { 144 | let result = `function ${name}(v){`; 145 | const pairs = ast[2].map((decorator): [number, Decorator] => { 146 | if (!decoratorToIdx.has(decorator)) { 147 | decoratorToIdx.set(decorator, decorators.length); 148 | decorators.push(decorator); 149 | } 150 | return [decoratorToIdx.get(decorator)!, decorator]; 151 | }); 152 | for (const [dId, decorator] of pairs) { 153 | if (decorator.cast) { 154 | result += `v=_d[${dId}].cast(v);`; 155 | } 156 | } 157 | result += `if(${condRoot(ast[1], 0, "v")})return false;`; 158 | for (const [dId, decorator] of pairs) { 159 | if (decorator.validate) { 160 | result += `if(!_d[${dId}].validate(v))return false;`; 161 | } 162 | if (decorator.transform) { 163 | result += `v=_d[${dId}].transform(v);`; 164 | } 165 | } 166 | result += `return true`; 167 | result += `}`; 168 | fns[idx] = result; 169 | return; 170 | } 171 | } 172 | throw new Error("Invalid schema"); 173 | } 174 | 175 | export function createValidateSource(ast: AstStrict) { 176 | fns = []; 177 | decorators = []; 178 | astToIndex.clear(); 179 | decoratorToIdx.clear(); 180 | traverse(ast); 181 | return { 182 | source: fns.join("\n"), 183 | decorators, 184 | }; 185 | } 186 | 187 | export function createValidate( 188 | ast: T, 189 | ): (data: unknown) => data is EstimateType { 190 | const { source, decorators } = createValidateSource(desugar(ast)); 191 | return new Function( 192 | "_d", 193 | `${source}\nreturn _0`, 194 | )(decorators); 195 | } 196 | -------------------------------------------------------------------------------- /validator/invalid_value_error.ts: -------------------------------------------------------------------------------- 1 | export class InvalidValueError extends Error { 2 | constructor( 3 | message: string, 4 | public reason: string, 5 | public path: string, 6 | ) { 7 | super(message); 8 | this.name = "InvalidValueError"; 9 | } 10 | } 11 | --------------------------------------------------------------------------------