├── .eslintrc.js ├── .gitignore ├── README.md ├── jest.config.js ├── package.json ├── rollup.config.js ├── rollup.iife.config.js ├── src ├── API.md ├── Builtins.ts ├── DefaultIR.ts ├── Language.ts ├── __test__ │ ├── currency.ts │ ├── english.test.ts │ ├── german.test.ts │ ├── logic.test.ts │ ├── mapped-parsers.test.ts │ ├── parsers.test.ts │ ├── run.ts │ ├── schema.test.ts │ ├── setupTests.js │ ├── spanish.test.ts │ ├── swedish.test.ts │ ├── testinputs.ts │ └── utils.test.ts ├── action.ts ├── docs │ ├── auto.png │ ├── bidirectional.md │ ├── customizing-errors.md │ ├── express.md │ ├── form.md │ ├── formik.md │ ├── index.md │ ├── languages.md │ ├── lightweight-api.md │ ├── logo.png │ └── vanilla-form.png ├── iife.ts ├── index.ts ├── locales │ ├── de-DE.ts │ ├── emptyLocale.ts │ ├── en-US.ts │ ├── es-ES.ts │ └── sv-SE.ts ├── logic.ts ├── memo.ts ├── path.ts ├── schema │ ├── alphaNumeric.ts │ ├── any.ts │ ├── apply.ts │ ├── array.ts │ ├── atLeast.ts │ ├── atMost.ts │ ├── between.ts │ ├── boolean.ts │ ├── both.ts │ ├── chain.ts │ ├── collections │ │ ├── iterable.ts │ │ ├── map.ts │ │ ├── set.ts │ │ ├── toArray.ts │ │ ├── toMap.ts │ │ ├── toMapFromObject.ts │ │ └── toSet.ts │ ├── compact.ts │ ├── date.ts │ ├── defaultTo.ts │ ├── either.ts │ ├── email.ts │ ├── emptyString.ts │ ├── even.ts │ ├── every.ts │ ├── exactly.ts │ ├── factories │ │ ├── core.ts │ │ ├── createSchema.ts │ │ ├── mkParser.ts │ │ ├── mkParserHaving.ts │ │ ├── mkSchema.ts │ │ └── mkSchemaHaving.ts │ ├── fix.ts │ ├── flip.ts │ ├── forget.ts │ ├── id.ts │ ├── integer.ts │ ├── irreversible.ts │ ├── length.ts │ ├── lessThan.ts │ ├── lift.ts │ ├── match.ts │ ├── moreThan.ts │ ├── not.ts │ ├── number.ts │ ├── object.ts │ ├── objectExact.ts │ ├── objectInexact.ts │ ├── odd.ts │ ├── oneOf.ts │ ├── optional.ts │ ├── optionalTo.ts │ ├── pair.ts │ ├── path.ts │ ├── pipe.ts │ ├── pure.ts │ ├── runners │ │ ├── check.ts │ │ ├── checkByKey.ts │ │ ├── cnf.ts │ │ ├── result.ts │ │ └── sync.ts │ ├── self.ts │ ├── setMessage.ts │ ├── size.ts │ ├── some.ts │ ├── string.ts │ ├── sum.ts │ ├── swap.ts │ ├── toDate.ts │ ├── toJSON.ts │ ├── toNumber.ts │ ├── toString.ts │ ├── toURL.ts │ ├── unknown.ts │ ├── updateMessage.ts │ ├── updateNestedMessages.ts │ └── when.ts ├── types.ts └── utils.ts ├── tsconfig.json ├── tsfmt.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: "module" // Allows for the use of imports 6 | }, 7 | extends: [ 8 | "plugin:@typescript-eslint/recommended" // Uses the recommended rules from the @typescript-eslint/eslint-plugin 9 | ], 10 | rules: { 11 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 12 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | cdn 3 | node_modules 4 | TODO.org 5 | yarn-error.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | A tiny, composable validation library. Bueno primary aims to be an 6 | improvement on form validation libraries like 7 | [`yup`](https://github.com/jquense/yup) and 8 | [`superstruct`](https://github.com/ianstormtaylor/superstruct), but 9 | can also be used as a lightweight API validation library. You'll like 10 | it if you need something 11 | 12 |

🌳 Small & tree-shakeable. 14 |

💡 Expressive! Use full boolean logic to compose your schemas 15 |

💫 Bidirectional. Learn 16 | more

🚀 Awesome error messages in multiple languages 17 | supported out of the box, with more on the way. Learn more

⏱ Asynchronous 19 | (when needed!)

20 | 21 | # Try it out 22 | 23 | You can check out `bueno` directly in the browser in this 24 | [jsfiddle](https://jsfiddle.net/gm1pbk3e/11/). 25 | 26 | # Installation 27 | 28 | Install using `npm install --save bueno` or `yarn add bueno`. 29 | 30 | Check out the quickstart section below, or go directly to the API docs 31 | 32 | 38 | 39 | # Quickstart 40 | 41 | `bueno` allows you to quickly and predictably compose validation 42 | schemas. Here's how it looks in action: 43 | 44 | ```typescript 45 | import { alphaNumeric, atLeast, check, checkPerKey, deDE, describePaths, either, email, enUS, length, moreThan, not, number, object, optional, string, svSE } from 'bueno' 46 | 47 | const username = 48 | string(length(atLeast(8)), alphaNumeric) 49 | 50 | const age = 51 | number(atLeast(18), not(moreThan(130))) 52 | 53 | const user = object({ 54 | id: either(email, username), 55 | age: optional(age) 56 | }) 57 | 58 | const input = { 59 | id: 'philip@example.com', 60 | age: 17 61 | } 62 | 63 | console.log(check(input, user, enUS)) 64 | // 'Age must be at least 18 or left out' 65 | 66 | console.log(check(input, user, describePaths(svSE, [['age', 'Ålder']]))) 67 | // 'Ålder måste vara som minst 18' 68 | 69 | console.log(checkPerKey(input, user, deDE)) 70 | // { age: 'Muss mindestens 18 sein' } 71 | ``` 72 | 73 | [Try this example in a Fiddle](https://jsfiddle.net/o9urhv3m/2/) 74 | 75 | # API documentation 76 | 77 | ## Core 78 | 79 | Schemas are constructed using basic 80 | schemas like `number` `string`, `atLeast(10)`, `exactly(null)` and 81 | by using combinators like `either`, 82 | `object`, `array`, `fix` to create more complex schemas. 83 | 84 | Most schemas (specifically `Schema_`:s) can be called as 85 | functions with other schemas as arguments. E.g. 86 | 87 | ```typescript 88 | number(even, atLeast(10)) 89 | ``` 90 | 91 | The semantics are a schema returning the value of `number` with the 92 | additional validations from `even` and `atLeast(10)` taking place. 93 | 94 | ## Running a schema 95 | 96 | [check](#check) • [checkPerKey](#check) • [result](#check) 97 | 98 |
99 | 100 | The following functions allow you to feed input into a schema to parse 101 | & validate it. Note that schema evaluation is cached, so calling e.g. 102 | `check(input)` then immediately `result(input)` is not inefficient. 103 | 104 | ### `check` 105 | 106 | ```java 107 | checkAsync :: (value : A, schema : Schema, locale : Locale) : string | null 108 | ``` 109 | 110 | Returns a `string` with a validation error constructed using the given 111 | locale, or `null` if validation succeeded. 112 | 113 | ```typescript 114 | check('123', number, enUS) 115 | // 'Must be a number' 116 | ``` 117 | 118 | ### `checkByKey` 119 | 120 | Returns an object of errors for each key in an object (for a schema 121 | constructed using the [`object`](#object) combinator) 122 | 123 | ```typescript 124 | checkByKey({ n: '123', b: true }, object({ n: number, b: boolean }, enUS) 125 | // { n: 'Must be a number', b: 'Must be a boolean' } 126 | ``` 127 | 128 | ### `result` 129 | 130 | Returns the result of parsing using a schema. 131 | 132 | ```typescript 133 | result({ n: '123', d: 'null' }, object({ n: toNumber, d: toJSON }) 134 | // { n: 123, d: null } 135 | ``` 136 | 137 | ### `checkAsync`, `checkByKeyAsync` and `resultAsync` 138 | 139 | The async versions of `check`, `checkByKey` and `result` respectively. 140 | 141 | ## Combinator API 142 | 143 | apply • 144 | both • 145 | compact • 146 | defaultTo • 147 | either • 148 | every • 149 | fix • 150 | flip • 151 | not • 152 | object • 153 | optional • 154 | pipe • 155 | self • 156 | setMessage • 157 | some • 158 | when 159 | 160 | Combinators create new, more complex schemas out of existing, simpler schemas. 161 | 162 | ### `both` 163 | 164 | Creates a schema that satisfies both of its arguments. 165 | 166 | ```java 167 | both :: (v : Schema, w : Schema,) => Schema_ 168 | ``` 169 | 170 | ```typescript 171 | const schema = 172 | both(even, atLeast(10)) 173 | 174 | check(schema, 11, enUS) 175 | // 'Must be even.' 176 | 177 | check(schema, 8, enUS) 178 | // 'Must be at least 10.' 179 | 180 | check(schema, 12, enUS) 181 | // null 182 | ``` 183 | 184 | You may prefer using the call signatures 185 | of schemas over using this combinator. 186 | 187 | ### `either` 188 | 189 | Creates a schema that satisfies either of its arguments. 190 | 191 | ```java 192 | either :: (v : Schema, w : Schema,) => Schema_ 193 | ``` 194 | 195 | ```typescript 196 | const schema = 197 | either(even, atLeast(10)) 198 | 199 | check(schema, 11, enUS) 200 | // null 201 | 202 | check(schema, 8, enUS) 203 | // null 204 | 205 | check(schema, 9, enUS) 206 | // 'Must be even or at least 10' 207 | ``` 208 | 209 | ### `optional` 210 | 211 | Make a schema also match `undefined`. 212 | 213 | ```java 214 | optional :: (v : Schema) : Schema 215 | ``` 216 | 217 | ```typescript 218 | const schema = optional(number) 219 | 220 | check(schema, 9, enUS) 221 | // null 222 | 223 | check(schema, undefined, enUS) 224 | // null 225 | 226 | check(schema, null, enUS) 227 | // 'Must be a number or left out 228 | ``` 229 | 230 | 231 | ### `not` 232 | 233 | ```java 234 | not :: (v : Schema) => Schema_ 235 | ``` 236 | 237 | Negates a schema. Note that negation only affect the "validation" and 238 | not the "parsing" part of a schema. Essentially, remember that `not` does not affect 239 | the type signature of a schema. 240 | 241 | For example, `not(number)` is the same as just `number`. The reason is 242 | that we can't really do much with a value that we know only to have 243 | type "not a number". 244 | 245 | ```typescript 246 | const schema = 247 | number(not(moreThan(100))) 248 | 249 | check(103, schema, enUS) 250 | // Must not be more than 100 251 | ``` 252 | 253 | ### `object` 254 | 255 | Create a schema on objects from an object of schemas. 256 | 257 | ```java 258 | object :: (vs : 259 | { [Key in keyof AS]: Schema } & 260 | { [Key in keyof BS]: Schema } 261 | ) => Schema_ 262 | ``` 263 | 264 | ```typescript 265 | const schema = object({ 266 | age: number, 267 | name: string 268 | }) 269 | 270 | check({ age: 13 }, schema, enUS) 271 | // Name must be a string 272 | 273 | check({ age: '30', name: 'Philip' }, schema, enUS) 274 | // Age must be a number 275 | 276 | check({ age: 30, name: 'Philip' }, schema, enUS) 277 | // null 278 | ``` 279 | 280 | You can use [`compact`](#compact) to make undefined keys optional. 281 | 282 | `inexactObject` and `exactObject` are versions of this that are more 283 | lenient / strict w.r.t keys not mentioned in the schema. 284 | 285 | ### `compact` 286 | 287 | Remove keys in an object that are `undefined`. 288 | 289 | ```java 290 | compact :: (p : Schema) : Schema_, UndefinedOptional> { 291 | ``` 292 | 293 | ```typescript 294 | const schema = 295 | compact(object({ n: optional(number) })) 296 | 297 | result({ n: undefined }, schema) 298 | // {} 299 | ``` 300 | 301 | ### `fix` 302 | 303 | Create a schema that can recursively be defined in terms 304 | itself. Useful for e.g. creating a schema that matches a binary tree 305 | or other recursive structures. 306 | 307 | ```java 308 | fix :: (fn : (v : Schema) => Schema) => Schema_ 309 | ``` 310 | 311 | TypeScript is not too great at inferring types using this combinators, 312 | so typically help it using an annotation as below 313 | 314 | ```typescript 315 | type BinTree = { 316 | left : BinTree | null, 317 | right : BinTree | null, 318 | value : A 319 | } 320 | 321 | const bintree = fix, BinTree>(bintree => object({ 322 | left: either(exactly(null), bintree), 323 | right: either(exactly(null), bintree), 324 | value: toNumber 325 | })) 326 | ``` 327 | 328 | ### `self` 329 | 330 | Create a schema dynamically defined in terms of its input. 331 | 332 | ```typescript 333 | type User = { 334 | verified : boolean, 335 | email : string | null 336 | } 337 | 338 | const schema = self(user => { 339 | return object({ 340 | verified: boolean, 341 | email: user.verified ? email : exactly(null) 342 | }) 343 | }) 344 | ``` 345 | 346 | ### `flip` 347 | 348 | Reverse a schema 349 | 350 | ```java 351 | flip :: (schema : Schema) => Schema_ 352 | ``` 353 | 354 | ```typescript 355 | const schema = reverse(toNumber) 356 | 357 | result(123, schema) 358 | // '123' 359 | ``` 360 | 361 | ### `defaultTo` 362 | 363 | Set a default value for a schema when it fails parsing. 364 | 365 | ```java 366 | defaultTo :: (b: B, schema : Schema) => Schema_ 367 | ``` 368 | 369 | ```typescript 370 | const schema = 371 | defaultTo(100, number) 372 | 373 | result(null, schema) 374 | // 100 375 | ``` 376 | 377 | ### `pipe` 378 | 379 | Pipe the output of a schema as the input into another 380 | 381 | ```java 382 | pipe :: (s : Schema, t : Schema) => Schema_ 383 | ``` 384 | 385 | ```typescript 386 | const schema = 387 | pipe(toNumber, lift(x => x + 1)) 388 | 389 | result('123', schema) 390 | // 124 391 | ``` 392 | 393 | ### `apply` 394 | 395 | Set the input of a schema to a fixed value. Can be used when creating 396 | a schema where the definition of one key depends on another. 397 | 398 | ```java 399 | apply :: (v : Schema, value : A, path : string) => Schema_; 400 | ``` 401 | 402 | ```typescript 403 | type Schedule = { 404 | weekday : string 405 | price : number 406 | } 407 | 408 | const schema = self((schedule : Schedule) => object({ 409 | weekday: oneOf('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'), 410 | price: when( 411 | // When schedule.weekday is Sat or Sun, the 412 | // price must be at least 100, otherwise at most 50. 413 | apply(oneOf('Sat', 'Sun'), schedule.weekday, 'weekday'), 414 | atLeast(100), 415 | atMost(50) 416 | ) 417 | }) 418 | ``` 419 | 420 | ### `every` 421 | 422 | A variable arguments version of `both`. 423 | 424 | ```java 425 | every :: (...vs : Schema[]) => Schema_ 426 | ``` 427 | 428 | ### `setMessage` 429 | 430 | Set the error message of a parser. 431 | 432 | ```typescript 433 | const thing = setMessage( 434 | object({ foo: string, bar: number }), 435 | l => l.noun('should be a thingy), 436 | ) 437 | 438 | check({ foo: '' } as any, thing, enUS) 439 | // 'Should be a thingy' 440 | ``` 441 | 442 | ### `some` 443 | 444 | A variable arguments version of `either`. 445 | 446 | ```java 447 | some :: (...vs : Schema[]) => Schema_ 448 | ``` 449 | 450 | ### `when` 451 | 452 | "if-then-else" on parsers. 453 | 454 | ```java 455 | when :: (cond : Schema, consequent : Schema, alternative : Schema) => Schema_ 456 | ``` 457 | 458 | The "else" part is optional, in which case this combinator has the signature 459 | 460 | ```java 461 | when :: (cond : Schema, consequent : Schema) => Schema_ 462 | ``` 463 | 464 | ```typescript 465 | const schema = 466 | when(even, atLeast(10), atMost(20)) 467 | 468 | check(8, schema, enUS) 469 | // 'Must be at least 10 when even' 470 | 471 | check(21, schema, enUS) 472 | // 'Must be at most 20 when not even' 473 | 474 | check(11, schema, enUS) 475 | // null 476 | ``` 477 | 478 | ## Basic schemas 479 | 480 | alphaNumeric • 481 | any • 482 | atLeast • 483 | atMost • 484 | between • 485 | boolean • 486 | date • 487 | email • 488 | emptyString • 489 | even • 490 | exactly • 491 | id • 492 | integer • 493 | length • 494 | lessThan • 495 | lift • 496 | match • 497 | moreThan • 498 | number • 499 | objectExact • 500 | objectInexact • 501 | odd • 502 | oneOf • 503 | optionalTo • 504 | pair • 505 | path • 506 | size • 507 | string • 508 | sum • 509 | swap • 510 | toDate • 511 | toJSON • 512 | toNumber • 513 | toString • 514 | toURL • 515 | unknown 516 | 517 | Basic schemas are simple schemas that can be composed into more 518 | complex ones using the combinator API. 519 | 520 | ### `alphaNumeric` 521 | 522 | ```java 523 | alphaNumeric :: Schema_ 524 | ``` 525 | 526 | Match an alphanumeric string. 527 | 528 | ```typescript 529 | check('acb123', alphaNumeric, enUS) 530 | // null 531 | 532 | check('acb|123', alphaNumeric, enUS) 533 | // Must have letters and numbers only 534 | ``` 535 | 536 | ### `any` 537 | 538 | ```java 539 | any :: Schema_ 540 | ``` 541 | 542 | Successfully matches any input. 543 | 544 | ```typescript 545 | check(123, any, enUS) 546 | // null 547 | 548 | check(undefined, any, enUS) 549 | // null 550 | ``` 551 | 552 | ### `atLeast` 553 | 554 | ```java 555 | atLeast :: (lb : A) => Schema_ 556 | ``` 557 | 558 | Matches a value at least as big as the provided lower bound `lb`. 559 | 560 | ```typescript 561 | const schema = 562 | atLeast(100) 563 | 564 | check(88, schema, enUS) 565 | // 'Must be at least 100' 566 | ``` 567 | 568 | ### `atMost` 569 | 570 | ```java 571 | atMost :: (ub : A) => Schema_ 572 | ``` 573 | 574 | Matches a value at most as big as the provided upper bound `ub`. 575 | 576 | ```typescript 577 | const schema = 578 | atMost(100) 579 | 580 | check(88, schema, enUS) 581 | // 'Must be at most 100' 582 | ``` 583 | 584 | ### `between` 585 | 586 | Matches a value between the provided lower and upper bounds (inclusive) 587 | 588 | ```java 589 | between :: (lb : number, ub : number) => Schema_ 590 | ``` 591 | 592 | ```typescript 593 | check(99, schema, enUS) 594 | // 'Must be between 100 and 200' 595 | 596 | check(201, schema, enUS) 597 | // 'Must be between 100 and 200' 598 | 599 | check(100, schema, enUS) 600 | // null 601 | 602 | check(200, schema, enUS) 603 | // null 604 | ``` 605 | 606 | ### `boolean` 607 | 608 | Matches a boolean. 609 | 610 | ```java 611 | boolean :: Schema_ 612 | ``` 613 | 614 | ### `date` 615 | 616 | Matches a `Date` object. 617 | 618 | ```java 619 | date :: Schema_ 620 | ``` 621 | 622 | ### `email` 623 | 624 | Matches an email (validated using a permissive regular expression) 625 | 626 | ```java 627 | email :: Schema_ 628 | ``` 629 | 630 | ### `emptyString` 631 | 632 | Matches the empty string 633 | 634 | ```java 635 | emptyString :: Schema_ 636 | ``` 637 | 638 | ### `even` 639 | 640 | Matches an even number 641 | 642 | ```java 643 | even :: Schema_ 644 | ``` 645 | 646 | ### `exactly` 647 | 648 | Creates a schema that matches a single value, optionally using an equality comparison operator. 649 | 650 | ```java 651 | exactly :: (target : A, equals : (x : A, y : A) => boolean = (x, y) => x === y) => Schema_ 652 | ``` 653 | 654 | ```typescript 655 | check('abc', exactly('abc'), enUS) 656 | // null 657 | 658 | check('abd', exactly('abc'), enUS) 659 | // 'Must be abc' 660 | 661 | check('abd', exactly('abc', (x, y) => x.length === y.length), enUS) 662 | // null 663 | ``` 664 | 665 | ### `id` 666 | 667 | ```java 668 | id :: () => Schema_ 669 | ``` 670 | 671 | The identity schema that always succeeds. Unlike `any`, `id` can be provided a type 672 | argument other than the `any` type. 673 | 674 | ```typescript 675 | const schema = 676 | object({ foo: id() }) 677 | 678 | check({ foo: 123 }, schema, enUS) 679 | // null 680 | 681 | check({ foo: 'hi!' }, schema, enUS) 682 | // evaluates to null, but has a type error 683 | ``` 684 | 685 | ### `integer` 686 | 687 | Match a whole number 688 | 689 | ```java 690 | integer :: Schema_ 691 | ``` 692 | 693 | ### `length` 694 | 695 | Match an object with property length matching the schema argument 696 | 697 | ```java 698 | length :: (...vs : Schema[]) => Schema_ 699 | ``` 700 | 701 | ```typescript 702 | const username = 703 | string(length(exactly(10))) 704 | 705 | const items = 706 | array(between(1, 10)) 707 | ``` 708 | 709 | ### `lessThan` 710 | 711 | Match a number less than the provided upper bound `ub` 712 | 713 | ```java 714 | lessThan :: (ub : number) => Schema_ 715 | ``` 716 | 717 | ### `lift` 718 | 719 | Lift a function into a schema that uses the function for parsing. 720 | 721 | ```typescript 722 | const schema = 723 | pipe(toNumber, lift(x => x + 1)) 724 | 725 | result('123', schema) 726 | // 124 727 | ``` 728 | 729 | ### `match` 730 | 731 | Match a string matching the provided regular expression. 732 | 733 | ```typescript 734 | const greeting = 735 | match(/Hello|Hi|Hola/, m => m.mustBe('a greeting')) 736 | 737 | check('Hello', greeting, enUS) 738 | // null 739 | 740 | check('Yo', greeting, enUS) 741 | // 'Must be a greeting' 742 | ``` 743 | 744 | ### `moreThan` 745 | 746 | Match a number more than the provided lower bound `lb` 747 | 748 | ```java 749 | moreThan :: (lb : number) => Schema_ 750 | ``` 751 | 752 | ### `number` 753 | 754 | Match any number 755 | 756 | ```java 757 | number :: Schema_ 758 | ``` 759 | 760 | ### `objectExact` 761 | 762 | Like `object` but match the object exactly, i.e. error if additional 763 | keys to the ones specified are present. 764 | 765 | ### `objectInexact` 766 | 767 | Like `object` but match the object inexactly, i.e. whereas `object` 768 | will silently remove any keys not specified in the schema, 769 | `objectInexact` will keep them. 770 | 771 | ### `odd` 772 | 773 | Matches an odd number 774 | 775 | ```java 776 | odd :: Schema_ 777 | ``` 778 | 779 | ### `oneOf` 780 | 781 | Match exactly one of the given elements 782 | 783 | ```typescript 784 | const weekend = 785 | oneOf('Fri', 'Sat', 'Sun'), 786 | 787 | check('Sat', weekend, enUS) 788 | // null 789 | 790 | check('Wed', weekend, enUS) 791 | // Must be Fri, Sat or Sun 792 | ``` 793 | 794 | ### `swap` 795 | 796 | Swap elements 797 | 798 | ```java 799 | swap :: (dict : [[A, A]]) => Schema_ 800 | ``` 801 | 802 | ```typescript 803 | const optionalToEmptyString = 804 | swap([[undefined, ''], [null, '']]) 805 | 806 | result(null, optionalToEmptyString) 807 | // '' 808 | 809 | result(undefined, optionalToEmptyString) 810 | // '' 811 | 812 | result('foo', optionalToEmptyString) 813 | // 'foo' 814 | ``` 815 | 816 | ### `optionalTo` 817 | 818 | Map `null` and `undefined` to another value. 819 | 820 | ```java 821 | optionalTo :: (to : A) => Schema; 822 | ``` 823 | 824 | ```typescript 825 | const schema = 826 | pipe(optionalTo(''), length(atMost(3))) 827 | 828 | result(null, schema) 829 | // '' 830 | 831 | check(null, schema, enUS) 832 | // null' 833 | 834 | check('123123', schema, enUS) 835 | // 'Must have length at most 3.' 836 | ``` 837 | 838 | ### `pair` 839 | 840 | Create a schema for pairs or values from a pair of schemas (where a 841 | pair is a typed two-element array) 842 | 843 | ```java 844 | pair :: (v : Schema,w : Schema) => Schema_<[A, B], [C, D]> 845 | ``` 846 | 847 | ```typescript 848 | const schema = 849 | pair(toNumber, toDate) 850 | 851 | result(['123', '2019-12-12'], schema) 852 | // [ 123, 2019-12-12T00:00:00.000Z ] 853 | ``` 854 | 855 | ### `path` 856 | 857 | Set the `path` that a schema reports errors at. 858 | 859 | ```java 860 | path :: (path : string, v : Schema) => Schema 861 | ``` 862 | 863 | ```typescript 864 | const schema = 865 | path('foo', number) 866 | 867 | check('', schema, enUS) 868 | // 'Foo must be a number' 869 | ``` 870 | 871 | ### `size` 872 | 873 | `size` is the same as `length` except using the `size` property. Usable 874 | for sets etc. 875 | 876 | ```java 877 | size :: (...vs : Schema[]) => Schema_ 878 | ``` 879 | 880 | ### `string` 881 | 882 | Match a string 883 | 884 | ```java 885 | string :: Schema_ 886 | ``` 887 | 888 | ### `sum` 889 | 890 | Match an array with sum matching the schema argument. 891 | 892 | ```java 893 | sum :: (...vs : Schema[]) => Schema_ 894 | ``` 895 | 896 | ```typescript 897 | const schema = 898 | sum(atLeast(10)) 899 | 900 | check([1,2,3], schema, enUS) 901 | // Must have sum at least 10 902 | ``` 903 | 904 | 905 | ### `toDate` 906 | 907 | Convert a string to a date. Simply parses the string using the date 908 | constructor which can be unreliable, so you may want to use [date-fns](https://date-fns.org/) instead. 909 | 910 | ```java 911 | toDate :: Schema_ 912 | ``` 913 | 914 | ### `toJSON` 915 | 916 | Converts a string to JSON. 917 | 918 | ```java 919 | toJSON :: Schema_ 920 | ``` 921 | 922 | ### `toNumber` 923 | 924 | Converts a string to a number. 925 | 926 | ```java 927 | toNumber :: Schema_ 928 | ``` 929 | 930 | ### `toString` 931 | 932 | Converts a value to a string. 933 | 934 | ```java 935 | toString :: Schema_ 936 | ``` 937 | 938 | ### `toURL` 939 | 940 | Converts a string to an [URL](https://developer.mozilla.org/en-us/docs/Web/API/URL) object. 941 | 942 | ### `unknown` 943 | 944 | Successfully parses a value (same as `any`) but types it as unknown. 945 | 946 | ## Collection related schemas 947 | 948 | array • 949 | iterable • 950 | map • 951 | set • 952 | toArray • 953 | toMap • 954 | toMapFromObject • 955 | toSet 956 | 957 | ### `array` 958 | 959 | Create a schema on arrays from a schema on its values. 960 | 961 | ```java 962 | array :: (v : Schema,) : Schema_ 963 | ``` 964 | 965 | ```typescript 966 | const schema = 967 | array(toNumber) 968 | 969 | result(['1', '2', '3'], schema) 970 | // [1, 2, 3] 971 | 972 | check(['1', '2', true], schema, enUS) 973 | // Element #3 must be a string 974 | ``` 975 | ### `iterable` 976 | 977 | Match any `Iterable` value. 978 | 979 | ```java 980 | iterable :: () => Schema_, Iterable> 981 | ``` 982 | 983 | ```typescript 984 | const schema = 985 | iterable() 986 | 987 | check(['hello', 'world'], schema, enUS) 988 | // null 989 | ``` 990 | 991 | ### `map` 992 | 993 | Create a schema that matches a `Map` from schemas describing the keys 994 | and values respectively. 995 | 996 | ``` 997 | map :: (k : Schema, v : Schema) => Schema_, Map> 998 | ``` 999 | 1000 | ```typescript 1001 | const schema = 1002 | map(number(atLeast(10)), string(length(atLeast(1)))) 1003 | 1004 | check(new Map([[1, 'a'], [2, 'b'], [3, 'c']]), schema, enUS) 1005 | // Element #1.key must be at least 10 1006 | 1007 | check(new Map([[11, 'a'], [12, 'b'], [13, '']]), schema, enUS) 1008 | // Element #3.value must have length at least 1 1009 | 1010 | check(new Map([[11, 'a'], [12, 'b'], [13, 'c']]), schema, enUS) 1011 | // null 1012 | ``` 1013 | 1014 | ### `set` 1015 | 1016 | Create a schema for a Set from a schema describing the values of the set. 1017 | 1018 | ```java 1019 | set :: (v : Schema) => Schema_, Set> 1020 | ``` 1021 | 1022 | ```typescript 1023 | const schema = 1024 | set(any) 1025 | 1026 | check(new Set([1, 'a', true], schema, enUS) 1027 | // null 1028 | 1029 | check([1, 'a', true], schema, enUS) 1030 | // 'Must be a set' 1031 | ``` 1032 | 1033 | ```typescript 1034 | const schema = 1035 | set(toNumber) 1036 | 1037 | parse(new Set(['1', '2', '3'])) 1038 | // Set(3) { 1, 2, 3 } 1039 | ``` 1040 | 1041 | ### `toArray` 1042 | 1043 | Convert an iterable to an array. 1044 | 1045 | ```java 1046 | toArray :: () => Schema_, A[]> 1047 | ``` 1048 | 1049 | ### `toMap` 1050 | 1051 | Convert an iterable of pairs to a Map 1052 | 1053 | ```java 1054 | toMap :: () => Schema_, Map> 1055 | ``` 1056 | 1057 | ### `toMapFromObject` 1058 | 1059 | Convert an objet into a Map. 1060 | 1061 | ```java 1062 | toMapFromObject :: () : Schema_<{ [key in A]: B }, Map> 1063 | ``` 1064 | 1065 | ```typescript 1066 | result({ 'a': 3, 'b': 10, 'c': 9 }, toMapFromObject()) 1067 | // Map(3) { 'a' => 3, 'b' => 10, 'c' => 9 } 1068 | ``` 1069 | 1070 | It only works on "real" objects. 1071 | 1072 | ```typescript 1073 | check('', toMapFromObject(), enUS) 1074 | // 'Must be an object' 1075 | ``` 1076 | 1077 | ### `toSet` 1078 | 1079 | Convert an iterable of values into a set. 1080 | 1081 | ```java 1082 | toSet :: () : Schema_, Set> 1083 | ``` 1084 | 1085 | ## Factory functions 1086 | 1087 | Factory functions let you create new schema definitions. 1088 | 1089 | ### `createSchema` 1090 | 1091 | ```java 1092 | createSchema :: ( 1093 | parse : SchemaFactory, 1094 | unparse : SchemaFactory = irreversible('createSchema') 1095 | ) : Schema_ 1096 | ``` 1097 | 1098 | Create a schema from two "parser factories" for each "direction" of 1099 | parsing. (See [Bidirectionality](./src/docs/bidirectionali.md).) A 1100 | single factory may be provided, but the schema will not be invertible. 1101 | 1102 | A `SchemaFactory` is simply a function of type 1103 | ```typescript 1104 | type SchemaFactory = (a : A) => Action<{ 1105 | parse?: { ok : boolean, msg : string | Message }, 1106 | validate?: { ok : boolean, msg : string | Message }, 1107 | result?: B, 1108 | score?: number 1109 | }> 1110 | ``` 1111 | 1112 | All properties are optional and described below. 1113 | 1114 | ##### `result` 1115 | 1116 | Provide this if the schema performs any parsing. This is the result 1117 | value of the parsing. A schema performs parsing when it transforms the 1118 | input of the schema into something else, e.g. by transforming a string 1119 | representation of a date into a `Date`-object. 1120 | 1121 | ##### `parse` 1122 | 1123 | Provide this if the schema is doing any parsing. (See `result`) 1124 | 1125 | The `ok` parameter indicates whether the parse was 1126 | successful. `message` is the error message describing what the parser does. 1127 | 1128 | ##### `validate` 1129 | 1130 | Provide this if the schema is doing any validation. The `ok` parameter 1131 | indicates whether the validation was successful. `message` is the 1132 | error message describing what the parser does. 1133 | 1134 | ##### `score` 1135 | 1136 | The score is used by `bueno` to generate better error messages in 1137 | certain situations. You're most likely fine not providing it. 1138 | 1139 | You may however optionally proide a `score` between 0 and 1 to 1140 | indicate how successful the schema was. This will by default be either 1141 | 0 or 1 depending on whether the schema successfully handled its input 1142 | or not. 1143 | 1144 | An example of a schema that uses a non-binary score is 1145 | `array(number)`. If we ask this schema to handle the input 1146 | `[1,2,3,4,'5']` it will be ranked with a score of 4/5. 1147 | 1148 | Here's an example, creating a schema matching a "special" number. 1149 | 1150 | ```typescript 1151 | const special = number(createSchema( 1152 | async function(a : number) { 1153 | // `createSchema` may be async. 1154 | await new Promise(k => setTimeout(k, 100)) 1155 | return { 1156 | validate: { 1157 | ok: [2, 3, 5, 7, 8].indexOf(a) >= 0, 1158 | msg: (l : Builder) => l.mustBe('a special number')> 1159 | }, 1160 | // Transform the special number into a string. 1161 | result: '' + a 1162 | } 1163 | } 1164 | )) 1165 | ``` 1166 | 1167 | ## Types 1168 | 1169 | ### `Schema` 1170 | 1171 | The type of a schema. Converts a value of type `A` into one of type `B` 1172 | and validates the result. 1173 | 1174 | ### `Schema_` 1175 | 1176 | A `Schema` that can be used with "call syntax". An example of a 1177 | `Schema_` is `number`, and it can be enhanced by calling it 1178 | with additional arguments. 1179 | 1180 | ```typescript 1181 | const schema = 1182 | number(even, atLeast(12)) 1183 | ``` 1184 | 1185 | ### `Action` 1186 | 1187 | An `Action` is either an `A` or a `Promise`. A schema returning a 1188 | `Promise` will be asynchronous. 1189 | 1190 | 1191 | ### `Builder` 1192 | 1193 | A builder is an object that contains methods for building error 1194 | messages when using type-safe i18n. See [customizing 1195 | errors](./src/docs/customizing-errors.md) 1196 | 1197 | ### `Message` 1198 | 1199 | This type is used to create error messages that are independent of a 1200 | specific locale. 1201 | 1202 | It is a value of type `(l : MessageBuilder) => Rep`. I.e. it 1203 | uses a message builder to create a representation of an error 1204 | message. An example would be 1205 | 1206 | ```typescript 1207 | (l : MessageBuilder) => l.mustBe('a thingy!') 1208 | ``` 1209 | 1210 | (The `'a thingy!` is hard-coded to english here. We can extend the 1211 | grammar of `MessageBuilder` to accommodate this. See Customzing error messages) 1213 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: 'src', 3 | testRegex: '.*\.test\.ts', 4 | setupFilesAfterEnv: ['/__test__/setupTests.js'], 5 | transform: { 6 | "^.+\\.ts$": "ts-jest" 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bueno", 3 | "version": "0.1.5", 4 | "description": "Bueno - composable validators for forms, API:s and more.", 5 | "main": "dist/cjs/index.js", 6 | "module": "dist/esm/index.js", 7 | "files": [ 8 | "dist" 9 | ], 10 | "types": "dist/cjs/index.d.ts", 11 | "scripts": { 12 | "build": "yarn lint && yarn test && yarn clean && yarn build:prod && yarn build:iife", 13 | "clean": "rm -rf dist cdn", 14 | "lint": "eslint './src/**/*.{js,ts,tsx}' --quiet --fix", 15 | "build:iife": "rollup -c rollup.iife.config.js --environment NODE_ENV:production", 16 | "build:prod": "rollup -c --environment NODE_ENV:production", 17 | "pub": "yarn prod && yarn publish", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "watch": "rollup -cw" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/philipnilsson/bueno" 25 | }, 26 | "author": "Philip Nilsson", 27 | "license": "MIT", 28 | "private": false, 29 | "sideEffects": false, 30 | "devDependencies": { 31 | "@types/jest": "^25.1.2", 32 | "@typescript-eslint/eslint-plugin": "^3.7.0", 33 | "@typescript-eslint/parser": "^3.7.0", 34 | "eslint": "^7.5.0", 35 | "jest": "^26.1.0", 36 | "rollup": "^2.18.0", 37 | "rollup-plugin-replace": "^2.2.0", 38 | "rollup-plugin-terser": "^6.0.4", 39 | "rollup-plugin-typescript2": "^0.27.1", 40 | "ts-jest": "^26.1.4", 41 | "typescript": "^3.8.3" 42 | }, 43 | "resolutions": { 44 | "minimist": "1.2.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import { terser } from 'rollup-plugin-terser' 3 | import replace from 'rollup-plugin-replace' 4 | 5 | export default { 6 | input: __dirname + '/src/index.ts', 7 | 8 | output: [ 9 | { 10 | sourcemap: true, 11 | dir: "dist/esm", 12 | format: 'esm', 13 | }, 14 | { 15 | sourcemap: true, 16 | dir: "dist/cjs", 17 | format: 'cjs', 18 | }, 19 | ], 20 | 21 | preserveModules: true, 22 | 23 | plugins: [ 24 | typescript({ 25 | typescript: require('typescript'), 26 | }), 27 | replace({ 28 | 'run_': 'R', 29 | 'parse_': 'P', 30 | 'unparse_': 'U', 31 | 'validate_': 'V', 32 | 'result_': 'R', 33 | 'cnf_': 'C', 34 | 'res_': 'R', 35 | 'score_': 'S', 36 | 'article_': 'A', 37 | 'noun_': 'N', 38 | 'name_': 'M', 39 | 'pos_': 'P', 40 | 'neg_': 'N', 41 | 'passive_': 'P', 42 | 'irIs_': 'I', 43 | 'when_': 'W', 44 | 'shouldBe_': 'S', 45 | 'verbs_': 'V', 46 | 'disj_': 'C', 47 | 'IS_DEV': 'process.env.NODE_ENV !== "production"' 48 | }), 49 | process.env.NODE_ENV === 'production' && terser(), 50 | ], 51 | } 52 | -------------------------------------------------------------------------------- /rollup.iife.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | import { terser } from 'rollup-plugin-terser' 3 | import replace from 'rollup-plugin-replace' 4 | 5 | export default { 6 | input: __dirname + '/src/iife.ts', 7 | 8 | output: { 9 | file: "cdn/bueno.min.js", 10 | format: 'iife', 11 | }, 12 | 13 | plugins: [ 14 | typescript({ 15 | typescript: require('typescript'), 16 | }), 17 | replace({ 18 | 'run_': 'R', 19 | 'parse_': 'P', 20 | 'unparse_': 'U', 21 | 'validate_': 'V', 22 | 'result_': 'R', 23 | 'cnf_': 'C', 24 | 'res_': 'R', 25 | 'score_': 'S', 26 | 'article_': 'A', 27 | 'noun_': 'N', 28 | 'name_': 'M', 29 | 'pos_': 'P', 30 | 'neg_': 'N', 31 | 'passive_': 'P', 32 | 'irIs_': 'I', 33 | 'when_': 'W', 34 | 'shouldBe_': 'S', 35 | 'verbs_': 'V', 36 | 'disj_': 'C', 37 | 'IS_DEV': 'false' 38 | }), 39 | terser(), 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /src/API.md: -------------------------------------------------------------------------------- 1 | // CORE 2 | 3 | both 4 | either 5 | optional 6 | object 7 | compact 8 | self 9 | fix 10 | not 11 | some 12 | 13 | 14 | // HIGHER-ORDER VALIDATORS 15 | 16 | apply 17 | defaultTo 18 | flip 19 | array 20 | every 21 | 22 | // BUILT-IN VALIDATORS 23 | any 24 | alphaNumeric 25 | atLeast 26 | atMost 27 | between 28 | boolean 29 | 30 | chain 31 | date 32 | email 33 | emptyString 34 | even 35 | exactly 36 | forget 37 | id 38 | integer 39 | invalidDate 40 | irreversible 41 | length 42 | lessThan 43 | lift 44 | match 45 | moreThan 46 | number 47 | objectExact 48 | objectInexact 49 | odd 50 | oneOf 51 | optionalToEmptyString 52 | path 53 | pipe 54 | pure 55 | size 56 | string 57 | stringToDate 58 | sum 59 | toJSON 60 | toNumber 61 | toString 62 | toURL 63 | tuple 64 | unknown 65 | updateMessage 66 | setMessage 67 | updateNestedMessages 68 | when 69 | 70 | // COLLECTIONS 71 | iterable 72 | map 73 | set 74 | toArray 75 | toMap 76 | toMapFromObject 77 | toSet 78 | 79 | // FACTORIES 80 | createSchema 81 | mkSchema 82 | mkParser 83 | mkParserCallable 84 | mkParserHaving 85 | mkSchemaHaving 86 | mkRenderer 87 | 88 | // REPORTING 89 | result, 90 | resultAsync 91 | check, 92 | checkAsync 93 | checkPerKey, 94 | checkPerKeyAsync 95 | 96 | // LOCALES 97 | export { enUS, enUSOptions, English } from './locales/en-US' 98 | export { svSE, Swedish } from './locales/sv-SE' 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/Builtins.ts: -------------------------------------------------------------------------------- 1 | import { not, CNF } from './logic' 2 | import { Message, Err } from 'bueno/locale' 3 | 4 | export function message(x : Message) { 5 | return x 6 | } 7 | 8 | export function mapError(f : (m : Message) => Message) : (err : Err) => Err { 9 | return err => ({ 10 | k: err.k, 11 | ok: err.ok, 12 | m: f(err.m) 13 | }) 14 | } 15 | 16 | export const negate = (c : CNF) : CNF => not(c, (e : Err) => ({ 17 | k: e.k, 18 | ok: !e.ok, 19 | m: l => l.not(e.m(l)) 20 | })) 21 | 22 | declare module 'bueno/locale' { 23 | export interface MessageBuilder { 24 | mustBe(r : string | IR) : IR; 25 | mustHave(r : string | IR) : IR; 26 | has(name : string) : (ir : IR) => IR; 27 | 28 | length(r : IR) : IR; 29 | sum(r : IR) : IR; 30 | more(lb : A) : IR; 31 | less(ub : A) : IR; 32 | between(lb : A, ub : A) : IR; 33 | atMost(ub : A) : IR; 34 | atLeast(lb : A) : IR; 35 | 36 | oneOf(n : A[]) : IR; 37 | exactly(n : A) : IR; 38 | keys(keys : string[]) : IR; 39 | number : IR; 40 | integer : IR; 41 | string : IR; 42 | email : IR; 43 | alphanum : IR, 44 | url : IR; 45 | object : IR; 46 | leftOut : IR; 47 | json : IR; 48 | uuid : IR; 49 | iterable : IR; 50 | bool : IR; 51 | array : IR; 52 | set : IR; 53 | map : IR; 54 | date : IR; 55 | even : IR; 56 | odd : IR; 57 | empty : IR; 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/DefaultIR.ts: -------------------------------------------------------------------------------- 1 | import { deepMerge, mapValues, mapEntries, assignPath, byEmpty, isString, isEmpty, keys, fromEntries, isNumber } from './utils' 2 | import { joinPath, defaultRenderPath } from './path' 3 | import { GroupedByKey, MessageRenderer } from 'bueno/locale' 4 | import { Paths } from 'bueno/locale' 5 | import { VerbBuilder } from 'bueno/locale' 6 | 7 | type VerbVariant = { 8 | pos_ : VerbBuilder, 9 | neg_ : VerbBuilder 10 | } 11 | 12 | export type Verb = { 13 | shouldBe_ : VerbVariant, 14 | when_ : VerbVariant, 15 | passive_ : VerbVariant, 16 | irIs_ : A[], 17 | irIsNot_ : A[], 18 | } 19 | 20 | export type Message = { 21 | k : 'msg', 22 | disj_ : { 23 | [byPath in string]: { 24 | verbs_ : { [byVerb in string]: Verb } 25 | } 26 | } 27 | } 28 | 29 | function msg(disj_ : Message['disj_']) : Message { 30 | return { k: 'msg' as const, disj_ } 31 | } 32 | 33 | type Implication = { 34 | k : 'when', 35 | cond : IR[], 36 | conseq : IR 37 | } 38 | 39 | export type IR = 40 | Message | Implication 41 | 42 | 43 | export function pickVerb( 44 | group : Verb, 45 | path : string, 46 | inWhen : boolean, 47 | negated : boolean 48 | ) : VerbBuilder { 49 | if (inWhen) { 50 | if (path) { 51 | return negated ? group.when_.neg_ : group.when_.pos_ 52 | } 53 | return negated ? group.passive_.neg_ : group.passive_.pos_ 54 | } 55 | return negated ? group.shouldBe_.neg_ : group.shouldBe_.pos_ 56 | } 57 | 58 | export type RenderingOptions = { 59 | fromstr : (s : string) => Out, 60 | or : (words : Out[]) => Out, 61 | and : (words : Out[]) => Out, 62 | noun : (noun : string, name : string, type : string) => Out, 63 | when : (conseq : Out, cond : Out) => Out, 64 | elem : (path : (string | number)[]) => Out, 65 | path : (path : string[]) => Out, 66 | atom : (ir : A[], props : RenderingOptions) => Out, 67 | words : (o : Out[]) => Out, 68 | pathAtKey : ( 69 | path : Out, 70 | verb : Out, 71 | byKey : boolean 72 | ) => Out, 73 | finalize : (o : Out) => Out 74 | } 75 | 76 | export type Context = { 77 | atPath : string | null 78 | } 79 | 80 | export function mkRenderer( 81 | props : RenderingOptions 82 | ) : MessageRenderer, Out> { 83 | function renderWhen( 84 | cond : IR[], 85 | conseq : IR, 86 | paths : Paths, 87 | context : Context 88 | ) : Out { 89 | if (isEmpty(cond)) { 90 | return clause(conseq, false, paths, context) 91 | } 92 | const conds = cond.map(ir => clause(ir, true, paths, { atPath: null })) 93 | return props.when(clause(conseq, false, paths, context), props.and(conds)) 94 | } 95 | 96 | function renderPath(pth : string, paths : Paths) : Out { 97 | return defaultRenderPath(paths, pth.split('.'), function(pth : (string | number)[]) : Out { 98 | if (pth.some(isNumber)) { 99 | return props.elem(pth) 100 | } 101 | return props.path(pth as string[]) 102 | }) 103 | } 104 | 105 | function clause( 106 | ir : IR, 107 | inWhen : boolean, 108 | paths : Paths, 109 | context : Context 110 | ) : Out { 111 | if (ir.k === 'when') { 112 | return renderWhen(ir.cond, ir.conseq, paths, context) 113 | } 114 | else { 115 | const clauses : Out[][] = [] 116 | keys(ir.disj_).sort(byEmpty).forEach(path => { 117 | const byPath = ir.disj_[path] 118 | const verbs = byPath.verbs_ 119 | keys(verbs).forEach(verb => { 120 | const group = verbs[verb]; 121 | const checks = [group.irIs_, group.irIsNot_] 122 | checks.forEach((xs, i) => { 123 | if (isEmpty(xs)) return 124 | const pth = 125 | renderPath(path, paths) 126 | 127 | const verb = 128 | pickVerb(group, path, inWhen, i !== 0) 129 | 130 | const words = 131 | verb(props.atom(xs, props)).map(x => isString(x) 132 | ? props.fromstr(x as string) 133 | : x as Out 134 | ) 135 | 136 | const hasPath = 137 | path === context.atPath 138 | 139 | clauses.push([ 140 | props.pathAtKey(pth, props.words(words), hasPath) 141 | ]) 142 | }) 143 | }) 144 | }) 145 | return props.or(clauses.map(props.words)); 146 | } 147 | } 148 | 149 | function render( 150 | input : IR, 151 | paths : Paths, 152 | context : Context = { atPath: null } 153 | ) : Out { 154 | return props.finalize(clause(input, false, paths, context)) 155 | } 156 | 157 | function relevantKeys(ir : IR) : string[] { 158 | if (ir.k === 'msg') { 159 | return keys(ir.disj_) 160 | } else { 161 | return relevantKeys(ir.conseq) 162 | } 163 | } 164 | 165 | function onlyPath(ir : IR, path : string) : IR { 166 | if (ir.k === 'msg') { 167 | if (ir.disj_[path]) { 168 | return msg({ [path]: ir.disj_[path] }) 169 | } 170 | return ir 171 | } 172 | return buildWhen( 173 | onlyPath(ir.conseq, path), 174 | ir.cond.map(x => onlyPath(x, path)), 175 | ) 176 | } 177 | 178 | function byKey(ir : IR, paths : Paths) : GroupedByKey { 179 | if (ir.k === 'msg') { 180 | const result : GroupedByKey = {} 181 | keys(ir.disj_).map(path => { 182 | const segments = path.split('.') 183 | const disj : IR[] = keys(ir.disj_) 184 | .filter(key => key !== path) 185 | .map(key => 186 | not(msg({ [key]: (ir.disj_[key]) }))) 187 | 188 | const context = { atPath: path } 189 | 190 | const conseq : IR = 191 | onlyPath(ir, path) 192 | 193 | assignPath(result, segments, props.finalize(renderWhen(disj, conseq, paths, context))) 194 | }) 195 | return result 196 | } else { 197 | return fromEntries( 198 | relevantKeys(ir.conseq).map(path => { 199 | return [ 200 | path, 201 | render(ir, paths, { atPath: path }) 202 | ] as [string, Out] 203 | }).concat([['', render(ir, paths, { atPath: null })]]) 204 | ) 205 | } 206 | } 207 | return { 208 | render, 209 | byKey, 210 | } 211 | } 212 | 213 | function mapVerbs( 214 | f : (inp : Message['disj_']['']) => Message['disj_'][''] 215 | ) : (ir : IR) => IR { 216 | const mapper = (ir : IR) : IR => { 217 | if (ir.k == 'msg') { 218 | return msg(mapValues(ir.disj_, f)) 219 | } 220 | else { 221 | return buildWhen( 222 | mapper(ir.conseq), 223 | ir.cond.map(mapper) 224 | ) 225 | } 226 | } 227 | return mapper 228 | } 229 | 230 | function adapt(f : string | VerbBuilder) : VerbBuilder { 231 | if (isString(f)) { 232 | return x => [f as string, x] 233 | } 234 | return f as VerbBuilder 235 | } 236 | 237 | export function createVerb( 238 | is : VerbBuilder, 239 | not : VerbBuilder, 240 | when : VerbBuilder, 241 | whenNot : VerbBuilder, 242 | passive : VerbBuilder, 243 | passiveNot : VerbBuilder, 244 | ) : Verb { 245 | return { 246 | shouldBe_: { pos_: is, neg_: not }, 247 | when_: { pos_: when, neg_: whenNot }, 248 | passive_: { pos_: passive, neg_: passiveNot }, 249 | irIs_: [], 250 | irIsNot_: [], 251 | } 252 | } 253 | 254 | export function setVerb(verb : Verb) : (ir : IR) => IR { 255 | return mapVerbs((byPath => { 256 | const fn = (_key : string | number | symbol, y : Verb) : [string | number | symbol, Verb] => { 257 | const name = 258 | verb.shouldBe_.pos_('').join('') 259 | return [ 260 | name, 261 | { ...verb, irIs_: y.irIs_, irIsNot_: y.irIsNot_ } 262 | ] 263 | } 264 | return ({ 265 | verbs_: mapEntries(byPath.verbs_, fn) 266 | }) 267 | })) 268 | } 269 | 270 | const n : VerbBuilder = 271 | x => [x] 272 | 273 | const noVerb = 274 | createVerb(n, n, n, n, n, n) 275 | 276 | export function atom(atom : A) : IR { 277 | return msg({ 278 | '': { verbs_: { '': { ...noVerb, irIs_: [atom] } } } 279 | }) 280 | } 281 | 282 | function fromString(noun_ : string, name_ : string = noun_) : IR { 283 | return atom({ article_: '', noun_, k: 'noun_', name_ }) 284 | } 285 | 286 | function buildWhen(conseq : IR, cond : IR[]) : IR { 287 | return { 288 | k: 'when', 289 | cond, 290 | conseq 291 | } 292 | } 293 | 294 | export function not(conj : IR) : IR { 295 | if (conj.k === 'msg') { 296 | const a = conj.disj_ 297 | return { 298 | k: 'msg', 299 | disj_: mapValues(a, v => { 300 | return ({ 301 | verbs_: mapValues(v.verbs_, x => ({ 302 | ...x, 303 | irIs_: x.irIsNot_, 304 | irIsNot_: x.irIs_ 305 | })) 306 | }) 307 | }) 308 | } as IR 309 | } else { 310 | return buildWhen(not(conj.conseq), conj.cond) 311 | } 312 | } 313 | 314 | export function at(pfx : K, r : IR) : IR { 315 | function atGroup(r : Message['disj_']) { 316 | const result : any = {} 317 | keys(r).forEach(key => { 318 | result[joinPath(pfx, key)] = (r as any)[key] 319 | }) 320 | return result as any 321 | } 322 | if (r.k === 'msg') { 323 | return msg(atGroup(r.disj_)) 324 | } else { 325 | return buildWhen( 326 | at(pfx, r.conseq), 327 | r.cond.map(c => at(pfx, c)) 328 | ) 329 | } 330 | } 331 | 332 | export function verb( 333 | is : VerbBuilder | string, 334 | not : VerbBuilder | string, 335 | when : VerbBuilder | string = is, 336 | whenNot : VerbBuilder | string = not, 337 | passive : VerbBuilder | string = when, 338 | passiveNot : VerbBuilder | string = whenNot, 339 | ) : (msgs : string | IR, name?: string) => IR { 340 | const setv = 341 | setVerb( 342 | createVerb( 343 | adapt(is), 344 | adapt(not), 345 | adapt(when), 346 | adapt(whenNot), 347 | adapt(passive), 348 | adapt(passiveNot) 349 | ) 350 | ) 351 | 352 | return (x : string | IR, name?: string) => 353 | setv( 354 | isString(x) 355 | ? fromString(x as string, name || x as string) as IR 356 | : (x as IR) 357 | ); 358 | } 359 | 360 | export const defaultBuilder = { 361 | 362 | not, 363 | 364 | either: deepMerge, 365 | 366 | when: buildWhen, 367 | 368 | at, 369 | 370 | fromString, 371 | 372 | atEvery(rs : IR) : IR { 373 | return at('every element', rs) 374 | }, 375 | 376 | verb 377 | } 378 | -------------------------------------------------------------------------------- /src/Language.ts: -------------------------------------------------------------------------------- 1 | declare module 'bueno/locale' { 2 | 3 | 4 | // Eslint wants to replace with with 5 | // export type MessageBuilder = MessageBuilderBase 6 | // which it thinks is equivalent, but isn't when you take 7 | // declaration merging into account. 8 | 9 | // eslint-disable-next-line 10 | export interface MessageBuilder extends MessageBuilderBase { 11 | } 12 | 13 | export type VerbBuilder = 14 | (s : any) => any[]; 15 | 16 | export interface MessageBuilderBase { 17 | not(ir : IR) : IR; 18 | either(disj : IR[]) : IR; 19 | when(conseq : IR, cond : IR[]) : IR; 20 | at(pfx : K, rs : IR) : IR; 21 | atEvery(rs : IR) : IR; 22 | fromString(s : string) : IR; 23 | verb( 24 | is : VerbBuilder, 25 | not : VerbBuilder, 26 | when?: VerbBuilder, 27 | whenNot?: VerbBuilder, 28 | passive?: VerbBuilder, 29 | passiveNot?: VerbBuilder, 30 | ) : (msgs : string | IR) => IR 31 | } 32 | 33 | export type GroupedByKey = 34 | | { [key in string]: GroupedByKey } 35 | | Output 36 | 37 | export type Paths = [string, Out][] 38 | 39 | export type MessageRenderer = MessageRendererBase 40 | 41 | export interface MessageRendererBase { 42 | render(ir : IR, paths : [string, Output][]) : Output; 43 | byKey(ir : IR, paths : [string, Output][]) : GroupedByKey 44 | } 45 | 46 | export interface Language { 47 | renderer : MessageRenderer 48 | builder : MessageBuilder 49 | } 50 | 51 | export type Message = 52 | (l : MessageBuilder) => IR 53 | 54 | export type Err = { 55 | k : 'v' | 'p', 56 | m : Message, 57 | ok : boolean 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/__test__/currency.ts: -------------------------------------------------------------------------------- 1 | import { object, oneOf, toNumber, pipe, lift } from '../index' 2 | 3 | export class Currency { 4 | amount : number 5 | currency : string 6 | 7 | constructor( 8 | amount : number, 9 | currency : string 10 | ) { 11 | this.amount = amount 12 | this.currency = currency 13 | } 14 | 15 | format() { 16 | return this.currency === 'USD' 17 | ? `$${this.amount}` 18 | : `${this.amount} kr` 19 | } 20 | } 21 | 22 | export const amount = object({ 23 | amount: toNumber, 24 | currency: oneOf('USD', 'SEK') 25 | }) 26 | 27 | export const currency = pipe( 28 | amount, 29 | lift( 30 | ({ amount, currency }) => 31 | new Currency(amount, currency).format(), 32 | currency => { 33 | if (currency.startsWith('$')) { 34 | return { currency: 'USD', amount: parseFloat(currency.substring(1)) } 35 | } else { 36 | return { currency: 'SEK', amount: parseFloat(currency.substring(0, currency.length - 3)) } 37 | } 38 | }) 39 | ) 40 | -------------------------------------------------------------------------------- /src/__test__/english.test.ts: -------------------------------------------------------------------------------- 1 | import { testInputs, TestName } from './testinputs' 2 | import { enUS } from '../locales/en-US' 3 | import { MessageBuilder } from 'bueno/locale' 4 | import { describePaths } from '../path' 5 | import { Locale } from '../index' 6 | 7 | const lang = describePaths( 8 | enUS as Locale, 9 | [ 10 | ['*.phoneNo', 'phone number'], 11 | ['*.age', 'age'], 12 | ['*.bar.baz', 'barbaz'], 13 | ], 14 | ) 15 | 16 | const inputs = 17 | testInputs(lang.builder) 18 | 19 | const outputs : { [name in keyof TestName]: { [key in TestName[name]]: string } } = { 20 | messages: { 21 | 'no path': 22 | 'Must be a number', 23 | 24 | 'no path / negated': 25 | 'Must not be a number', 26 | 27 | 'simple path': 28 | 'Bar must be a number', 29 | 30 | 'simple path / negated': 31 | 'Bar must not be a number', 32 | 33 | 'or / mixed path': 34 | 'Must be a number or bar must be a string', 35 | 36 | 'or / multiple paths': 37 | 'Baz must be a number or bar must be a string', 38 | 39 | 'or / multiple no path': 40 | 'Must be a number or string', 41 | 42 | 'or / negated / multiple no path': 43 | 'Must not be a number or string', 44 | 45 | 'or / negated / multiple no path / same article': 46 | 'Must not be a number or object', 47 | 48 | 'or / multiple same path': 49 | 'Bar must be a number, string or object, baz must be a string or baz must not be an object', 50 | 51 | 'or / multiple different paths': 52 | 'Barbaz must be a number or object, baz must be true or false or baz must not be a string or object', 53 | 54 | 'or / multiple different paths / empty path': 55 | 'Must be true or false, must not be a string or object or bar must be a number or object', 56 | 57 | 'or / multiple different paths / empty path / adjectives': 58 | 'Must be more than 10 or more than 12 or must not be less than 5', 59 | 60 | 'at every': 61 | 'Every element must be more than 10', 62 | 63 | 'alpha-numeric': 64 | 'Must have letters and numbers only' 65 | }, 66 | having: { 67 | 'sum': 68 | 'Must have sum at least 10', 69 | 70 | 'length': 71 | 'Must have length at least 10', 72 | 73 | 'unexpected keys': 74 | 'Must not have unexpected keys foo, bar and baz', 75 | }, 76 | when: { 77 | 'simple': 78 | 'Must be more than 10 when bar is a string', 79 | 80 | 'with disjunctions': 81 | "Must be more than 10 or a string or phone number must be a number when not more than 3 or an object, age is more than 18 and age is less than 65", 82 | 83 | 'when / length': 84 | 'Must be even when having length at least 10', 85 | 86 | 'when / between': 87 | 'Must be even when foobar is between 10 and 20', 88 | 89 | 'when / left out': 90 | 'FOO must be even when BAR is left out', 91 | }, 92 | custom: { 93 | 'simple': 94 | 'Should be a custom thingy!' 95 | } 96 | } 97 | 98 | Object.keys(inputs).forEach(key => { 99 | describe(key, () => { 100 | ((inputs as any)[key] as any[]).forEach((input : any) => { 101 | test(input.name, () => { 102 | expect(lang.renderer.render(input.input, [])).toEqual((outputs as any)[key][input.name]) 103 | }) 104 | }) 105 | }) 106 | }) 107 | 108 | describe('by key', () => { 109 | 110 | test('by key 1', () => { 111 | 112 | const msg = (l : MessageBuilder) => l.either([ 113 | l.at('age', l.more(18)), 114 | l.at('name', l.less(65)), 115 | l.at('verified', l.exactly(true)) 116 | ]) 117 | 118 | expect(lang.renderer.byKey(msg(lang.builder), [])).toEqual({ 119 | age: 'Must be more than 18 when name is not less than 65 and verified is not true', 120 | name: 'Must be less than 65 when age is not more than 18 and verified is not true', 121 | verified: 'Must be true when age is not more than 18 and name is not less than 65' 122 | }) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /src/__test__/german.test.ts: -------------------------------------------------------------------------------- 1 | import { testInputs, TestName } from './testinputs' 2 | import { deDE } from '../locales/de-DE' 3 | import { MessageBuilder } from 'bueno/locale' 4 | import { describePaths } from '../path' 5 | import { Locale } from '../index' 6 | 7 | const lang = describePaths( 8 | deDE as Locale, 9 | [ 10 | ['*.phoneNo', 'Telefonzahl'], 11 | ['*.age', 'alt'], 12 | ['*.bar.baz', 'barbaz'], 13 | ] 14 | ) 15 | 16 | const inputs = 17 | testInputs(lang.builder) 18 | 19 | const outputs : { [name in keyof TestName]: { [key in TestName[name]]: string } } = { 20 | messages: { 21 | 'no path': 22 | 'Muss eine Zahl sein', 23 | 24 | 'no path / negated': 25 | 'Darf nicht eine Zahl sein', 26 | 27 | 'simple path': 28 | 'Bar muss eine Zahl sein', 29 | 30 | 'simple path / negated': 31 | 'Bar darf nicht eine Zahl sein', 32 | 33 | 'or / mixed path': 34 | 'Muss eine Zahl sein oder bar muss ein Text sein', 35 | 36 | 'or / multiple paths': 37 | 'Baz muss eine Zahl sein oder bar muss ein Text sein', 38 | 39 | 'or / multiple no path': 40 | 'Muss eine Zahl oder ein Text sein', 41 | 42 | 'or / negated / multiple no path': 43 | 'Darf nicht eine Zahl oder ein Text sein', 44 | 45 | 'or / negated / multiple no path / same article': 46 | 'Darf nicht eine Zahl oder ein Objekt sein', 47 | 48 | 'or / multiple same path': 49 | 'Bar muss eine Zahl, ein Text oder Objekt sein, ' + 50 | 'baz muss ein Text sein oder baz darf nicht ein Objekt sein', 51 | 52 | 'or / multiple different paths': 53 | 'Barbaz muss eine Zahl oder ein Objekt sein, baz muss wahr oder falsch sein oder baz darf nicht ein Text oder Objekt sein', 54 | 55 | 'or / multiple different paths / empty path': 56 | 'Muss wahr oder falsch sein, darf nicht ein Text oder Objekt sein oder bar muss eine Zahl oder ein Objekt sein', 57 | 58 | 'or / multiple different paths / empty path / adjectives': 59 | 'Muss mehr als 10 oder mehr als 12 sein oder darf nicht weniger als 5 sein', 60 | 61 | 'at every': 62 | 'Jedes Element muss mehr als 10 sein', 63 | 64 | 'alpha-numeric': 65 | 'Muss nur aus Buchstaben und Zahlen bestehen' 66 | }, 67 | having: { 68 | 'sum': 69 | 'Muss eine Summe von mindestens 10 haben', 70 | 71 | 'length': 72 | 'Muss eine Länge von mindestens 10 haben', 73 | 74 | 'unexpected keys': 75 | 'Darf nicht unerwarteten Schlüssel foo, bar und baz haben' 76 | }, 77 | when: { 78 | 'simple': 79 | 'Muss mehr als 10 sein wenn bar ein Text ist', 80 | 81 | 'with disjunctions': 82 | 'Muss mehr als 10 oder ein Text sein oder Telefonzahl muss eine Zahl sein wenn nicht mehr als 3 oder ein Objekt ist, alt mehr als 18 ist und alt weniger als 65 ist', 83 | 84 | 'when / length': 85 | 'Muss gerade sein wenn es eine Länge von mindestens 10 hat', 86 | 87 | 'when / between': 88 | 'Muss gerade sein wenn foobar zwischen 10 und 20 liegt', 89 | 90 | 'when / left out': 91 | 'FOO muss gerade sein wenn BAR weggelassen wird' 92 | }, 93 | 94 | custom: { 95 | 'simple': 96 | 'Should be a custom thingy!' 97 | } 98 | } 99 | 100 | Object.keys(inputs).forEach(key => { 101 | describe(key, () => { 102 | ((inputs as any)[key] as any[]).forEach((input : any) => { 103 | test(input.name, () => { 104 | expect(lang.renderer.render(input.input, [])).toEqual((outputs as any)[key][input.name]) 105 | }) 106 | }) 107 | }) 108 | }) 109 | 110 | describe('by key', () => { 111 | test('by key 1', () => { 112 | const msg = (l : MessageBuilder) => l.either([ 113 | l.at('alt', l.more(18)), 114 | l.at('name', l.less(65)), 115 | l.at('verifiziert', l.exactly('"Ja"')) 116 | ]) 117 | 118 | expect(lang.renderer.byKey(msg(lang.builder), [])).toEqual({ 119 | "alt": "Muss mehr als 18 sein wenn name nicht weniger als 65 ist und verifiziert nicht \"Ja\" ist", 120 | "name": "Muss weniger als 65 sein wenn alt nicht mehr als 18 ist und verifiziert nicht \"Ja\" ist", 121 | "verifiziert": "Muss \"Ja\" sein wenn alt nicht mehr als 18 ist und name nicht weniger als 65 ist", 122 | }) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /src/__test__/logic.test.ts: -------------------------------------------------------------------------------- 1 | import { CNF, or, and } from "../logic" 2 | 3 | describe('logic', () => { 4 | test('or', () => { 5 | const x : CNF = [ 6 | ['a', 'b', 'c'], 7 | ['d', 'e'] 8 | ] 9 | const y : CNF = [ 10 | ['x', 'y', 'z'], 11 | ['u', 'v'] 12 | ] 13 | 14 | expect(or(x, y)).toEqual([ 15 | ['a', 'b', 'c', 'x', 'y', 'z'], 16 | ['a', 'b', 'c', 'u', 'v'], 17 | ['d', 'e', 'x', 'y', 'z'], 18 | ['d', 'e', 'u', 'v'] 19 | ]) 20 | }) 21 | 22 | test('and / true / id left', () => { 23 | expect(and([], [['a', 'b'], ['c']])).toEqual( 24 | [['a', 'b'], ['c']] 25 | ) 26 | }) 27 | 28 | test('and / true / id right', () => { 29 | expect(and([['a', 'b'], ['c']], [])).toEqual( 30 | [['a', 'b'], ['c']] 31 | ) 32 | }) 33 | 34 | test('and / false / abs left', () => { 35 | expect(and([[]], [['a', 'b'], ['c']])).toEqual( 36 | [[]] 37 | ) 38 | }) 39 | 40 | test('and / false / abs right', () => { 41 | expect(and([['a', 'b'], ['c']], [[]])).toEqual( 42 | [[]] 43 | ) 44 | }) 45 | 46 | test('or / true / abd left', () => { 47 | expect(or([], [['a', 'b'], ['c']])).toEqual( 48 | [] 49 | ) 50 | }) 51 | 52 | test('or / true / abs right', () => { 53 | expect(or([['a', 'b'], ['c']], [])).toEqual( 54 | [] 55 | ) 56 | }) 57 | 58 | test('or / false / id left', () => { 59 | expect(or([[]], [['a', 'b'], ['c']])).toEqual( 60 | [['a', 'b'], ['c']] 61 | ) 62 | }) 63 | 64 | test('or / false / id right', () => { 65 | expect(or([['a', 'b'], ['c']], [[]])).toEqual( 66 | [['a', 'b'], ['c']] 67 | ) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/__test__/mapped-parsers.test.ts: -------------------------------------------------------------------------------- 1 | import { currency } from './currency' 2 | import { run } from './run' 3 | 4 | describe('mapping objects and using as parsers', () => { 5 | test('parse / valid', async () => { 6 | expect( 7 | await run(currency.parse_, { 8 | amount: '188.00e0', 9 | currency: 'USD' 10 | }) 11 | ).toEqual([ 12 | [], 13 | '$188', 14 | 1 15 | ]) 16 | }) 17 | 18 | test('parse / invalid', async () => { 19 | expect( 20 | await run(currency.parse_, { 21 | amount: '188.00', 22 | currency: 'DKK' 23 | }) 24 | ).toEqual([ 25 | ['Currency must be one of USD or SEK'], 26 | '$188', 27 | 0.75 28 | ]) 29 | }) 30 | 31 | test('parse / both invalid', async () => { 32 | expect( 33 | await run(currency.parse_, { amount: '188k', currency: 'DKK' }) 34 | ).toEqual([ 35 | [ 36 | 'Amount must be a number', 37 | 'Currency must be one of USD or SEK' 38 | ], 39 | '$0', 40 | 0.625 41 | ]) 42 | }) 43 | 44 | test('unparse / ok', async () => { 45 | expect( 46 | await run(currency.unparse_, '$0') 47 | ).toEqual([ 48 | [], 49 | { amount: '0', currency: 'USD' }, 50 | 1 51 | ]) 52 | }) 53 | 54 | test('unparse / ok 2', async () => { 55 | expect( 56 | await run(currency.unparse_, '123 kr') 57 | ).toEqual([ 58 | [], 59 | { amount: '123', currency: 'SEK' }, 60 | 1 61 | ]) 62 | }) 63 | }) 64 | -------------------------------------------------------------------------------- /src/__test__/run.ts: -------------------------------------------------------------------------------- 1 | import { enUS } from '../locales/en-US'; 2 | import { Parser, Schema } from '../types'; 3 | import { result } from '../schema/runners/result'; 4 | import { check } from '../schema/runners/check'; 5 | import { checkPerKey, Errors } from '../schema/runners/checkByKey'; 6 | import { all, chain, Action } from '../action'; 7 | import { sync } from '../schema/runners/sync'; 8 | import { flip } from '../schema/flip'; 9 | import { Locale } from '../index'; 10 | 11 | export async function run( 12 | v : Parser, a : unknown 13 | ) : Promise<[string[], B, number]> { 14 | const c = 15 | await v.run_(a as A, false); 16 | 17 | const cnf_ 18 | = await c.cnf_(); 19 | 20 | const res_ = 21 | await c.res_(); 22 | 23 | const simplified = 24 | cnf_.filter(x => !x.some(y => y.ok)); 25 | 26 | const build = 27 | simplified.map(disj => { 28 | const lang = enUS as Locale 29 | return enUS.builder.either(disj.map(x => x.m(lang.builder))); 30 | }) 31 | 32 | const msg = 33 | build.map(x => enUS.renderer.render(x, [])) 34 | 35 | return [msg, res_, c.score_]; 36 | } 37 | 38 | export function parseAsync( 39 | value : A, 40 | schema : Schema, 41 | language : Locale, 42 | ) : Action<{ 43 | result : B, 44 | error : string | null, 45 | errorsByKey : Errors 46 | }> { 47 | const results = chain(all([ 48 | result(value, schema), 49 | check(value, schema, language), 50 | checkPerKey(value, schema, language), 51 | ]) as any, 52 | (kvs : [B, string | null, Errors]) => ({ 53 | result: kvs[0], 54 | error: kvs[1], 55 | errorsByKey: kvs[2] 56 | }) 57 | ) 58 | return results 59 | } 60 | 61 | export const parse = 62 | sync(parseAsync) 63 | 64 | export const unparse = 65 | sync((v : B, s : Schema, l : Locale) => parseAsync(v, flip(s), l)) 66 | -------------------------------------------------------------------------------- /src/__test__/setupTests.js: -------------------------------------------------------------------------------- 1 | global.IS_DEV = false 2 | -------------------------------------------------------------------------------- /src/__test__/spanish.test.ts: -------------------------------------------------------------------------------- 1 | import { testInputs, TestName } from './testinputs' 2 | import { esES } from '../locales/es-ES' 3 | import { MessageBuilder } from 'bueno/locale' 4 | import { describePaths } from '../path' 5 | import { Locale } from '../index' 6 | 7 | const lang = describePaths( 8 | esES as Locale, 9 | [ 10 | ['*.phoneNo', 'telefonnummer'], 11 | ['*.age', 'ålder'], 12 | ['*.bar.baz', 'barbaz'], 13 | ] 14 | ) 15 | 16 | const inputs = 17 | testInputs(lang.builder) 18 | 19 | const outputs : { [name in keyof TestName]: { [key in TestName[name]]: string } } = { 20 | messages: { 21 | 'no path': 22 | 'Debe ser un número', 23 | 24 | 'no path / negated': 25 | 'No debe ser un número', 26 | 27 | 'simple path': 28 | 'Bar debe ser un número', 29 | 30 | 'simple path / negated': 31 | 'Bar no debe ser un número', 32 | 33 | 'or / mixed path': 34 | 'Debe ser un número o bar debe ser un texto', 35 | 36 | 'or / multiple paths': 37 | 'Baz debe ser un número o bar debe ser un texto', 38 | 39 | 'or / multiple no path': 40 | 'Debe ser un número o texto', 41 | 42 | 'or / negated / multiple no path': 43 | 'No debe ser un número o texto', 44 | 45 | 'or / negated / multiple no path / same article': 46 | 'No debe ser un número o objeto', 47 | 48 | 'or / multiple same path': 49 | 'Bar debe ser un número, texto o objeto, baz debe ser un texto o baz no debe ser un objeto', 50 | 51 | 'or / multiple different paths': 52 | 'Barbaz debe ser un número o objeto, baz debe ser cierto o falso o baz no debe ser un texto o objeto', 53 | 54 | 'or / multiple different paths / empty path': 55 | 'Debe ser cierto o falso, no debe ser un texto o objeto o bar debe ser un número o objeto', 56 | 57 | 'or / multiple different paths / empty path / adjectives': 58 | 'Debe ser más que 10 o más que 12 o no debe ser menor que 5', 59 | 60 | 'at every': 61 | 'Cada elemento debe ser más que 10', 62 | 'alpha-numeric': 63 | 'Debe ser solo números y letras', 64 | }, 65 | having: { 66 | 'sum': 67 | 'Debe tener una suma de al menos 10', 68 | 69 | 'length': 70 | 'Debe tener una longitud de al menos 10', 71 | 72 | 'unexpected keys': 73 | 'No debe tener llaves inesperadas foo, bar y baz' 74 | }, 75 | when: { 76 | 'simple': 77 | 'Debe ser más que 10 cuando bar es un texto', 78 | 79 | 'with disjunctions': 80 | 'Debe ser más que 10 o un texto o telefonnummer debe ser un número cuando no más que 3 o un objeto, ålder es más que 18 y ålder es menor que 65', 81 | 82 | 'when / length': 83 | 'Debe ser parejo cuando tiene una longitud de al menos 10', 84 | 85 | 'when / between': 86 | 'Debe ser parejo cuando foobar está entre 10 y 20', 87 | 88 | 'when / left out': 89 | 'FOO debe ser parejo cuando BAR es omitido', 90 | }, 91 | custom: { 92 | 'simple': 93 | 'Should be a custom thingy!' 94 | } 95 | } 96 | 97 | Object.keys(inputs).forEach(key => { 98 | describe(key, () => { 99 | ((inputs as any)[key] as any[]).forEach((input : any) => { 100 | test(input.name, () => { 101 | expect(lang.renderer.render(input.input, [])).toEqual((outputs as any)[key][input.name]) 102 | }) 103 | }) 104 | }) 105 | }) 106 | 107 | describe('by key', () => { 108 | test('by key 1', () => { 109 | const msg = (l : MessageBuilder) => l.either([ 110 | l.at('edad', l.more(18)), 111 | l.at('nombre', l.less(65)), 112 | l.at('verificada', l.exactly('"si"')) 113 | ]) 114 | 115 | expect(lang.renderer.byKey(msg(lang.builder), [])).toEqual({ 116 | "edad": "Debe ser más que 18 cuando nombre no es menor que 65 y verificada no es \"si\"", 117 | "nombre": "Debe ser menor que 65 cuando edad no es más que 18 y verificada no es \"si\"", 118 | "verificada": "Debe ser \"si\" cuando edad no es más que 18 y nombre no es menor que 65", 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /src/__test__/swedish.test.ts: -------------------------------------------------------------------------------- 1 | import { testInputs, TestName } from './testinputs' 2 | import { svSE } from '../locales/sv-SE' 3 | import { MessageBuilder } from 'bueno/locale' 4 | import { describePaths } from '../path' 5 | import { Locale } from '../index' 6 | 7 | const lang = describePaths( 8 | svSE as Locale, 9 | [ 10 | ['*.phoneNo', 'telefonnummer'], 11 | ['*.age', 'ålder'], 12 | ['*.bar.baz', 'barbaz'], 13 | ] 14 | ) 15 | 16 | const inputs = 17 | testInputs(lang.builder) 18 | 19 | const outputs : { [name in keyof TestName]: { [key in TestName[name]]: string } } = { 20 | messages: { 21 | 'no path': 22 | 'Måste vara ett tal', 23 | 24 | 'no path / negated': 25 | 'Får inte vara ett tal', 26 | 27 | 'simple path': 28 | 'Bar måste vara ett tal', 29 | 30 | 'simple path / negated': 31 | 'Bar får inte vara ett tal', 32 | 33 | 'or / mixed path': 34 | 'Måste vara ett tal eller bar måste vara en text', 35 | 36 | 'or / multiple paths': 37 | 'Baz måste vara ett tal eller bar måste vara en text', 38 | 39 | 'or / multiple no path': 40 | 'Måste vara ett tal eller en text', 41 | 42 | 'or / negated / multiple no path': 43 | 'Får inte vara ett tal eller en text', 44 | 45 | 'or / negated / multiple no path / same article': 46 | 'Får inte vara ett tal eller objekt', 47 | 48 | 'or / multiple same path': 49 | 'Bar måste vara ett tal, en text eller ett objekt, baz måste vara en text eller baz får inte vara ett objekt', 50 | 51 | 'or / multiple different paths': 52 | 'Barbaz måste vara ett tal eller objekt, baz måste vara sant eller falskt eller baz får inte vara en text eller ett objekt', 53 | 54 | 'or / multiple different paths / empty path': 55 | 'Måste vara sant eller falskt, får inte vara en text eller ett objekt eller bar måste vara ett tal eller objekt', 56 | 57 | 'or / multiple different paths / empty path / adjectives': 58 | 'Måste vara mer än 10 eller mer än 12 eller får inte vara mindre än 5', 59 | 60 | 'at every': 61 | 'Varje element måste vara mer än 10', 62 | 63 | 'alpha-numeric': 64 | 'Måste ha endast bokstäver och siffror' 65 | }, 66 | having: { 67 | 'sum': 68 | 'Måste ha summa som minst 10', 69 | 70 | 'length': 71 | 'Måste ha längd som minst 10', 72 | 73 | 'unexpected keys': 74 | 'Får inte ha oväntade nycklar foo, bar och baz', 75 | }, 76 | when: { 77 | 'simple': 78 | 'Måste vara mer än 10 när bar är en text', 79 | 80 | 'with disjunctions': 81 | 'Måste vara mer än 10 eller en text eller telefonnummer måste vara ett tal när inte mer än 3 eller ett objekt, ålder är mer än 18 och ålder är mindre än 65', 82 | 83 | 'when / length': 84 | 'Måste vara jämnt när längd som minst 10', 85 | 86 | 'when / between': 87 | 'Måste vara jämnt när foobar är mellan 10 och 20', 88 | 89 | 'when / left out': 90 | 'FOO måste vara jämnt när BAR är utlämnad', 91 | }, 92 | custom: { 93 | 'simple': 94 | 'Should be a custom thingy!' 95 | } 96 | } 97 | 98 | Object.keys(inputs).forEach(key => { 99 | describe(key, () => { 100 | ((inputs as any)[key] as any[]).forEach((input : any) => { 101 | test(input.name, () => { 102 | expect(lang.renderer.render(input.input, [])).toEqual((outputs as any)[key][input.name]) 103 | }) 104 | }) 105 | }) 106 | }) 107 | 108 | describe('by key', () => { 109 | test('by key 1', () => { 110 | const msg = (l : MessageBuilder) => l.either([ 111 | l.at('ålder', l.more(18)), 112 | l.at('namn', l.less(65)), 113 | l.at('verifierad', l.exactly('"ja"')) 114 | ]) 115 | 116 | expect(lang.renderer.byKey(msg(lang.builder), [])).toEqual({ 117 | ålder: 'Måste vara mer än 18 när namn inte är mindre än 65 och verifierad inte är "ja"', 118 | namn: 'Måste vara mindre än 65 när ålder inte är mer än 18 och verifierad inte är "ja"', 119 | verifierad: 'Måste vara "ja" när ålder inte är mer än 18 och namn inte är mindre än 65', 120 | }) 121 | }) 122 | }) 123 | -------------------------------------------------------------------------------- /src/__test__/testinputs.ts: -------------------------------------------------------------------------------- 1 | import { MessageBuilder } from 'bueno/locale' 2 | 3 | type TestGroup = 4 | ReturnType 5 | 6 | export type TestName = { 7 | [key in keyof TestGroup]: TestGroup[key][number]['name'] 8 | } 9 | 10 | const inputs = (l : MessageBuilder) => ({ 11 | messages: [ 12 | { 13 | name: 'no path' as const, 14 | input: l.number 15 | }, 16 | { 17 | name: 'no path / negated' as const, 18 | input: l.not(l.number) 19 | }, 20 | { 21 | name: 'simple path' as const, 22 | input: l.at('bar', l.number) 23 | }, 24 | { 25 | name: 'simple path / negated' as const, 26 | input: l.not(l.at('bar', l.number)) 27 | 28 | }, 29 | { 30 | name: 'or / mixed path' as const, 31 | input: l.either([ 32 | l.number, 33 | l.at('bar', l.string) 34 | ]) 35 | }, 36 | { 37 | name: 'or / multiple paths' as const, 38 | input: l.either([ 39 | l.at('baz', l.number), 40 | l.at('bar', l.string) 41 | ]) 42 | }, 43 | 44 | { 45 | name: 'or / multiple no path' as const, 46 | input: l.either([l.number, l.string]) 47 | }, 48 | { 49 | name: 'or / negated / multiple no path' as const, 50 | input: l.not(l.either([l.number, l.string])) 51 | }, 52 | { 53 | name: 'or / negated / multiple no path / same article' as const, 54 | input: l.not(l.either([l.number, l.object])) 55 | }, 56 | { 57 | name: 'or / multiple same path' as const, 58 | input: l.either([ 59 | l.at('bar', l.either([l.number, l.string, l.object])), 60 | l.at('baz', l.either([l.string])), 61 | l.at('baz', l.either([l.not(l.object)])) 62 | ]) 63 | }, 64 | { 65 | name: 'or / multiple different paths' as const, 66 | input: l.either([ 67 | l.at('bar', l.at('baz', l.either([l.number, l.object]))), 68 | l.at('baz', l.bool), 69 | l.not(l.at('baz', l.either([l.string, l.object]))) 70 | ]) 71 | }, 72 | { 73 | name: 'or / multiple different paths / empty path' as const, 74 | input: l.either([ 75 | l.bool, 76 | l.not(l.either([l.string, l.object])), 77 | l.at('bar', l.either([l.number, l.object])) 78 | ]) 79 | }, 80 | { 81 | name: 'or / multiple different paths / empty path / adjectives' as const, 82 | input: l.either([ 83 | l.more(10), 84 | l.more(12), 85 | l.not(l.less(5)) 86 | ]) 87 | }, 88 | { 89 | name: 'at every', 90 | input: l.atEvery(l.more(10)) 91 | }, 92 | { 93 | name: 'alpha-numeric', 94 | input: l.alphanum 95 | } 96 | ], 97 | when: [ 98 | { 99 | name: 'simple' as const, 100 | input: l.when( 101 | l.more(10), 102 | [l.at('bar', l.string)] 103 | ) 104 | }, 105 | { 106 | name: 'with disjunctions' as const, 107 | input: l.when( 108 | l.either([ 109 | l.more(10), l.string, 110 | l.at('phoneNo', l.number) 111 | ]), 112 | [ 113 | l.not(l.either([l.more(3), l.object])), 114 | l.at('age', l.more(18)), 115 | l.at('age', l.less(65)) 116 | ] 117 | ) 118 | }, 119 | { 120 | name: 'when / length', 121 | input: l.when( 122 | l.even, 123 | [l.length(l.atLeast(10))] 124 | ) 125 | }, 126 | { 127 | name: 'when / between', 128 | input: l.when( 129 | l.even, 130 | [l.at('foobar', l.between(10, 20))] 131 | ) 132 | }, 133 | { 134 | name: 'when / left out', 135 | input: l.when( 136 | l.at('FOO', l.even), 137 | [l.at('BAR', l.leftOut)] 138 | ) 139 | } 140 | ], 141 | having: [ 142 | { 143 | name: 'sum', 144 | input: l.sum(l.atLeast(10)) 145 | }, 146 | { 147 | name: 'length', 148 | input: l.length(l.atLeast(10)) 149 | }, 150 | { 151 | name: 'unexpected keys', 152 | input: l.keys(['foo', 'bar', 'baz']) 153 | } 154 | ], 155 | custom: [ 156 | { 157 | name: 'simple' as const, 158 | input: l.fromString('Should be a custom thingy!'), 159 | } 160 | ] 161 | }) 162 | 163 | export const testInputs = inputs 164 | -------------------------------------------------------------------------------- /src/__test__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { joinWithCommas } from '../utils' 2 | import { joinPath } from '../path' 3 | 4 | const display = 5 | (x : string[]) => joinWithCommas(x, 'or') 6 | 7 | describe('oneOf', () => { 8 | test('1', () => { 9 | expect(display([])) 10 | .toEqual('') 11 | }) 12 | 13 | test('2', () => { 14 | expect(display(['foo'])) 15 | .toEqual('foo') 16 | }) 17 | 18 | test('3', () => { 19 | expect(display(['foo', 'bar'])) 20 | .toEqual('foo or bar') 21 | }) 22 | 23 | test('4', () => { 24 | expect(display(['foo', 'bar', 'baz'])) 25 | .toEqual('foo, bar or baz') 26 | }) 27 | 28 | test('5', () => { 29 | expect(display(['foo', 'bar', 'baz', 'quux'])) 30 | .toEqual('foo, bar, baz or quux') 31 | }) 32 | }) 33 | 34 | describe('join path', () => { 35 | 36 | test('"" left unit', () => { 37 | expect(joinPath('', 'foo')).toEqual('foo') 38 | }) 39 | 40 | test('"" right unit', () => { 41 | expect(joinPath('foo', '')).toEqual('foo') 42 | }) 43 | 44 | test('happy', () => { 45 | expect(joinPath('foo', 'bar')).toEqual('foo.bar') 46 | }) 47 | 48 | test('segment / empty', () => { 49 | expect(joinPath('', '_#_bar')).toEqual('_#_bar') 50 | }) 51 | 52 | test('segment / foo', () => { 53 | expect(joinPath('foo', '_#_bar')).toEqual('_#_bar') 54 | }) 55 | 56 | test('segment / segment', () => { 57 | expect(joinPath('_#_foo', '_#_bar')).toEqual('_#_foo') 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | import { isPromise } from "./utils" 2 | 3 | export type Action = 4 | Promise | A 5 | 6 | export function chain( 7 | a : A | Promise, 8 | f : (a : A) => B | Promise 9 | ) : B | Promise { 10 | if (isPromise(a)) { 11 | return (a as Promise).then(f) 12 | } 13 | return f(a as A) 14 | } 15 | 16 | export function all( 17 | as : (A | Promise)[] 18 | ) : A[] | Promise { 19 | if (!as.some(isPromise)) { 20 | return as as any 21 | } 22 | return Promise.all(as) 23 | } 24 | 25 | export function pairA( 26 | a : Action, 27 | b : Action, 28 | f : (a : A, b : B) => Action 29 | ) : Action { 30 | return chain(all([a, b]), (kv) => f(kv[0] as any, kv[1] as any)) 31 | } 32 | 33 | export function toPromise( 34 | a : Action 35 | ) : Promise { 36 | return (isPromise(a) ? a : Promise.resolve(a)) as any 37 | } 38 | -------------------------------------------------------------------------------- /src/docs/auto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philipnilsson/bueno/c3700db828a6dce0f6df98139f4d4a91b8d898c0/src/docs/auto.png -------------------------------------------------------------------------------- /src/docs/bidirectional.md: -------------------------------------------------------------------------------- 1 | # Bidirectionality 2 | 3 | Bidirectionality means that the parsing element of schemas can be run 4 | in reverse. This can be really useful for converting back and forth 5 | between a "nice" representation and one expected e.g. by an API. 6 | 7 | For example, we can create a simple schema to convert a string to and 8 | from a `date-fns` representation. 9 | 10 | ```typescript 11 | import { lift } from 'bueno' 12 | import { parseISO, formatISO } from 'date-fns' 13 | 14 | const date = lift( 15 | parseISO, 16 | d => formatISO(d, { representation: 'date' }) 17 | ) 18 | ``` 19 | 20 | Now, let's say we have a form where we want to edit the 21 | following data from our API. 22 | 23 | ```typescript 24 | const purchase = { 25 | currency: '99.00', 26 | date: '2019-08-11' 27 | } 28 | ``` 29 | 30 | Our API has stored a currency amount and date as strings, but 31 | we'd like to work with this data using real `Date` and `number` 32 | objects. We simply create the following schema... 33 | 34 | ```typescript 35 | import { lift, object, result, toNumber } from 'bueno' 36 | 37 | // ... 38 | 39 | const schema = object({ 40 | currency: toNumber, 41 | date: date 42 | }) 43 | ``` 44 | 45 | ...parse the data and edit it... 46 | 47 | ```typescript 48 | const parsedPurchase = 49 | result(schema, purchase) 50 | 51 | parsedPurchase.currency = 110.50 52 | parsedPurchase.date = new Date('2020-01-01') 53 | ``` 54 | 55 | ...and we use the schema to convert back to the representation our API 56 | expects. 57 | 58 | ```typescript 59 | result(parsedPurchase, schema) 60 | // { currency: '110.5', date: '2020-01-01' } 61 | ``` 62 | 63 | The combinators of `bueno` will maintain bidirectionality so you can 64 | always work with data in the most convenient way without paying the 65 | price of manually having to write code converting between various 66 | representations. 67 | -------------------------------------------------------------------------------- /src/docs/customizing-errors.md: -------------------------------------------------------------------------------- 1 | # Customizing errors 2 | 3 | `bueno` let's you provide custom error messages in a few different 4 | ways. The simplest is to use the `setMessage` combinator with a 5 | string argument. 6 | 7 | ```typescript 8 | import { check, emptyLocale, setMessage } from 'bueno' 9 | 10 | const schema = 11 | setMessage(number, 'You gotta provide a number!') 12 | 13 | check('123', schema, emtpyLocale) 14 | // 'You gotta provide a number!' 15 | 16 | check(123, schema, emtpyLocale) 17 | // null 18 | ``` 19 | 20 | This approach does not work well if you're looking to support multiple 21 | locales though. Errors in `bueno` can be type-safely extended using 22 | declaration merging in TypeScript. 23 | 24 | ```typescript 25 | declare module 'bueno/locale' { 26 | interface MessageBuilder { 27 | myCustomError: IR 28 | } 29 | } 30 | ``` 31 | 32 | `IR` is an intermediate representation that is locale-specific. You 33 | won't typically need to interact with it directly. 34 | 35 | You can now create custom locales that will be checked against the 36 | specification you've provided. 37 | 38 | ``` 39 | const myEnUS: Locale = { 40 | builder: { 41 | ...enUS.builder, 42 | myCustomError: 'My custom error!' 43 | }, 44 | renderer: enUS.renderer 45 | } 46 | 47 | const mySvSE: Locale = { 48 | builder: { 49 | ...svSE.builder, 50 | myCustomError: 'Mitt nya felmeddelande!' 51 | }, 52 | renderer: svSE.renderer 53 | } 54 | ``` 55 | 56 | We then use this 57 | -------------------------------------------------------------------------------- /src/docs/express.md: -------------------------------------------------------------------------------- 1 | # Usage example with `express` 2 | 3 | `bueno` can quickly be integrated into an `express` project, providing 4 | validation on endpoints with helpful error messages, as well adding 5 | autocompletion and type safe access to the request data. 6 | 7 | ```typescript 8 | import express from 'express' 9 | import bodyParser from 'body-parser' 10 | import { atLeast, optional, boolean, either, enUS, even, object, number, string, length, result, checkPerKey, toNumber } from 'bueno' 11 | 12 | const mySchema = object({ 13 | params: object({ 14 | id: toNumber(either(even, atLeast(100))) 15 | }), 16 | query: object({ 17 | name: optional(string(length(atLeast(5)))) 18 | }), 19 | body: object({ 20 | foo: number, 21 | bar: boolean 22 | }) 23 | }) 24 | 25 | express() 26 | .post('/hi/:id', bodyParser.json(), (req: any, res: any) => { 27 | const errors = checkPerKey(req, mySchema, enUS) 28 | if (Object.keys(errors).length) { 29 | return res.status(500).send(JSON.stringify(errors, null, 2) + '\n') 30 | } 31 | const data = result(req, mySchema) 32 | return res.status(200).send('Ok!\n') 33 | }) 34 | .listen(3030) 35 | ``` 36 | 37 | In addition to validating the request, editors will now provide 38 | auto-completion & type safety for the request parameters. 🎉 39 | 40 | 41 | 42 | Here's the output when calling the endpoint with some different parameters. 43 | 44 | ```bash 45 | >> curl -d '' 'http://localhost:3030/hi/99' 46 | { 47 | "body": { 48 | "bar": "Must be true or false", 49 | "foo": "Must be a number" 50 | }, 51 | "params": { 52 | "id": "Must be even or at least 100" 53 | } 54 | } 55 | 56 | >> curl -d '' 'http://localhost:3030/hi/100?name=bob' 57 | { 58 | "body": { 59 | "bar": "Must be true or false", 60 | "foo": "Must be a number" 61 | }, 62 | "query": { 63 | "name": "Must have length at least 5" 64 | } 65 | } 66 | 67 | >> curl -d '' 'http://localhost:3030/hi/100?name=robert' 68 | { 69 | "body": { 70 | "bar": "Must be true or false", 71 | "foo": "Must be a number" 72 | } 73 | } 74 | 75 | >> curl -d '{ "foo": "123" }' --header "Content-Type: application/json" 'http://localhost:3030/hi/100?name=robert' 76 | { 77 | "body": { 78 | "bar": "Must be true or false", 79 | "foo": "Must be a number" 80 | } 81 | } 82 | 83 | >> curl -d '{ "foo": 123, "bar": true }' --header "Content-Type: application/json" 'http://localhost:3030/hi/100?name=robert' 84 | Ok! 85 | ``` 86 | -------------------------------------------------------------------------------- /src/docs/form.md: -------------------------------------------------------------------------------- 1 | # Usage example: Vanilla JS form 2 | 3 | 4 | 5 | ### HTML 6 | 7 | ```html 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 | 23 |
24 | 25 |
26 |
27 |
28 | 29 | 30 | ``` 31 | 32 | ### TypeScript 33 | 34 | We provide TypeScript source for those who prefer it. You can get the 35 | vanilla version for your preferred JavaScript version 36 | [here](https://www.typescriptlang.org/play/?ssl=8&ssc=1&pln=1&pc=1#code/JYWwDg9gTgLgBAbzgQxgGQKbIM4wDRwDGAFhoQNYAKGUA0hgJ4EbAylTMB2AqgMrMA3DJwIQARgCsy+OLijBOAcwIAbYYrYEYEAHIBXEGJpwAvnABmUCCDgByMXuERbAKBeEInXHBANeJDBBkOABeOHEpQhgACgQXODhOAwAuOG19QxpoljYsjCERFHQsXGiARgAGCoBKWrx42RgoVLkFRWi1JTZo1EwcGIBWWur6k2qAbjdogHcFABMIaZRsFE4GaoA6PTA51AxQiz1OKOBPaOrEBo8veHNoGxCGhOiFwgNhGA27qBAVnFX1htfAAxe5uBLXbwKMB6eBhOIJBJJECpb4gDbIjYCZAqRzLRryJT1REE1H3DZyLE4vH-VpKBomcFETzeKAYbB6FRwp5EUgUah0RjRaGwgi+fykIJcPgTJmvd6cT4AR0cUD8GDUUWg0VsAGJkQBRKBWKC2aoAQg2Ck4NAAEgAVACyaAObI5XIxBjgAB9vXZXAl5SAPhsVTR1ZrtFAdbq5EaTWbLda7U6XWE3ZzPnIfX7bK5GUA). (Use Config -> Target) 37 | 38 | ```typescript 39 | import { atLeast, checkPerKey, either, enUS, even, object, string, length, toNumber } from 'bueno' 40 | 41 | const mySchema = object({ 42 | num: toNumber(either(even, atLeast(100))), 43 | str: string(length(atLeast(5))), 44 | }); 45 | 46 | (window as any).update = function() { 47 | const form = 48 | (document.forms as any).myForm 49 | 50 | const input = { 51 | num: form.num.value as string, 52 | str: form.str.value as string 53 | } 54 | 55 | const result = 56 | checkPerKey(input, mySchema, enUS); 57 | 58 | document.querySelector('#numError')!.innerHTML = result.num || '' 59 | document.querySelector('#strError')!.innerHTML = result.str || '' 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /src/docs/formik.md: -------------------------------------------------------------------------------- 1 | # Usage example with formik 2 | 3 | Integrating `bueno` with `formik`/`react` is very easy and requires 4 | only using the existing functions provided. 5 | 6 | ```typescript 7 | import { atLeast, checkPerKey, either, enUS, even, flip, object, number, string, length, result } from 'bueno' 8 | 9 | const mySchema = object({ 10 | num: number(either(even, atLeast(100))), 11 | str: string(length(atLeast(5))), 12 | }) 13 | 14 | const Form = () => { 15 | const formik = useFormik({ 16 | initialValues: 17 | { num: 0, str: 'hello' }, 18 | validate: 19 | // Simply use `checkPerKey` or checkPerKeyAsync` for validation. 20 | values => checkPerKey(values, mySchema, enUS), 21 | onSubmit: function(values) { 22 | // The form values. 23 | console.log(values) 24 | // You may sometimes wish to `flip` the schema if you're doing any 25 | // parsing and want to send the form result back to an API endpoint 26 | console.log(result(values, flip(mySchema))) 27 | } 28 | }) 29 | 30 | // Implement your form using `formik.values` and `formik.errors` 31 | // as usual 32 | return ( 33 |
34 | ... 35 |
36 | ) 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /src/docs/index.md: -------------------------------------------------------------------------------- 1 | # Bueno 2 | 3 | ## What 4 | 5 | The tiny, composable validation library with awesome built-in 6 | localized error messages. 7 | 8 | ## Is it any good? 9 | 10 | Yup 11 | 12 | You can try it in a [sandbox]('link') 13 | 14 | ```typescript 15 | 16 | 17 | const mySchema = object({ 18 | name: string, 19 | age: number(atLeast(18)), 20 | username: optional(string(not(empty), length(atLeast(1)))), 21 | magicNumber: number(either(atLeast(10), not(even))), 22 | bucket: optional(oneOf(1, 2, 3)), 23 | elements: array(number(even, atLeast(0)))(length(atLeast(1))) 24 | }) 25 | 26 | import { number, string, email, optional, atLeast, length } from 'bueno' 27 | 28 | const schema = object({ 29 | name: optional(string(not(empty))), 30 | age: number(atLeast(18)), 31 | email: email, 32 | password: string(length(atLeast(10))), 33 | }) 34 | 35 | const result = check(schema, { 36 | name: 'Philip', 37 | age: '34', 38 | email: 'phni@example', 39 | password: '<3' 40 | }) 41 | 42 | { 43 | age: 'Must be a number', 44 | email: 'Must be a valid email', 45 | password: 'Must be of length at least 10' 46 | } 47 | ``` 48 | 49 | ```js 50 | import { either, nonEmptyString, number, url, email, object, optional } from 'bueno' 51 | 52 | const book = object({ 53 | title: nonEmptyString, 54 | author: nonEmptyString, 55 | ISBN: optional(repeat(13, digit)) 56 | }) 57 | 58 | const movie = object({ 59 | title: nonEmptyString, 60 | director: nonEmptyString, 61 | yearReleased: optional(repeat(4, digit)) 62 | }) 63 | 64 | const recommendations = 65 | arrayOf(either(book, movie)) 66 | 67 | const user = object({ 68 | id: either(string, number), 69 | website: url, 70 | email: email, 71 | recommendations 72 | }) 73 | ``` 74 | 75 | ## Why 76 | 77 | You may want to use `bueno` if: 78 | 79 | ### You care about bundle size 80 | 81 | `bueno` [tree-shakes](link) well because it uses a functional style, 82 | much like e.g. `date-fns` 83 | 84 | ### You use typescript 85 | 86 | `bueno` takes great care to ensure type-safety and 87 | "auto-completability" works to their full potential. 88 | 89 | [Check it out in a sandbox](link) 90 | 91 | ### i18n is important to you. 92 | 93 | `bueno` is built with i18n in mind. 94 | 95 | It's compliant with i18next syntax out of the box, but can be used 96 | standalone or with other i18n libraries. 97 | 98 | ### You want to easily write your own validators 99 | 100 | Check out some examples 101 | 102 | ### You need a powerful but simple API that scales 103 | 104 | `bueno` gives you one of the most powerful validator APIs while 105 | staying simple, both under the hood and in its usage. 106 | 107 | ### You're not looking for a parsing library 108 | 109 | `bueno` does one thing right - validation. Therefore it does *not* 110 | attempt to parse values, it simply validates already parsed values. 111 | 112 | This matches well with libraries like React where, let's say, a 113 | `` component will produce a valid `Date` object. 114 | 115 | ## How 116 | 117 | Install 118 | 119 | ``` 120 | yarn add bueno 121 | ``` 122 | 123 | or 124 | 125 | ``` 126 | npm add bueno 127 | ``` 128 | 129 | and use 130 | 131 | ```typescript 132 | import { describeError, validate, number } from 'bueno' 133 | import english from 'bueno/locale/en' 134 | 135 | const user = object({ 136 | id: either(number, string) 137 | email: email 138 | }) 139 | 140 | // bueno 141 | const { error } = validate("12323", number) // error is null 142 | 143 | // not bueno 144 | const { error } = validate("a2323", number) 145 | console.log(describeError(error, english)) // "This must be a number" 146 | ``` 147 | 148 | ### Usage with e.g. Formik 149 | 150 | ```typescript 151 | import { toYupCompliantSchema, both, minLength, maxLength, email } from 'bueno' 152 | import english from 'bueno/locale/en' 153 | 154 | const name = 155 | both(minLength(2), maxLength(50)), 156 | 157 | const signupSchema = object({ 158 | firstName: name, 159 | lastName: name, 160 | email 161 | }) 162 | 163 | 171 | ... 172 | 173 | ``` 174 | 175 | ### Usage with Express 176 | 177 | ```typescript 178 | import express from 'express' 179 | import { object, numberFromString, number, string, length, between, optional } from 'bueno' 180 | 181 | const app = express() 182 | 183 | app.post('/hello/:name/:age, (res, req) => { 184 | const schema = object({ 185 | path: object({ 186 | age: numberFromString, 187 | name: string(length(between(1, 100))), 188 | }), 189 | body: object({ 190 | greeting: string, 191 | }), 192 | query: object({ 193 | override_ab_test_group: optional(number), 194 | }) 195 | }) 196 | 197 | const { 198 | result: { path, body, query }, 199 | errorsByKey, 200 | } = validate(req, schema) 201 | 202 | if (errors) { 203 | return res.status(502).json(errorsByKey) 204 | } 205 | 206 | // ... 207 | }) 208 | ``` 209 | 210 | ### Integration with other libraries 211 | 212 | ```js 213 | import { between, exactly } from '@bueno/core' 214 | import { date, month, day } from '@bueno/date-fns' 215 | import { parse } from 'date-fns' 216 | 217 | const schema = object({ 218 | date2020: date(between(parse('2020-01-01'), parse('2020-12-31'))), 219 | christmas: date(exactly(parse('2020-12-25'))), 220 | summer: month(between(6, 9)), 221 | weekend: day(oneOf([0, 6])) 222 | }) 223 | ``` 224 | -------------------------------------------------------------------------------- /src/docs/languages.md: -------------------------------------------------------------------------------- 1 | # Supported languages 2 | 3 | `bueno` currently supports the following locales 4 | 5 | * `en-US` 6 | * `es-ES` 7 | * `de-DE` 8 | * `sv-SE` 9 | 10 | Some additional locales are in progress, and contributions for 11 | additional ones will be happily accepted. 12 | 13 | If you have a locale you'd like to have supported, please consider 14 | contributing **test cases** for that locale to help with its 15 | implementation. 16 | 17 | 18 | **Please Note:** `bueno` always tries to generate grammatically 19 | acceptable error messages, but it is a new library and **you will want 20 | to double check any error messages it generates for any production 21 | usage where incorrect grammar would appear unprofessional**. 22 | -------------------------------------------------------------------------------- /src/docs/lightweight-api.md: -------------------------------------------------------------------------------- 1 | # Lightweight API validation using `bueno`. 2 | 3 | 4 |
5 | 6 | `bueno` is library for writing *composable* validation schemas in 7 | TypeScript. Schemas of the kind that you can pretty much just 8 | understand by reading them (and to write yourself with very little 9 | additional effort). 10 | 11 | ```typescript 12 | const mySchema = object({ 13 | foo: either(number(atLeast(2)), string), 14 | bar: array(number)(length( 15 | either(exactly(0), atLeast(3)) 16 | )) 17 | }) 18 | ``` 19 | 20 | In spite of being quite expressive, `bueno` is still able to generate 21 | useful error messages *automatically* without any further input from the 22 | schema writer. 23 | 24 | ```typescript 25 | >> checkPerKey({ foo: 1, bar: [1] }, mySchema, enUS) 26 | 27 | { 28 | foo: 'Must be at least 2 or a string', 29 | bar: 'Must have length 0 or at least 3' 30 | } 31 | ``` 32 | 33 | This let's us quickly write some really decent data validation for 34 | API:s that'll also be super helpful for the caller when your API gets 35 | invoked with incorrect data. 36 | 37 | Of course, `bueno` will not be a replacement for e.g. `JSON-schema` or 38 | similar projects that'll typically interface with a lot of other, more 39 | complex and sophisticated tooling. Nor is it intended to. 40 | 41 | However, if you have a simple node API in e.g. Express that you'd like 42 | to add some validation to without spending the time to set up new 43 | tools, `bueno` adds quite a lot of bang for the buck for the few 44 | minutes you'll need to [get started](../../README.md#quickstart) 45 | 46 | Importantly, it'll also get you **type-safe data handling, 47 | auto-completion and all the other TypeScript IDE goodies** for your 48 | request data at a very low cost. 49 | 50 | Here's a full example in `express` 51 | 52 | ```typescript 53 | import express from 'express' 54 | import bodyParser from 'body-parser' 55 | import { atLeast, optional, boolean, either, enUS, even, object, number, string, length, result, checkPerKey, toNumber } from 'bueno' 56 | 57 | const mySchema = object({ 58 | params: object({ 59 | id: toNumber(either(even, atLeast(100))) 60 | }), 61 | query: object({ 62 | name: optional(string(length(atLeast(5)))) 63 | }), 64 | body: object({ 65 | foo: number, 66 | bar: boolean 67 | }) 68 | }) 69 | 70 | express() 71 | .post('/hi/:id', bodyParser.json(), (req: any, res: any) => { 72 | const errors = checkPerKey(req, mySchema, enUS) 73 | if (Object.keys(errors).length) { 74 | return res.status(500).send(JSON.stringify(errors, null, 2) + '\n') 75 | } 76 | const data = result(req, mySchema) 77 | return res.status(200).send('Ok!\n') 78 | }) 79 | .listen(3030) 80 | ``` 81 | 82 | Auto-completion now working on the data! 83 | 84 | 85 | 86 | and below - some of the helpful error responses the client will receive. 87 | 88 | Hope you'll enjoy using `bueno` to harden all those slightly neglected 89 | API:s that haven't received the full attention of having more complex 90 | tooling set up! 91 | 92 | ```sh 93 | >> curl -d '' 'http://localhost:3030/hi/99' 94 | { 95 | "body": { 96 | "bar": "Must be true or false", 97 | "foo": "Must be a number" 98 | }, 99 | "params": { 100 | "id": "Must be even or at least 100" 101 | } 102 | } 103 | 104 | >> curl -d '' 'http://localhost:3030/hi/100?name=bob' 105 | { 106 | "body": { 107 | "bar": "Must be true or false", 108 | "foo": "Must be a number" 109 | }, 110 | "query": { 111 | "name": "Must have length at least 5" 112 | } 113 | } 114 | 115 | >> curl -d '' 'http://localhost:3030/hi/100?name=robert' 116 | { 117 | "body": { 118 | "bar": "Must be true or false", 119 | "foo": "Must be a number" 120 | } 121 | } 122 | 123 | >> curl -d '{ "foo": "123" }' --header "Content-Type: application/json" 'http://localhost:3030/hi/100?name=robert' 124 | { 125 | "body": { 126 | "bar": "Must be true or false", 127 | "foo": "Must be a number" 128 | } 129 | } 130 | 131 | >> curl -d '{ "foo": 123, "bar": true }' --header "Content-Type: application/json" 'http://localhost:3030/hi/100?name=robert' 132 | Ok! 133 | ``` 134 | -------------------------------------------------------------------------------- /src/docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philipnilsson/bueno/c3700db828a6dce0f6df98139f4d4a91b8d898c0/src/docs/logo.png -------------------------------------------------------------------------------- /src/docs/vanilla-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philipnilsson/bueno/c3700db828a6dce0f6df98139f4d4a91b8d898c0/src/docs/vanilla-form.png -------------------------------------------------------------------------------- /src/iife.ts: -------------------------------------------------------------------------------- 1 | import * as bueno from './index' 2 | 3 | (window as any).bueno = bueno 4 | 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | const IS_DEV : boolean 3 | } 4 | 5 | export { any } from './schema/any' 6 | export { apply } from './schema/apply' 7 | export { alphaNumeric } from './schema/alphaNumeric' 8 | export { array } from './schema/array' 9 | export { atLeast } from './schema/atLeast' 10 | export { atMost } from './schema/atMost' 11 | export { between } from './schema/between' 12 | export { boolean } from './schema/boolean' 13 | export { both } from './schema/both' 14 | export { compact } from './schema/compact' 15 | export { chain } from './schema/chain' 16 | export { date } from './schema/date' 17 | export { defaultTo } from './schema/defaultTo' 18 | export { describePaths } from './path' 19 | export { either } from './schema/either' 20 | export { email } from './schema/email' 21 | export { emptyString } from './schema/emptyString' 22 | export { even } from './schema/even' 23 | export { every } from './schema/every' 24 | export { exactly } from './schema/exactly' 25 | export { fix } from './schema/fix' 26 | export { flip } from './schema/flip' 27 | export { forget } from './schema/forget' 28 | export { id } from './schema/id' 29 | export { integer } from './schema/integer' 30 | export { invalidDate } from './schema/date'; 31 | export { irreversible } from './schema/irreversible' 32 | export { length } from './schema/length' 33 | export { lessThan } from './schema/lessThan' 34 | export { lift } from './schema/lift' 35 | export { match } from './schema/match' 36 | export { moreThan } from './schema/moreThan' 37 | export { not } from './schema/not' 38 | export { number } from './schema/number' 39 | export { object } from './schema/object' 40 | export { objectExact } from './schema/objectExact' 41 | export { objectInexact } from './schema/objectInexact' 42 | export { odd } from './schema/odd' 43 | export { oneOf } from './schema/oneOf' 44 | export { optional } from './schema/optional' 45 | export { optionalTo } from './schema/optionalTo' 46 | export { pair } from './schema/pair' 47 | export { path } from './schema/path' 48 | export { pipe } from './schema/pipe' 49 | export { pure } from './schema/pure' 50 | export { self } from './schema/self' 51 | export { size } from './schema/size' 52 | export { some } from './schema/some' 53 | export { string } from './schema/string' 54 | export { sum } from './schema/sum' 55 | export { toDate } from './schema/toDate' 56 | export { toJSON } from './schema/toJSON' 57 | export { toNumber } from './schema/toNumber' 58 | export { toString } from './schema/toString' 59 | export { toURL } from './schema/toURL' 60 | export { unknown } from './schema/unknown' 61 | export { updateMessage } from './schema/updateMessage' 62 | export { setMessage } from './schema/setMessage' 63 | export { swap } from './schema/swap' 64 | export { updateNestedMessages } from './schema/updateNestedMessages' 65 | export { when } from './schema/when' 66 | 67 | export { iterable } from './schema/collections/iterable' 68 | export { map } from './schema/collections/map' 69 | export { set } from './schema/collections/set' 70 | export { toArray } from './schema/collections/toArray' 71 | export { toMap } from './schema/collections/toMap' 72 | export { toMapFromObject } from './schema/collections/toMapFromObject' 73 | export { toSet } from './schema/collections/toSet' 74 | 75 | // Factories 76 | export { createSchema } from './schema/factories/createSchema' 77 | export { mkSchema } from './schema/factories/mkSchema' 78 | export { mkParser } from './schema/factories/mkParser' 79 | export { mkParserCallable } from './schema/factories/core' 80 | export { mkParserHaving } from './schema/factories/mkParserHaving' 81 | export { mkSchemaHaving } from './schema/factories/mkSchemaHaving' 82 | 83 | // Runners 84 | export { result, resultAsync } from './schema/runners/result'; 85 | export { check, checkAsync } from './schema/runners/check'; 86 | export { checkPerKey, checkPerKeyAsync } from './schema/runners/checkByKey'; 87 | 88 | // Locales 89 | export { enUS, enUSOptions, English } from './locales/en-US' 90 | export { svSE, Swedish } from './locales/sv-SE' 91 | export { deDE, German } from './locales/de-DE' 92 | export { esES, Spanish } from './locales/es-ES' 93 | export { emptyLocale } from './locales/emptyLocale' 94 | 95 | export { mkRenderer, RenderingOptions } from './DefaultIR' 96 | 97 | // Types 98 | export { 99 | Schema, 100 | Parser, 101 | Schema_, 102 | Parser_, 103 | SchemaFactory, 104 | UndefinedOptional 105 | } from './types' 106 | 107 | import './Builtins' 108 | import { Language } from 'bueno/locale' 109 | import { MessageBuilder } from 'bueno/locale' 110 | 111 | export type Locale = Language 112 | 113 | export type Builder = MessageBuilder 114 | -------------------------------------------------------------------------------- /src/locales/de-DE.ts: -------------------------------------------------------------------------------- 1 | import { joinWithCommas, words, capFirst, id, isNumber, deepMerge } from '../utils'; 2 | import { IR, mkRenderer, verb, atom, defaultBuilder, not, at, RenderingOptions } from '../DefaultIR' 3 | 4 | export type German = 5 | | { k : 'noun_', article_ : '' | 'ein' | 'eine', noun_ : string, name_ : string } 6 | 7 | const germanVerb = (v : string, w : string) => verb( 8 | x => ['muss', x, v], 9 | x => ['darf nicht', x, v], 10 | x => [x, w], 11 | x => ['nicht', x, w] 12 | ) 13 | 14 | const bestehen = 15 | germanVerb('bestehen', 'besteht') 16 | 17 | const sein = 18 | germanVerb('sein', 'ist') 19 | 20 | const liegen = 21 | germanVerb('liegen', 'liegt') 22 | 23 | const werden = 24 | germanVerb('werden', 'wird') 25 | 26 | function haben(name : string) : (ir : IR) => IR { 27 | return verb( 28 | x => ['muss', name, x, 'haben'], 29 | x => ['darf nicht', name, x, 'haben'], 30 | x => ['es', name, x, 'hat'], 31 | x => ['es', 'nicht', name, x, 'hat'] 32 | ) 33 | } 34 | 35 | function eine(noun_ : string, name_ : string) : IR { 36 | return sein(atom({ article_: 'eine', noun_, k: 'noun_', name_ })) 37 | } 38 | 39 | function ein(noun_ : string, name_ : string) : IR { 40 | return sein(atom({ article_: 'ein', noun_, k: 'noun_', name_ })) 41 | } 42 | 43 | const germanBuilder = { 44 | 45 | ...defaultBuilder, 46 | 47 | mustBe: sein, 48 | 49 | mustHave: haben(''), 50 | 51 | has: haben, 52 | 53 | length: haben('eine Länge von'), 54 | 55 | sum: haben('eine Summe von'), 56 | 57 | atEvery(ir : IR) : IR { 58 | return at('jedes Element', ir) 59 | }, 60 | 61 | more
(n : A) { 62 | return sein(`mehr als ${n}`, 'more'); 63 | }, 64 | 65 | less(n : A) { 66 | return sein(`weniger als ${n}`, 'less'); 67 | }, 68 | 69 | atLeast(lb : A) { 70 | return sein(`mindestens ${lb}`, 'atLeast') 71 | }, 72 | 73 | between(lb : A, ub : A) { 74 | return liegen(`zwischen ${lb} und ${ub}`, 'between') 75 | }, 76 | 77 | atMost(ub : A) { 78 | return sein(`höchstens ${ub}`, 'atMost') 79 | }, 80 | 81 | oneOf(choices : A[]) { 82 | if (choices.length === 1) { 83 | return sein(`${choices[0]}`, 'oneOf') 84 | } 85 | return sein( 86 | `einer von ${joinWithCommas(choices.map(x => `${x}`), 'o')}`, 87 | 'oneOf' 88 | ) 89 | }, 90 | 91 | exactly(target : A) { 92 | return sein(`${target}`, 'exactly') 93 | }, 94 | 95 | keys(keys : string[]) { 96 | const showKeys = (keys.length === 1 97 | ? `${keys[0]}` 98 | : `${joinWithCommas(keys, 'und')}`) 99 | // TODO: Remove cast to `any` 100 | return not((haben('unerwarteten Schlüssel') as any)(showKeys, 'keys')) 101 | }, 102 | 103 | email: eine('gültige E-Mail-Adresse', 'email'), 104 | 105 | alphanum: bestehen('nur aus Buchstaben und Zahlen', 'alphanum'), 106 | 107 | uuid: ein('UUID', 'uuid'), 108 | 109 | url: eine('gültige URL', 'url'), 110 | 111 | even: sein('gerade', 'even'), 112 | 113 | odd: sein('ungerade', 'odd'), 114 | 115 | empty: sein('leer', 'empty'), 116 | 117 | date: ein('gültiges Datum', 'date'), 118 | 119 | iterable: sein('iterable', 'iterable'), 120 | 121 | array: eine('Liste', 'array'), 122 | 123 | string: ein('Text', 'string'), 124 | 125 | set: ein('Satz', 'set'), 126 | 127 | map: eine('Abbildung', 'map'), 128 | 129 | bool: deepMerge([sein('wahr'), sein('falsch')]), 130 | 131 | number: eine('Zahl', 'number'), 132 | 133 | integer: eine('ganze Zahl', 'integer'), 134 | 135 | object: ein('Objekt', 'object'), 136 | 137 | leftOut: werden('weggelassen'), 138 | 139 | json: ein('gültiges JSON-Objekt', 'json') 140 | } 141 | 142 | function renderGermanAtom( 143 | nouns : German[], 144 | props : RenderingOptions 145 | ) : Out { 146 | let lastArticle = ''; 147 | const ns : Out[] = nouns.filter(x => x.k === 'noun_').map(n => { 148 | if (n.article_ && (n.article_ !== lastArticle)) { 149 | lastArticle = n.article_ 150 | return props.words([ 151 | props.fromstr(n.article_), 152 | props.noun(n.noun_, n.name_, n.article_) 153 | ]) 154 | } 155 | return props.noun(n.noun_, n.name_, n.article_) 156 | }) 157 | return props.or(ns) 158 | } 159 | 160 | export const deDEOptions : RenderingOptions = { 161 | fromstr: id, 162 | or: (words : string[]) => joinWithCommas(words, 'oder'), 163 | and: (words : string[]) => joinWithCommas(words, 'und'), 164 | when: (conseq : string, cond : string) => `${conseq} wenn ${cond}`, 165 | elem: (path : (string | number)[]) => `element Nummer #${path.map(x => isNumber(x) ? (x as number) + 1 : x).join('.')}`, 166 | path: (path : string[]) => path.join('.'), 167 | noun: id, 168 | atom: renderGermanAtom, 169 | words, 170 | finalize: capFirst, 171 | pathAtKey: (path, verb, byKey) => { 172 | return words(byKey ? [verb] : [path, verb]) 173 | } 174 | } 175 | 176 | export const deDE = { 177 | renderer: mkRenderer(deDEOptions), 178 | builder: germanBuilder, 179 | } 180 | -------------------------------------------------------------------------------- /src/locales/emptyLocale.ts: -------------------------------------------------------------------------------- 1 | import { Locale } from "../index"; 2 | 3 | export const emptyLocale : Locale = { 4 | builder: { fromString: (x : any) => x, either: (xs : any) => xs[0] } as any, 5 | renderer: { render: (x : any) => x } as any 6 | } 7 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | import { joinWithCommas, deepMerge, capFirst, push, isNumber, words, id } from '../utils' 2 | import { IR, not, mkRenderer, atom, verb, defaultBuilder, RenderingOptions } from '../DefaultIR' 3 | import '../Builtins' 4 | import { MessageBuilder } from 'bueno/locale' 5 | 6 | export type English = 7 | | { k : 'noun_', article_ : '' | 'a' | 'an', noun_ : string, name_ : string } 8 | 9 | function has(name : string) : (ir : IR) => IR { 10 | return verb( 11 | x => ['must have', name, x], 12 | x => ['must not have', name, x], 13 | x => ['has', name, x], 14 | x => ['does not have', name, x], 15 | x => ['having', name, x], 16 | x => ['not having', name, x] 17 | ) 18 | } 19 | 20 | const mustBe = 21 | verb( 22 | x => ['must be', x], 23 | x => ['must not be', x], 24 | x => ['is', x], 25 | x => ['is not', x], 26 | x => [x], 27 | x => ['not', x] 28 | ) 29 | 30 | const mustHave = 31 | verb('must have', 'must not have', 'has', 'does not have', 'having', 'not having') 32 | 33 | function a(noun_ : string, name_ : string = noun_) { 34 | return mustBe(atom({ article_: 'a', noun_, k: 'noun_', name_ })) 35 | } 36 | 37 | function an(noun_ : string, name_ : string = noun_) { 38 | return mustBe(atom({ article_: 'an', noun_, k: 'noun_', name_ })) 39 | } 40 | 41 | const englishBuilder : MessageBuilder> = { 42 | ...defaultBuilder, 43 | 44 | mustBe: mustBe, 45 | 46 | mustHave: mustHave, 47 | 48 | has: has, 49 | 50 | length: has('length'), 51 | 52 | sum: has('sum'), 53 | 54 | more(n : A) { 55 | return mustBe(`more than ${n}`, 'more') 56 | }, 57 | 58 | less(n : A) { 59 | return mustBe(`less than ${n}`, 'less') 60 | }, 61 | 62 | atLeast(lb : A) { 63 | return mustBe(`at least ${lb}`, 'atLeast') 64 | }, 65 | 66 | between(lb : A, ub : A) { 67 | return mustBe(`between ${lb} and ${ub}`, 'between') 68 | }, 69 | 70 | atMost(ub : A) { 71 | return mustBe(`at most ${ub}`, 'atMost') 72 | }, 73 | 74 | oneOf(choices : A[]) { 75 | return mustBe( 76 | choices.length === 1 77 | ? `${choices[0]}` 78 | : `one of ${joinWithCommas(choices.map(x => `${x}`), 'or')}`, 79 | 'oneOf' 80 | ) 81 | }, 82 | 83 | exactly(target : A) { 84 | return mustBe(`${target}`, 'exactly') 85 | }, 86 | 87 | keys(keys : string[]) : IR { 88 | const showKeys = keys.length === 1 89 | ? ` ${keys[0]}` 90 | : `s ${joinWithCommas(keys, 'and')}` 91 | return not(mustHave(`unexpected key${showKeys}`, 'keys')) 92 | }, 93 | 94 | bool: deepMerge([mustBe('true'), mustBe('false')]), 95 | 96 | email: a('valid email address', 'email'), 97 | 98 | alphanum: mustHave('letters and numbers only', 'alphanum'), 99 | 100 | uuid: a('valid uuid', 'uuid'), 101 | 102 | url: a('valid URL', 'url'), 103 | 104 | even: mustBe('even'), 105 | 106 | odd: mustBe('odd'), 107 | 108 | empty: mustBe('empty'), 109 | 110 | date: a('date'), 111 | 112 | iterable: mustBe('iterable'), 113 | 114 | array: an('array'), 115 | 116 | set: a('set', 'set'), 117 | 118 | map: a('map'), 119 | 120 | string: a('string'), 121 | 122 | number: a('number'), 123 | 124 | integer: a('whole number', 'integer'), 125 | 126 | object: an('object'), 127 | 128 | leftOut: mustBe('left out'), 129 | 130 | json: a('valid JSON object', 'json'), 131 | } 132 | 133 | export function groupByArticle(words : English[]) : { [key in English['article_']]?: English[] } { 134 | const result : { [key in English['article_']]?: English[] } = {} 135 | for (const w of words) { 136 | result[w.article_] = push(w, result[w.article_] || []) 137 | } 138 | return result 139 | } 140 | 141 | export function renderEnglishAtom( 142 | nouns : English[], 143 | props : RenderingOptions 144 | ) : O { 145 | let firstArticle = true 146 | return props.or( 147 | nouns.filter(x => x.k === 'noun_').map(n => { 148 | if (n.article_ && firstArticle) { 149 | firstArticle = false 150 | return props.words([ 151 | props.fromstr(n.article_), 152 | props.noun(n.noun_, n.name_, n.article_) 153 | ]) 154 | } 155 | return props.noun(n.noun_, n.name_, n.article_) 156 | }) 157 | ) 158 | } 159 | 160 | export const enUSOptions : RenderingOptions = { 161 | fromstr: id, 162 | or: (words : string[]) => joinWithCommas(words, 'or'), 163 | and: (words : string[]) => joinWithCommas(words, 'and'), 164 | when: (conseq : string, cond : string) => `${conseq} when ${cond}`, 165 | elem: (path : (string | number)[]) => `element #${path.map(x => isNumber(x) ? (x as number) + 1 : x).join('.')}`, 166 | path: (path : string[]) => path.join('.'), 167 | noun: id, 168 | atom: renderEnglishAtom, 169 | words, 170 | finalize: capFirst, 171 | pathAtKey: (path, verb, byKey) => { 172 | return words(byKey ? [verb] : [path, verb]) 173 | } 174 | } 175 | 176 | export const enUS = { 177 | renderer: mkRenderer(enUSOptions), 178 | builder: englishBuilder, 179 | } 180 | -------------------------------------------------------------------------------- /src/locales/es-ES.ts: -------------------------------------------------------------------------------- 1 | import { joinWithCommas, words, capFirst, id, isNumber, deepMerge } from '../utils'; 2 | import { IR, mkRenderer, verb, atom, defaultBuilder, not, at, RenderingOptions } from '../DefaultIR' 3 | import { MessageBuilder } from 'bueno/locale'; 4 | 5 | export type Spanish = 6 | | { k : 'noun_', article_ : '' | 'un' | 'una', noun_ : string, name_ : string } 7 | 8 | const debeSer = 9 | verb('debe ser', 'no debe ser', 'es', 'no es', '', 'no') 10 | 11 | const debeEstar = 12 | verb('debe estar', 'no debe estar', 'está', 'no está', '', 'no') 13 | 14 | const mustHave = 15 | verb('debe tener', 'no debe tener', 'tiene', 'no tiene', '', 'no') 16 | 17 | function un(noun_ : string, name_ : string) : IR { 18 | return debeSer(atom({ article_: 'un', noun_, k: 'noun_', name_ })) 19 | } 20 | 21 | function una(noun_ : string, name_ : string) : IR { 22 | return debeSer(atom({ article_: 'una', noun_, k: 'noun_', name_ })) 23 | } 24 | 25 | function tener(name : string) : (ir : IR) => IR { 26 | return verb( 27 | `debe tener ${name}`, 28 | `no debe tener ${name}`, 29 | `tiene ${name}`, 30 | `no tiene ${name}`, 31 | ) 32 | } 33 | 34 | const spanishBuilder : MessageBuilder> = { 35 | 36 | ...defaultBuilder, 37 | 38 | mustBe: debeSer, 39 | 40 | mustHave, 41 | 42 | has: tener, 43 | 44 | length: tener('una longitud de'), 45 | 46 | sum: tener('una suma de'), 47 | 48 | atEvery(ir : IR) : IR { 49 | return at('cada elemento', ir) 50 | }, 51 | 52 | more(n : A) { 53 | return debeSer(`más que ${n}`, 'more'); 54 | }, 55 | 56 | less(n : A) { 57 | return debeSer(`menor que ${n}`, 'less'); 58 | }, 59 | 60 | atLeast(lb : A) { 61 | return debeSer(`al menos ${lb}`, 'atLeast') 62 | }, 63 | 64 | between(lb : A, ub : A) { 65 | return debeEstar(`entre ${lb} y ${ub}`, 'between') 66 | }, 67 | 68 | atMost(ub : A) { 69 | return debeSer(`como máximo ${ub}`, 'atMost') 70 | }, 71 | 72 | oneOf(choices : A[]) { 73 | if (choices.length === 1) { 74 | return debeSer(`${choices[0]}`, 'oneOf') 75 | } 76 | return debeSer( 77 | `uno de ${joinWithCommas(choices.map(x => `${x}`), 'o')}`, 78 | 'oneOf' 79 | ) 80 | }, 81 | 82 | exactly(target : A) { 83 | return debeSer(`${target}`, 'exactly') 84 | }, 85 | 86 | keys(keys : string[]) { 87 | const showKeys = keys.length === 1 88 | ? `llave inesperada ${keys[0]}` 89 | : `llaves inesperadas ${joinWithCommas(keys, 'y')}` 90 | return not(mustHave(showKeys, 'keys')) 91 | }, 92 | 93 | email: un('correo electrónico válido', 'email'), 94 | 95 | alphanum: debeSer('solo números y letras', 'alphanum'), 96 | 97 | uuid: un('UUID', 'uuid'), 98 | 99 | url: una('URL valida', 'url'), 100 | 101 | even: debeSer('parejo', 'even'), 102 | 103 | odd: debeSer('impar', 'odd'), 104 | 105 | empty: debeEstar('vacío', 'empty'), 106 | 107 | date: debeSer('una fecha', 'date'), 108 | 109 | iterable: debeSer('iterable', 'iterable'), 110 | 111 | array: una('lista', 'array'), 112 | 113 | string: un('texto', 'string'), 114 | 115 | set: un('conjunto', 'set'), 116 | 117 | map: una('asociación', 'map'), 118 | 119 | bool: deepMerge([debeSer('cierto'), debeSer('falso')]), 120 | 121 | number: un('número', 'number'), 122 | 123 | integer: un('número entero', 'integer'), 124 | 125 | object: un('objeto', 'object'), 126 | 127 | leftOut: debeSer('omitido'), 128 | 129 | json: un('objeto json válido', 'json') 130 | } 131 | 132 | function renderSpanishAtom( 133 | nouns : Spanish[], 134 | props : RenderingOptions 135 | ) : Out { 136 | let lastArticle = ''; 137 | const ns : Out[] = nouns.filter(x => x.k === 'noun_').map(n => { 138 | if (n.article_ && (n.article_ !== lastArticle)) { 139 | lastArticle = n.article_ 140 | return props.words([ 141 | props.fromstr(n.article_), 142 | props.noun(n.noun_, n.name_, n.article_) 143 | ]) 144 | } 145 | return props.noun(n.noun_, n.name_, n.article_) 146 | }) 147 | return props.or(ns) 148 | } 149 | 150 | export const esESOptions : RenderingOptions = { 151 | fromstr: id, 152 | or: (words : string[]) => joinWithCommas(words, 'o'), 153 | and: (words : string[]) => joinWithCommas(words, 'y'), 154 | when: (conseq : string, cond : string) => `${conseq} cuando ${cond}`, 155 | elem: (path : (string | number)[]) => `elemento número #${path.map(x => isNumber(x) ? (x as number) + 1 : x).join('.')}`, 156 | path: (path : string[]) => path.join('.'), 157 | noun: id, 158 | atom: renderSpanishAtom, 159 | words, 160 | finalize: capFirst, 161 | pathAtKey: (path, verb, byKey) => { 162 | return words(byKey ? [verb] : [path, verb]) 163 | } 164 | } 165 | 166 | export const esES = { 167 | renderer: mkRenderer(esESOptions), 168 | builder: spanishBuilder, 169 | } 170 | -------------------------------------------------------------------------------- /src/locales/sv-SE.ts: -------------------------------------------------------------------------------- 1 | import { joinWithCommas, words, capFirst, id, isNumber, deepMerge } from '../utils'; 2 | import { IR, mkRenderer, verb, atom, defaultBuilder, not, at, RenderingOptions } from '../DefaultIR' 3 | 4 | export type Swedish = 5 | | { k : 'noun_', article_ : '' | 'en' | 'ett', noun_ : string, name_ : string } 6 | 7 | const skallVara = 8 | verb('måste vara', 'får inte vara', 'är', 'inte är', '', 'inte') 9 | 10 | const skallHa = 11 | verb('måste ha', 'får inte ha', 'har', 'inte har', '', 'inte') 12 | 13 | function en(noun_ : string, name_ : string) { 14 | return skallVara(atom({ article_: 'en', noun_, k: 'noun_', name_ })) 15 | } 16 | 17 | function ett(noun_ : string, name_ : string) { 18 | return skallVara(atom({ article_: 'ett', noun_, k: 'noun_', name_ })) 19 | } 20 | 21 | function has(name : string) : (ir : IR) => IR { 22 | return verb( 23 | `måste ha ${name}`, 24 | `får inte ha ${name}`, 25 | `${name}`, 26 | `inte ${name}` 27 | ) 28 | } 29 | 30 | const swedishBuilder = { 31 | 32 | ...defaultBuilder, 33 | 34 | mustBe: skallVara, 35 | 36 | mustHave: skallHa, 37 | 38 | has, 39 | 40 | length: has('längd'), 41 | 42 | sum: has('summa'), 43 | 44 | atEvery(ir : IR) : IR { 45 | return at('varje element', ir) 46 | }, 47 | 48 | more(n : A) { 49 | return skallVara(`mer än ${n}`, 'more'); 50 | }, 51 | 52 | less(n : A) { 53 | return skallVara(`mindre än ${n}`, 'less'); 54 | }, 55 | 56 | atLeast(lb : A) { 57 | return skallVara(`som minst ${lb}`, 'atLeast') 58 | }, 59 | 60 | between(lb : A, ub : A) { 61 | return skallVara(`mellan ${lb} och ${ub}`, 'between') 62 | }, 63 | 64 | atMost(ub : A) { 65 | return skallVara(`som mest ${ub}`, 'atMost') 66 | }, 67 | 68 | oneOf(choices : A[]) { 69 | if (choices.length === 1) { 70 | return skallVara(`${choices[0]}`, 'oneOf') 71 | } 72 | return skallVara( 73 | `en av ${joinWithCommas(choices.map(x => `${x}`), 'eller')}`, 74 | 'oneOf' 75 | ) 76 | }, 77 | 78 | exactly(target : A) { 79 | return skallVara(`${target}`, 'exactly') 80 | }, 81 | 82 | keys(keys : string[]) { 83 | const showKeys = keys.length === 1 84 | ? `oväntad nyckel ${keys[0]}` 85 | : `oväntade nycklar ${joinWithCommas(keys, 'och')}` 86 | return not(skallHa(showKeys, 'keys')) 87 | }, 88 | 89 | email: en('giltig e-postaddress', 'email'), 90 | 91 | alphanum: skallHa('endast bokstäver och siffror', 'alphanum'), 92 | 93 | uuid: en('giltig uuid', 'uuid'), 94 | 95 | url: en('giltig webbaddress', 'url'), 96 | 97 | even: skallVara('jämnt', 'even'), 98 | 99 | odd: skallVara('udda', 'odd'), 100 | 101 | empty: skallVara('tom', 'empty'), 102 | 103 | date: skallVara('datum', 'date'), 104 | 105 | iterable: skallVara('itererbar', 'iterable'), 106 | 107 | array: en('lista', 'array'), 108 | 109 | string: en('text', 'string'), 110 | 111 | set: en('mängd', 'set'), 112 | 113 | map: en('association', 'map'), 114 | 115 | bool: deepMerge([skallVara('sant'), skallVara('falskt')]), 116 | 117 | number: ett('tal', 'number'), 118 | 119 | integer: ett('heltal', 'integer'), 120 | 121 | object: ett('objekt', 'object'), 122 | 123 | leftOut: skallVara('utlämnad'), 124 | 125 | json: ett('giltigt JSON-objekt', 'json') 126 | } 127 | 128 | function renderSwedishAtom( 129 | nouns : Swedish[], 130 | props : RenderingOptions 131 | ) : Out { 132 | let lastArticle = ''; 133 | const ns : Out[] = nouns.filter(x => x.k === 'noun_').map(n => { 134 | if (n.article_ && (n.article_ !== lastArticle)) { 135 | lastArticle = n.article_ 136 | return props.words([ 137 | props.fromstr(n.article_), 138 | props.noun(n.noun_, n.name_, n.article_) 139 | ]) 140 | } 141 | return props.noun(n.noun_, n.name_, n.article_) 142 | }) 143 | return props.or(ns) 144 | } 145 | 146 | export const svSEOptions : RenderingOptions = { 147 | fromstr: id, 148 | or: (words : string[]) => joinWithCommas(words, 'eller'), 149 | and: (words : string[]) => joinWithCommas(words, 'och'), 150 | when: (conseq : string, cond : string) => `${conseq} när ${cond}`, 151 | elem: (path : (string | number)[]) => `element #${path.map(x => isNumber(x) ? (x as number) + 1 : x).join('.')}`, 152 | path: (path : string[]) => path.join('.'), 153 | noun: id, 154 | atom: renderSwedishAtom, 155 | words, 156 | finalize: capFirst, 157 | pathAtKey: (path, verb, byKey) => { 158 | return words(byKey ? [verb] : [path, verb]) 159 | } 160 | } 161 | 162 | export const svSE = { 163 | renderer: mkRenderer(svSEOptions), 164 | builder: swedishBuilder, 165 | } 166 | -------------------------------------------------------------------------------- /src/logic.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from "./utils" 2 | 3 | export type CNF = A[][] 4 | 5 | export function mapCNF(f : (a : A) => B) { 6 | return function(cnf_ : CNF) : CNF { 7 | return cnf_.map(disj => disj.map(f)) 8 | } 9 | } 10 | 11 | export const _true : CNF = [] 12 | export const _false : CNF = [[]] 13 | 14 | export function and(x : CNF, y : CNF) { 15 | const result = x.concat(y) 16 | if (result.some(isEmpty)) { 17 | return _false 18 | } 19 | return result 20 | } 21 | 22 | export function or(x : CNF, y : CNF) : CNF { 23 | const result : CNF = [] 24 | x.forEach(a => { 25 | y.forEach(b => { 26 | result.push(a.concat(b)) 27 | }) 28 | }) 29 | return result 30 | } 31 | 32 | export function not(x : CNF, negate : (a : A) => A) : CNF { 33 | if (isEmpty(x)) { 34 | return _false 35 | } 36 | 37 | const cs = 38 | not(x.slice(1), negate) 39 | 40 | const result : CNF = [] 41 | x[0].forEach(a => { 42 | result.push(...cs.map(c => [negate(a)].concat(c))) 43 | }) 44 | return result 45 | } 46 | -------------------------------------------------------------------------------- /src/memo.ts: -------------------------------------------------------------------------------- 1 | export function memo2( 2 | f : (a : A, b : B) => C 3 | ) : (a : A, c : B) => C { 4 | let cc : C 5 | let ac = {} 6 | let bc = {} 7 | return (a : A, b : B) => { 8 | if (ac !== a || bc !== b) { 9 | cc = f(a, b) 10 | ac = a 11 | bc = b 12 | } 13 | return cc 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/path.ts: -------------------------------------------------------------------------------- 1 | import { Language } from 'bueno/locale' 2 | import { Paths } from 'bueno/locale' 3 | import { isDigits } from './utils' 4 | 5 | export function defaultRenderPath( 6 | paths : Paths, 7 | p : string[], 8 | renderPath : (path : (string | number)[]) => Out 9 | ) : Out { 10 | return renderOverriddenPath(paths, p) ?? (renderBasePath(p, renderPath)) 11 | } 12 | 13 | export function renderBasePath( 14 | p : string[], 15 | renderPath : (path : (string | number)[]) => Out 16 | ) : Out { 17 | const segments = 18 | p.map(s => s.replace(/^_#_/, '')) 19 | 20 | return renderPath( 21 | segments.map(x => { 22 | if (isDigits(x)) { 23 | return Number(x) 24 | } 25 | return x 26 | })) 27 | } 28 | 29 | function isAbsolutePath(p : string) : boolean { 30 | return p.startsWith('_#_') 31 | } 32 | 33 | export function joinPath(p : string, q : string) { 34 | if (p === '') return q 35 | if (q === '') return p 36 | if (isAbsolutePath(p)) { 37 | return p 38 | } 39 | if (isAbsolutePath(q)) { 40 | return q 41 | } 42 | return p + '.' + q 43 | } 44 | 45 | export function renderOverriddenPath( 46 | paths : [string, Out][], 47 | path : string[] 48 | ) : null | Out { 49 | const exprs : [RegExp, Out][] = 50 | paths.map(kv => { 51 | const expr = kv[0] 52 | const translation = kv[1] 53 | return [ 54 | new RegExp( 55 | '^' 56 | + expr.replace(/\*/g, '\\w*').replace(/\./g, '\\.?') 57 | + '$' 58 | ), 59 | translation 60 | ] 61 | }) 62 | const p = path.join('.') 63 | const c = exprs.find(x => x[0].test(p)) 64 | if (c !== undefined) return c[1] 65 | return null 66 | } 67 | 68 | export function describePaths( 69 | l : Language, 70 | dict : Paths 71 | ) : Language { 72 | return { 73 | builder: l.builder, 74 | renderer: { 75 | ...l.renderer, 76 | byKey(ir : IR, paths : Paths) { 77 | return l.renderer.byKey(ir, dict.concat(paths)) 78 | }, 79 | render(ir : IR, paths : Paths) { 80 | return l.renderer.render(ir, dict.concat(paths)) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/schema/alphaNumeric.ts: -------------------------------------------------------------------------------- 1 | import { Schema_ } from '../types' 2 | import { match } from './match' 3 | 4 | export const alphaNumeric : Schema_ = match( 5 | /[a-zA-Z0-9]*/, 6 | l => l.alphanum 7 | ) 8 | -------------------------------------------------------------------------------- /src/schema/any.ts: -------------------------------------------------------------------------------- 1 | export { any, anyP } from './factories/core' 2 | -------------------------------------------------------------------------------- /src/schema/apply.ts: -------------------------------------------------------------------------------- 1 | import { Parser, Parser_, Schema, Schema_ } from "../types" 2 | import { mkParserCallable } from "./factories/core" 3 | import { mkSchema } from "./factories/mkSchema" 4 | import { pathP } from "./path" 5 | 6 | export function applyP( 7 | v : Parser, 8 | val : A, 9 | ) : Parser_ { 10 | return mkParserCallable((_, inv) => v.run_(val, inv)) 11 | } 12 | 13 | export function apply( 14 | v : Schema, 15 | value : A, 16 | path : string 17 | ) : Schema_ { 18 | return mkSchema( 19 | pathP(path, applyP(v.parse_, value)), 20 | pathP(path, applyP(v.unparse_, value)) 21 | ) 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/schema/array.ts: -------------------------------------------------------------------------------- 1 | import { Action, all, chain as chainA, pairA } from '../action' 2 | import { CNF, and, mapCNF } from '../logic' 3 | import { Err } from 'bueno/locale' 4 | import { mkParser_ } from './factories/mkParser' 5 | import { Parser, Parser_, Schema, Schema_ } from '../types' 6 | import { pipeP } from './pipe' 7 | import { mkParserCallable } from './factories/core' 8 | import { mapError } from '../Builtins' 9 | import { average, isArray } from '../utils' 10 | import { mkSchema } from './factories/mkSchema' 11 | 12 | const arrayType = mkParser_((a : unknown[]) => { 13 | const ok = isArray(a) 14 | return { 15 | parse_: { ok, msg: l => l.array }, 16 | result_: ok ? a : [], 17 | } 18 | }) 19 | 20 | function pairMaybeCNFs( 21 | a : (Action>) | null, 22 | b : (Action>) | null 23 | ) : Action> { 24 | if (a && b) return pairA(a, b, and) 25 | if (a) return a 26 | if (b) return b 27 | return [] 28 | } 29 | 30 | export function arrayP(v : Parser) : Parser_ { 31 | return pipeP(arrayType, mkParserCallable(function(arr : A[], inv : boolean) { 32 | return chainA(all(arr.map(a => v.run_(a, inv))), results => { 33 | return { 34 | cnf_: () => { 35 | const ixPassed = results.findIndex(x => x.score_ === 1) 36 | const ixFailed = results.findIndex(x => x.score_ < 1) 37 | return pairMaybeCNFs( 38 | ixPassed < 0 ? null : 39 | chainA( 40 | results[ixPassed].cnf_(), 41 | mapCNF(mapError(m => l => l.atEvery(m(l)))) 42 | ), 43 | ixFailed < 0 ? null : chainA( 44 | results[ixFailed].cnf_(), 45 | mapCNF(mapError(m => l => l.at(`${ixFailed}`, m(l)))) 46 | ) 47 | ) 48 | }, 49 | res_: () => all(results.map(x => x.res_())), 50 | score_: average(results.map(x => x.score_)) 51 | } 52 | }) 53 | })) 54 | } 55 | 56 | export function array( 57 | v : Schema, 58 | ) : Schema_ { 59 | return mkSchema( 60 | arrayP(v.parse_), 61 | arrayP(v.unparse_)) 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/schema/atLeast.ts: -------------------------------------------------------------------------------- 1 | import { mkParser_ } from "./factories/mkParser" 2 | import { Parser_, Schema_ } from "../types" 3 | import { mkSchema } from "./factories/mkSchema" 4 | import '../Builtins' 5 | 6 | export function atLeastP(lb : A) : Parser_ { 7 | return mkParser_(a => ({ 8 | validate_: { msg: l => l.atLeast(lb), ok: a >= lb }, 9 | result_: typeof a === typeof lb ? a : lb 10 | })) 11 | } 12 | 13 | export function atLeast(lb : A) : Schema_ { 14 | const v = atLeastP(lb) 15 | return mkSchema(v, v) 16 | } 17 | -------------------------------------------------------------------------------- /src/schema/atMost.ts: -------------------------------------------------------------------------------- 1 | import { Parser_, Schema_ } from "../types" 2 | import { mkParser_ } from "./factories/mkParser" 3 | import { mkSchema } from "./factories/mkSchema" 4 | 5 | export function atMostP(ub : A) : Parser_ { 6 | return mkParser_(a => ({ 7 | validate_: { msg: l => l.atMost(ub), ok: (a as any) <= ub }, 8 | result_: typeof a === typeof ub ? a : ub 9 | })) 10 | } 11 | 12 | export function atMost(lb : number) : Schema_ { 13 | const v = atMostP(lb) 14 | return mkSchema(v, v) 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/schema/between.ts: -------------------------------------------------------------------------------- 1 | import { mkParser_ } from "./factories/mkParser" 2 | import { mkSchema } from "./factories/mkSchema" 3 | import { Parser_, Schema_ } from "../types" 4 | 5 | function betweenP(lb : A, ub : A) : Parser_ { 6 | return mkParser_(a => ({ 7 | validate_: { msg: l => l.between(lb, ub), ok: a >= lb && a <= ub }, 8 | result_: typeof a === typeof lb ? a : lb 9 | })) 10 | } 11 | 12 | export function between(lb : number, ub : number) : Schema_ { 13 | const v = betweenP(lb, ub) 14 | return mkSchema(v, v) 15 | } 16 | -------------------------------------------------------------------------------- /src/schema/boolean.ts: -------------------------------------------------------------------------------- 1 | import { Schema_ } from '../types' 2 | import { mkSchema } from './factories/mkSchema' 3 | import { mkParser_ } from './factories/mkParser' 4 | 5 | export const booleanP = mkParser_((a : boolean) => { 6 | const ok = typeof a === 'boolean' 7 | return { 8 | parse_: { ok, msg: l => l.bool }, 9 | result_: ok ? a : false, 10 | } 11 | }) 12 | 13 | export const boolean : Schema_ = 14 | mkSchema(booleanP, booleanP) 15 | -------------------------------------------------------------------------------- /src/schema/both.ts: -------------------------------------------------------------------------------- 1 | export { both, bothP } from './factories/core' 2 | -------------------------------------------------------------------------------- /src/schema/chain.ts: -------------------------------------------------------------------------------- 1 | import { self, selfP } from "./self" 2 | import { Parser, Parser_, Schema, Schema_ } from "../types" 3 | import { pipe, pipeP } from "./pipe" 4 | import { irreversible } from "./irreversible" 5 | 6 | export function chainP( 7 | v : Parser, 8 | f : (b : B) => Parser 9 | ) : Parser_ { 10 | return pipeP(v, selfP(f)) 11 | } 12 | 13 | export function chain( 14 | v : Schema, 15 | f : (b : B) => Schema, 16 | g : (c : C) => Schema = irreversible('chain'), 17 | ) : Schema_ { 18 | return pipe(v, self(f, g)) 19 | } 20 | -------------------------------------------------------------------------------- /src/schema/collections/iterable.ts: -------------------------------------------------------------------------------- 1 | import { mkParser_ } from "../factories/mkParser" 2 | import { mkSchema } from "../factories/mkSchema" 3 | import { isIterable } from "../../utils" 4 | import { Parser_, Schema_ } from "../../types" 5 | 6 | const _iterable = mkParser_((a : Iterable) => { 7 | const ok = isIterable(a) 8 | return { 9 | parse_: { ok, msg: l => l.iterable }, 10 | result_: ok ? a : [] 11 | } 12 | }) 13 | 14 | export const iterableP = () : Parser_, Iterable> => { 15 | return _iterable 16 | } 17 | 18 | export function iterable() : Schema_, Iterable> { 19 | return mkSchema(iterableP(), iterableP()) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/schema/collections/map.ts: -------------------------------------------------------------------------------- 1 | import { Parser, Parser_, Schema, Schema_ } from "../../types" 2 | import { pipeP } from "../pipe" 3 | import { aMap } from "./toMapFromObject" 4 | import { pairP } from "../pair" 5 | import { toMapP } from "./toMap" 6 | import { mkSchema } from "../factories/mkSchema" 7 | import { toArrayP } from "./toArray" 8 | import { arrayP } from "../array" 9 | 10 | export function mapP(key : Parser, val : Parser) : Parser_, Map> { 11 | return pipeP(aMap, pipeP(toArrayP(), pipeP(arrayP(pairP(key, val)), toMapP()))) 12 | } 13 | 14 | export function map(k : Schema, v : Schema) : Schema_, Map> { 15 | return mkSchema( 16 | mapP(k.parse_, v.parse_), 17 | mapP(k.unparse_, v.unparse_) 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/schema/collections/set.ts: -------------------------------------------------------------------------------- 1 | import { pipeP } from "../pipe" 2 | import { Parser, Parser_, Schema, Schema_ } from "../../types" 3 | import { toArrayP } from "./toArray" 4 | import { arrayP } from "../array" 5 | import { toSetP } from "./toSet" 6 | import { mkSchema } from "../factories/mkSchema" 7 | import { mkParser_ } from "../factories/mkParser" 8 | 9 | const aSet = mkParser_((a : Set) => ({ 10 | parse_: { ok: a instanceof Set, msg: l => l.set }, 11 | result_: a instanceof Set ? a : new Set(), 12 | })) 13 | 14 | export function setP(v : Parser) : Parser_, Set> { 15 | return pipeP(aSet as Parser, Set>, pipeP(toArrayP(), pipeP(arrayP(v), toSetP()))) 16 | } 17 | 18 | setP.run_ = aSet.run_ 19 | 20 | export function set(v : Schema) : Schema_, Set> { 21 | return mkSchema(setP(v.parse_), setP(v.unparse_)) 22 | } 23 | 24 | set.parse_ = aSet 25 | set.unparse_ = aSet 26 | 27 | -------------------------------------------------------------------------------- /src/schema/collections/toArray.ts: -------------------------------------------------------------------------------- 1 | import { mkSchema } from "../factories/mkSchema" 2 | import { Parser_, Schema_ } from "../../types" 3 | import { pipeP } from "../pipe" 4 | import { iterableP } from "./iterable" 5 | import { liftP } from "../lift" 6 | 7 | const _toArray : Parser_, any[]> = 8 | pipeP(iterableP(), liftP(a => [...a])) 9 | 10 | export function toArrayP() : Parser_, A[]> { 11 | return _toArray 12 | } 13 | 14 | export function toArray() : Schema_, A[]> { 15 | return mkSchema(toArrayP(), toArrayP() as Parser_>) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/schema/collections/toMap.ts: -------------------------------------------------------------------------------- 1 | import { mkSchema } from "../factories/mkSchema" 2 | import { Schema_, Parser_ } from "../../types" 3 | import { pipeP } from "../pipe" 4 | import { liftP } from "../lift" 5 | import { iterableP } from "./iterable" 6 | 7 | const _toMap : Parser_, Map> = 8 | pipeP(iterableP(), liftP((a => new Map([...a])))) 9 | 10 | export function toMapP() : Parser_, Map> { 11 | return pipeP(iterableP(), _toMap) 12 | } 13 | 14 | export function toMap() : Schema_, Map> { 15 | return mkSchema(toMapP(), toMapP() as Parser_, Iterable<[A, B]>>) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/schema/collections/toMapFromObject.ts: -------------------------------------------------------------------------------- 1 | import { pipeP } from "../pipe" 2 | import { Parser_, Schema_ } from "../../types" 3 | import { objectP } from "../object" 4 | import { liftP } from "../lift" 5 | import { entries, fromEntries } from "../../utils" 6 | import { mkSchema } from "../factories/mkSchema" 7 | import { mkParser_ } from "../factories/mkParser" 8 | 9 | export const aMap = mkParser_((a : Map) => { 10 | const ok = a instanceof Map 11 | return { 12 | parse_: { ok, msg: l => l.map }, 13 | result_: ok ? a : new Map() 14 | } 15 | }) 16 | 17 | export function toMapFromObjectP() : Parser_<{ [key in A]: B }, Map> { 18 | // TODO investigate "as any" 19 | return pipeP(objectP as any, pipeP(liftP(entries as any), liftP(es => new Map(es as any)))) 20 | } 21 | 22 | export function toObjectFromMap() : Parser_, { [key in A]: B }> { 23 | return pipeP(aMap as Parser_, Map>, pipeP(liftP(map => [...map]), liftP(fromEntries))) 24 | } 25 | 26 | export function toMapFromObject() : Schema_<{ [key in A]: B }, Map> { 27 | return mkSchema( 28 | toMapFromObjectP(), 29 | toObjectFromMap() 30 | ) 31 | } 32 | 33 | -------------------------------------------------------------------------------- /src/schema/collections/toSet.ts: -------------------------------------------------------------------------------- 1 | import { Parser_, Schema_ } from "../../types" 2 | import { pipeP } from "../pipe" 3 | import { iterableP } from "./iterable" 4 | import { liftP } from "../lift" 5 | import { mkSchema } from "../factories/mkSchema" 6 | import { anyP } from "../factories/core" 7 | 8 | const _toSetP : Parser_, Set> = 9 | pipeP(iterableP(), liftP(a => new Set([...a]))) 10 | 11 | export function toSetP() : Parser_, Set> { 12 | return _toSetP 13 | } 14 | 15 | export function toSet() : Schema_, Set> { 16 | return mkSchema(toSetP(), anyP as Parser_, Iterable>) 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/schema/compact.ts: -------------------------------------------------------------------------------- 1 | import { mkParser_, pipeP, mkSchema } from "./factories/core"; 2 | import { UndefinedOptional, Parser, Parser_, Schema_, Schema, Obj } from "../types"; 3 | import { fromEntries, entries } from "../utils"; 4 | 5 | export function _compact(a : A) : UndefinedOptional { 6 | return fromEntries( 7 | entries(a).filter(kv => kv[1] !== undefined) 8 | ) as any 9 | } 10 | 11 | const compactParser = 12 | mkParser_((a : Obj) => ({ result_: _compact(a) })) 13 | 14 | export function compactP( 15 | p : Parser 16 | ) : Parser_, UndefinedOptional> { 17 | return pipeP(p, compactParser) as any 18 | } 19 | 20 | export function compact( 21 | p : Schema 22 | ) : Schema_, UndefinedOptional> { 23 | return mkSchema( 24 | compactP(p.parse_), 25 | compactP(p.unparse_) 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/schema/date.ts: -------------------------------------------------------------------------------- 1 | import { Schema_ } from '../types' 2 | import { mkSchema } from './factories/mkSchema' 3 | import { mkParser_ } from './factories/mkParser' 4 | 5 | export const invalidDate = new Date('') 6 | 7 | export const dateP = mkParser_((a : Date) => { 8 | const ok = a instanceof Date 9 | return { 10 | parse_: { ok, msg: l => l.date }, 11 | result_: ok ? a : invalidDate, 12 | } 13 | }) 14 | 15 | export const date : Schema_ = 16 | mkSchema(dateP, dateP) 17 | -------------------------------------------------------------------------------- /src/schema/defaultTo.ts: -------------------------------------------------------------------------------- 1 | import { Parser, Parser_, Schema, Schema_ } from "../types" 2 | import { mkParserCallable } from "./factories/core" 3 | import { mkSchema } from "./factories/mkSchema" 4 | import { chain } from "../action" 5 | 6 | export function defaultToP( 7 | v : Parser, 8 | fallback : B 9 | ) : Parser_ { 10 | return mkParserCallable(function(a : A, inv : boolean) { 11 | return chain(v.run_(a, inv), c => ({ 12 | cnf_: c.cnf_, 13 | res_: c.score_ < 1 ? (() => fallback) : c.res_, 14 | score_: c.score_ 15 | })) 16 | }) 17 | } 18 | 19 | export function defaultTo( 20 | defaultFrom : B, 21 | schema : Schema, 22 | ) : Schema_ { 23 | return mkSchema( 24 | defaultToP(schema.parse_, defaultFrom), 25 | schema.unparse_ 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/schema/either.ts: -------------------------------------------------------------------------------- 1 | export { either, eitherP } from './factories/core' 2 | -------------------------------------------------------------------------------- /src/schema/email.ts: -------------------------------------------------------------------------------- 1 | import { Schema_ } from '../types' 2 | import { match } from './match' 3 | 4 | export const email : Schema_ = match( 5 | /[^@]+@[^\.]+\..+/, 6 | l => l.email 7 | ) 8 | -------------------------------------------------------------------------------- /src/schema/emptyString.ts: -------------------------------------------------------------------------------- 1 | import { Schema_ } from "../types" 2 | import { mkSchema } from "./factories/mkSchema" 3 | import { pipeP } from "./pipe" 4 | import { stringP } from "./string" 5 | import { mkParser_ } from "./factories/mkParser" 6 | 7 | export const emptyStringP = pipeP(stringP, mkParser_(s => ({ 8 | validate_: { msg: l => l.empty, ok: s === '' }, 9 | result_: s 10 | }))) 11 | 12 | export const emptyString : Schema_ = 13 | mkSchema(emptyStringP, emptyStringP) 14 | -------------------------------------------------------------------------------- /src/schema/even.ts: -------------------------------------------------------------------------------- 1 | import { mkSchema } from "./factories/mkSchema" 2 | import { pipeP } from "./pipe" 3 | import { numberP } from "./number" 4 | import { mkParser_ } from "./factories/mkParser" 5 | 6 | export const evenP = pipeP(numberP, mkParser_((a : number) => ({ 7 | validate_: { ok: !(a % 2), msg: l => l.even }, 8 | result_: a 9 | }))) 10 | 11 | export const even = 12 | mkSchema(evenP, evenP) 13 | 14 | -------------------------------------------------------------------------------- /src/schema/every.ts: -------------------------------------------------------------------------------- 1 | export { every, everyP } from './factories/core' 2 | -------------------------------------------------------------------------------- /src/schema/exactly.ts: -------------------------------------------------------------------------------- 1 | import { Parser_, Schema_ } from '../types' 2 | import { mkSchema } from './factories/mkSchema' 3 | import { mkParser_ } from './factories/mkParser' 4 | 5 | export const exactlyP = ( 6 | target : A, 7 | equals : (x : A, y : A) => boolean = (x, y) => x === y 8 | ) : Parser_ => { 9 | return mkParser_(a => { 10 | const ok = equals(a as A, target) 11 | return { 12 | validate_: { 13 | ok, 14 | msg: l => l.exactly(target) 15 | }, 16 | result_: typeof a === typeof target ? a as A : target 17 | } 18 | }) 19 | } 20 | 21 | export const exactly = ( 22 | target : A, 23 | equals : (x : A, y : A) => boolean = (x, y) => x === y 24 | ) : Schema_ => { 25 | return mkSchema( 26 | exactlyP(target, equals), 27 | exactlyP(target, equals) 28 | ) 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/schema/factories/core.ts: -------------------------------------------------------------------------------- 1 | import { ParseResult, Parser_, Schema_, Schema, Parser, SchemaFactory } from '../../types' 2 | import { Action, chain, pairA } from '../../action' 3 | import { memo2 } from '../../memo' 4 | import { _true, CNF, and, or } from '../../logic' 5 | import { irreversible } from '../irreversible' 6 | import { Err, Message } from 'bueno/locale' 7 | import { isObject, constant, isEmpty, getParse, getUnparse, keys, toMessage } from '../../utils' 8 | 9 | export function notP( 10 | v : Parser 11 | ) : Parser_ { 12 | return mkParserCallable(function(a : A, inv : boolean) { 13 | return v.run_(a, !inv) 14 | }) 15 | } 16 | 17 | export function eitherP( 18 | v : Parser, 19 | w : Parser 20 | ) : Parser_ { 21 | return mkParserCallable(function(a : A | B, inv : boolean) { 22 | if (inv) { 23 | return bothP(notP(v), notP(w)).run_(a as any, false) 24 | } 25 | return pairA(v.run_(a as A, false), w.run_(a as B, false), (c, d) => { 26 | const res_ : () => Action = 27 | c.score_ >= d.score_ ? c.res_ : d.res_ 28 | let cnf_; 29 | if (c.score_ === 0 && d.score_ > 0.5) { 30 | cnf_ = d.cnf_ 31 | } 32 | else if (d.score_ === 0 && c.score_ > 0.5) { 33 | cnf_ = c.cnf_ 34 | } else { 35 | cnf_ = () => pairA(c.cnf_(), d.cnf_(), or) 36 | } 37 | return { 38 | cnf_, 39 | res_, 40 | score_: Math.max(c.score_, d.score_) 41 | } 42 | }) 43 | }) 44 | } 45 | 46 | export function either( 47 | v : Schema, 48 | w : Schema 49 | ) : Schema_ { 50 | return mkSchema( 51 | eitherP(v.parse_, w.parse_), 52 | eitherP(v.unparse_, w.unparse_) 53 | ) 54 | } 55 | 56 | export function merge(a : A, b : B) : A & B { 57 | if (isObject(a) && isObject(b)) { 58 | return { ...a, ...b } 59 | } 60 | return a as A & B 61 | } 62 | 63 | export function bothP( 64 | v : Parser, 65 | w : Parser 66 | ) : Parser_ { 67 | return mkParserCallable(function(a : A & B, inv : boolean) { 68 | if (inv) { 69 | return eitherP(notP(v), notP(w)).run_(a, false) as any 70 | } 71 | return pairA(v.run_(a, false), w.run_(a, false), (c, d) => ({ 72 | cnf_: () => pairA(c.cnf_(), d.cnf_(), and), 73 | res_: () => pairA(c.res_(), d.res_(), (rc, rd) => c.score_ >= d.score_ 74 | ? merge(rc, rd) 75 | : merge(rd, rc) 76 | ), 77 | score_: (c.score_ + d.score_) / 2 78 | })) 79 | }) 80 | } 81 | 82 | export function both( 83 | v : Schema, 84 | w : Schema, 85 | ) : Schema_ { 86 | return mkSchema( 87 | bothP(v.parse_, w.parse_), 88 | bothP(v.unparse_, w.unparse_) 89 | ) 90 | } 91 | 92 | export function pipeP( 93 | v : Parser, 94 | w : Parser 95 | ) : Parser_ { 96 | return mkParserCallable(function(a : A, inv : boolean) { 97 | return chain(v.run_(a, inv), c => { 98 | if (c.score_ < 1) { 99 | return { 100 | cnf_: c.cnf_, 101 | res_: () => chain(c.res_(), rc => chain(w.run_(rc, inv), d => d.res_())), 102 | score_: c.score_, 103 | } as ParseResult 104 | } 105 | return chain(c.res_(), rc => { 106 | return chain(w.run_(rc, inv), d => { 107 | return { 108 | res_: d.res_, 109 | cnf_: () => chain(d.cnf_(), cnf_ => cnf_.length ? cnf_ : c.cnf_()), 110 | score_: (d.score_ + 1) / 2, 111 | } 112 | }) 113 | }) 114 | }) 115 | }) 116 | } 117 | 118 | export function pipe( 119 | s : Schema, 120 | t : Schema 121 | ) : Schema_ { 122 | return mkSchema( 123 | pipeP(s.parse_, t.parse_), 124 | pipeP(t.unparse_, s.unparse_) 125 | ) 126 | } 127 | 128 | export const anyP : Parser_ = 129 | mkParser_(constant({})) 130 | 131 | export const any = 132 | mkSchema(anyP, anyP) 133 | 134 | export const id : () => Schema_ = 135 | constant(mkSchema(anyP as any, anyP as any)) 136 | 137 | export function everyP( 138 | vs : Parser[] 139 | ) : Parser_ { 140 | if (isEmpty(vs)) return anyP 141 | return (vs as any).reduce(bothP) 142 | } 143 | 144 | export function every( 145 | ...vs : Schema[] 146 | ) : Schema_ { 147 | return mkSchema( 148 | everyP(vs.map(getParse)), 149 | everyP(vs.map(getUnparse)) 150 | ) 151 | } 152 | 153 | export function forgetP(v : Parser) : Parser_ { 154 | return mkParserCallable(function(a : A, inv : boolean) { 155 | return chain(v.run_(a, inv), r => { 156 | return { 157 | cnf_: r.cnf_, 158 | res_: constant(a), 159 | score_: r.score_ 160 | } 161 | }) 162 | }) 163 | } 164 | 165 | export function forget( 166 | schema : Schema_ 167 | ) : Schema_ { 168 | return mkSchema( 169 | forgetP(schema.parse_), 170 | id().parse_ 171 | ) 172 | } 173 | 174 | export function mkSchema( 175 | parse : Parser_, 176 | unparse : Parser_ = mkParserCallable(irreversible('mkSchema')) 177 | ) : Schema_ { 178 | function call(...ss : Schema[]) : Schema_ { 179 | const nested = every(...ss) 180 | return mkSchema( 181 | pipeP(parse, forgetP(nested.parse_)), 182 | pipeP(forgetP(nested.unparse_), unparse) 183 | ) 184 | } 185 | call.parse_ = parse 186 | call.unparse_ = unparse 187 | return call 188 | } 189 | 190 | export function mkParserCallable( 191 | fn : (a : A, inverted : boolean) => Action> 192 | ) : Parser_ { 193 | function call(...vs : Parser_[]) : Parser_ { 194 | return pipeP(call, forgetP(everyP(vs))) 195 | } 196 | call.run_ = memo2(fn) 197 | return call 198 | } 199 | 200 | export function mkParser( 201 | check : SchemaFactory 202 | ) : Parser_ { 203 | return mkParser_((a : A) => { 204 | return chain(check(a), result => { 205 | return ({ 206 | parse_: result.parse, 207 | validate_: result.validate, 208 | score_: result.score, 209 | result_: result.result, 210 | }) 211 | }) 212 | }) 213 | } 214 | 215 | export type InternalParserFactory = (a : A) => Action<{ 216 | parse_?: { ok : boolean, msg : string | Message }, 217 | validate_?: { ok : boolean, msg : string | Message }, 218 | result_?: B, 219 | score_?: number 220 | }> 221 | 222 | export function mkParser_( 223 | check : InternalParserFactory 224 | ) : Parser_ { 225 | return mkParserCallable(function(a : A, inv : boolean) { 226 | return chain(check(a), args => { 227 | const { parse_, validate_, result_, score_ } = args 228 | if (IS_DEV) { 229 | const { parse_, validate_, result_, score_, ...rest } = args 230 | const extras = keys(rest) 231 | if (extras.length) { 232 | throw new Error(`Unknown argument(s) ${extras.join(', ')}`) 233 | } 234 | } 235 | let msg : CNF = _true 236 | let points = 1 237 | if (parse_) { 238 | const good = parse_.ok 239 | if (!good) { 240 | points = 0 241 | } 242 | msg = [[{ k: 'p', m: toMessage(parse_.msg), ok: good }]] 243 | } 244 | if (validate_) { 245 | const good = validate_.ok !== inv 246 | const validationScore = score_ ?? (good ? 1 : 0) 247 | if (parse_) { 248 | points = (points + validationScore) / 2 249 | } else { 250 | points = validationScore 251 | } 252 | if (!parse_ || parse_.ok) { 253 | msg = and( 254 | msg, 255 | [[{ 256 | k: 'v', 257 | m: l => inv 258 | ? l.not(toMessage(validate_.msg)(l)) 259 | : toMessage(validate_.msg)(l), 260 | ok: good 261 | }]] 262 | ) 263 | } 264 | } 265 | return { 266 | cnf_: constant(msg), 267 | res_: constant(('result_' in args ? result_ : a) as B), 268 | score_: points 269 | } 270 | }) 271 | }) 272 | } 273 | -------------------------------------------------------------------------------- /src/schema/factories/createSchema.ts: -------------------------------------------------------------------------------- 1 | import { mkParser } from "./mkParser" 2 | import { mkSchema } from "./mkSchema" 3 | import { SchemaFactory, Schema_ } from "../../types" 4 | import { irreversible } from "../irreversible" 5 | 6 | export function createSchema( 7 | parse : SchemaFactory, 8 | unparse : SchemaFactory = irreversible('createSchema') 9 | ) : Schema_ { 10 | return mkSchema( 11 | mkParser(parse), 12 | mkParser(unparse) 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/schema/factories/mkParser.ts: -------------------------------------------------------------------------------- 1 | export { mkParser, mkParser_ } from './core' 2 | -------------------------------------------------------------------------------- /src/schema/factories/mkParserHaving.ts: -------------------------------------------------------------------------------- 1 | import { chain as chainA } from '../../action' 2 | import { Message } from 'bueno/locale' 3 | import { SchemaFactory, ForgetfulValidator, Parser_ } from "../../types" 4 | import { updateNestedMessagesP } from '../updateNestedMessages' 5 | import { mkParser } from "./mkParser" 6 | import { mkParserCallable } from "./core" 7 | import { constant, compose, toMessage } from '../../utils' 8 | 9 | function forgetful(v : Parser_) : ForgetfulValidator { 10 | return function(...vs) { 11 | return mkParserCallable(function (a : C, inv : boolean) { 12 | return chainA(v(...vs).run_(a, inv), r => { 13 | return { 14 | cnf_: r.cnf_, 15 | res_: constant(a), 16 | score_: r.score_ 17 | } 18 | }) 19 | }) 20 | } 21 | } 22 | 23 | export function mkParserHaving( 24 | msg : (msg : Message) => string | Message, 25 | factory : SchemaFactory 26 | ) : ForgetfulValidator { 27 | return forgetful(updateNestedMessagesP(compose(msg, toMessage), mkParser(factory))) 28 | } 29 | -------------------------------------------------------------------------------- /src/schema/factories/mkSchema.ts: -------------------------------------------------------------------------------- 1 | export { mkSchema } from './core' 2 | -------------------------------------------------------------------------------- /src/schema/factories/mkSchemaHaving.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'bueno/locale' 2 | import { SchemaFactory, Schema, Schema_, ForgetfulSchema } from '../../types' 3 | import { mkParserHaving } from './mkParserHaving' 4 | import { mkSchema } from './mkSchema' 5 | import { getUnparse, getParse } from '../../utils' 6 | 7 | export function mkSchemaHaving( 8 | msg : (msg : Message) => string | Message, 9 | parse : SchemaFactory 10 | ) : ForgetfulSchema { 11 | const mk = mkParserHaving(msg, parse) 12 | return function(...vs : Schema[]) : Schema_ { 13 | return mkSchema( 14 | mk(...vs.map(getParse)), 15 | mk(...vs.map(getUnparse)), 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/schema/fix.ts: -------------------------------------------------------------------------------- 1 | import { Schema_, Parser_, Parser } from "../types" 2 | import { mkSchema } from "./factories/mkSchema" 3 | import { mkParserCallable } from "./factories/core" 4 | 5 | export function errFix() : any { 6 | throw new Error('Strict usage of `fix`') 7 | } 8 | 9 | export function fixP( 10 | fn : (v : Parser) => Parser 11 | ) : Parser_ { 12 | let x : Parser = mkParserCallable(errFix) 13 | const check : Parser_ = 14 | mkParserCallable((a, inv) => x.run_(a, inv)) 15 | x = fn(check) 16 | return mkParserCallable(x.run_) 17 | } 18 | 19 | export function fix( 20 | fn : (v : Schema_) => Schema_ 21 | ) : Schema_ { 22 | let x : Schema_ = mkSchema( 23 | mkParserCallable(errFix), 24 | mkParserCallable(errFix) 25 | ) 26 | const check : Schema_ = mkSchema( 27 | mkParserCallable((a : A, inv : boolean) => x.parse_.run_(a, inv)), 28 | mkParserCallable((a : B, inv : boolean) => x.unparse_.run_(a, inv)) 29 | ) 30 | return x = fn(check) 31 | } 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/schema/flip.ts: -------------------------------------------------------------------------------- 1 | import { mkSchema } from "./factories/mkSchema" 2 | import { Schema, Schema_ } from "../types" 3 | 4 | export function flip(schema : Schema) : Schema_ { 5 | return mkSchema(schema.unparse_, schema.parse_) 6 | } 7 | -------------------------------------------------------------------------------- /src/schema/forget.ts: -------------------------------------------------------------------------------- 1 | import { chain } from '../action' 2 | import { Parser, Parser_, Schema_ } from '../types' 3 | import { mkParserCallable } from './factories/core' 4 | import { id } from './id' 5 | import { mkSchema } from './factories/mkSchema' 6 | import { constant } from '../utils' 7 | 8 | export function forgetP(v : Parser) : Parser_ { 9 | return mkParserCallable(function(a : A, inv : boolean) { 10 | return chain(v.run_(a, inv), r => { 11 | return { 12 | cnf_: r.cnf_, 13 | res_: constant(a), 14 | score_: r.score_ 15 | } 16 | }) 17 | }) 18 | } 19 | 20 | export function forget( 21 | schema : Schema_ 22 | ) : Schema_ { 23 | return mkSchema( 24 | forgetP(schema.parse_), 25 | id().parse_ 26 | ) 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/schema/id.ts: -------------------------------------------------------------------------------- 1 | export { id } from './factories/core'; 2 | -------------------------------------------------------------------------------- /src/schema/integer.ts: -------------------------------------------------------------------------------- 1 | import { mkParser_ } from "./factories/mkParser" 2 | import { mkSchema } from "./factories/mkSchema" 3 | 4 | export const integerP = mkParser_((n : number) => ({ 5 | validate_: { ok: Math.floor(n) === n, msg: l => l.integer }, 6 | result_: Math.floor(n) 7 | })) 8 | 9 | export const integer = 10 | mkSchema(integerP, integerP) 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/schema/irreversible.ts: -------------------------------------------------------------------------------- 1 | import { constant } from "../utils"; 2 | 3 | export let irreversible : (name : string) => () => any; 4 | 5 | if (IS_DEV) { 6 | irreversible = function(func : string) { 7 | return () => { 8 | throw new Error(`Schema irreversible as ${func} was used with a single argument.`) 9 | } 10 | } 11 | } 12 | else { 13 | irreversible = constant(undefined as any) 14 | } 15 | -------------------------------------------------------------------------------- /src/schema/length.ts: -------------------------------------------------------------------------------- 1 | import { ForgetfulValidator, Schema, Schema_ } from "../types" 2 | import { mkParserHaving } from "./factories/mkParserHaving" 3 | import { mkSchema } from "./factories/mkSchema" 4 | import { isNumber, getParse, getUnparse } from "../utils" 5 | 6 | export const lengthP : ForgetfulValidator<{ length : number }, number> = 7 | mkParserHaving( 8 | m => l => l.length(m(l)), 9 | (l : { length : number }) => { 10 | const ok = (l !== null && l !== undefined) && isNumber(l.length) 11 | return { 12 | parse: { ok, msg: l => l.either([l.array, l.string]) }, 13 | result: ok ? l.length : 0 14 | } 15 | }) 16 | 17 | export function length( 18 | ...vs : Schema[] 19 | ) : Schema_ { 20 | return mkSchema( 21 | lengthP(...vs.map(getParse)), 22 | lengthP(...vs.map(getUnparse)), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/schema/lessThan.ts: -------------------------------------------------------------------------------- 1 | import { mkParser_ } from './factories/mkParser' 2 | import { Parser_, Schema_ } from '../types' 3 | import { mkSchema } from './factories/mkSchema' 4 | 5 | export function lessThanP(lb : A) : Parser_ { 6 | return mkParser_(a => ({ 7 | validate_: { msg: l => l.less(lb), ok: a < lb }, 8 | result_: typeof a === typeof lb ? a : lb 9 | })) 10 | } 11 | 12 | export function lessThan(ub : number) : Schema_ { 13 | const v = lessThanP(ub) 14 | return mkSchema(v, v) 15 | } 16 | -------------------------------------------------------------------------------- /src/schema/lift.ts: -------------------------------------------------------------------------------- 1 | import { Parser_, Schema_ } from "../types" 2 | import { mkParser_ } from "./factories/mkParser" 3 | import { irreversible } from "./irreversible" 4 | import { mkSchema } from "./factories/mkSchema" 5 | 6 | export function liftP(f : (a : A) => B) : Parser_ { 7 | return mkParser_((x : A) => { 8 | try { 9 | return { result_: f(x) } 10 | } catch (e) { 11 | return { 12 | parse_: { ok: false, msg: l => l.fromString(e?.message ?? `${e}`) }, 13 | result_: null as any 14 | } 15 | } 16 | }) 17 | } 18 | 19 | export function lift( 20 | f : (a : A) => B, 21 | g : (a : B) => A = irreversible('lift'), 22 | ) : Schema_ { 23 | return mkSchema(liftP(f), liftP(g)) 24 | } 25 | -------------------------------------------------------------------------------- /src/schema/match.ts: -------------------------------------------------------------------------------- 1 | import { Parser_, Schema_ } from '../types' 2 | import { mkSchema } from './factories/mkSchema' 3 | import { mkParser_ } from './factories/mkParser' 4 | import { Message } from 'bueno/locale' 5 | import { isString } from '../utils' 6 | 7 | export function matchP(regex : RegExp, msg : Message) : Parser_ { 8 | return mkParser_((s : string) => { 9 | const result = isString(s) && s.match(regex) 10 | return { 11 | validate_: { ok: !!result && result[0] === s, msg }, 12 | result_: s 13 | } 14 | }) 15 | } 16 | 17 | export function match(regex : RegExp, msg : Message) : Schema_ { 18 | const v = matchP(regex, msg) 19 | return mkSchema(v, v) 20 | } 21 | -------------------------------------------------------------------------------- /src/schema/moreThan.ts: -------------------------------------------------------------------------------- 1 | import { mkParser_ } from './factories/mkParser' 2 | import { Parser_, Schema_ } from '../types' 3 | import { mkSchema } from './factories/mkSchema' 4 | 5 | export function moreThanP(ub : A) : Parser_ { 6 | return mkParser_(a => ({ 7 | validate_: { msg: l => l.more(ub), ok: a > ub }, 8 | result_: typeof a === typeof ub ? a : ub 9 | })) 10 | } 11 | 12 | export function moreThan(lb : number) : Schema_ { 13 | const v = moreThanP(lb) 14 | return mkSchema(v, v) 15 | } 16 | -------------------------------------------------------------------------------- /src/schema/not.ts: -------------------------------------------------------------------------------- 1 | export { notP } from './factories/core' 2 | import { mkSchema, notP } from './factories/core' 3 | import { Schema, Schema_ } from '../types' 4 | 5 | export function not(v : Schema) : Schema_ { 6 | return mkSchema( 7 | notP(v.parse_), 8 | notP(v.unparse_) 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/schema/number.ts: -------------------------------------------------------------------------------- 1 | import { Schema_ } from '../types' 2 | import { mkSchema } from './factories/mkSchema' 3 | import { mkParser_ } from './factories/mkParser' 4 | import { isNumber } from '../utils' 5 | 6 | export const numberP = mkParser_((a : number) => { 7 | const ok = isNumber(a) 8 | return { 9 | parse_: { ok, msg: l => l.number }, 10 | result_: ok ? a : 0, 11 | } 12 | }) 13 | 14 | export const number : Schema_ = 15 | mkSchema(numberP, numberP) 16 | -------------------------------------------------------------------------------- /src/schema/object.ts: -------------------------------------------------------------------------------- 1 | import { chain as chainA } from '../action' 2 | import { notP } from './not' 3 | import { mkSchema } from './factories/mkSchema' 4 | import { mkParser_ } from './factories/mkParser' 5 | import { Parser, Schema_, Schema, Parser_, UndefinedOptional, Obj } from '../types' 6 | import { Err, } from 'bueno/locale' 7 | import { mapValues, isObject, getParse, getUnparse, keys } from '../utils' 8 | import { mkParserCallable, everyP } from './factories/core' 9 | import { pipeP } from './pipe' 10 | 11 | export const anObject = mkParser_(o => ({ 12 | parse_: { ok: isObject(o), msg: l => l.object }, 13 | result_: (isObject(o) ? o : {}) as Obj, 14 | })) 15 | 16 | function atKey( 17 | key : K, 18 | v : Parser 19 | ) : Parser_<{ [k in K]: A }, { [k in K]: B }> { 20 | return mkParserCallable(function(a : { [k in K]: A }, inv : boolean) { 21 | return chainA(v.run_((a || {})[key], inv), c => { 22 | return { 23 | cnf_: () => chainA(c.cnf_(), ec => ec.map(c => c.map( 24 | d => ({ 25 | k: d.k, 26 | m: l => l.at(key as string, d.m(l)), 27 | ok: d.ok 28 | }) as Err 29 | ))), 30 | res_: () => chainA(c.res_(), rc => ({ [key]: rc })) as { [k in K]: B }, 31 | score_: c.score_ 32 | } 33 | }) 34 | }) 35 | } 36 | 37 | export function objectP( 38 | vs : { [Key in keyof BS]: Parser } 39 | ) : Parser_> { 40 | const ks = 41 | keys(vs) as (keyof BS)[] 42 | 43 | const byKey = 44 | ks.map((key : keyof BS) => atKey(key as string, vs[key])) 45 | 46 | const vpos : Parser_> = 47 | pipeP(anObject, everyP(byKey) as any) 48 | 49 | const vneg : Parser_> = 50 | pipeP(anObject, everyP(byKey.map(notP)) as any) 51 | 52 | return mkParserCallable(function(a : Obj, inv : boolean) { 53 | return (inv ? vneg : vpos).run_(a, false) 54 | }) 55 | } 56 | 57 | objectP.run_ = anObject.run_ 58 | 59 | export function object( 60 | vs : 61 | { [Key in keyof AS]: Schema } & 62 | { [Key in keyof BS]: Schema } 63 | ) : Schema_ { 64 | return mkSchema( 65 | objectP(mapValues(vs, getParse)) as any, 66 | objectP(mapValues(vs, getUnparse)) as any 67 | ) 68 | } 69 | 70 | object.parse_ = anObject 71 | object.unparse_ = anObject 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/schema/objectExact.ts: -------------------------------------------------------------------------------- 1 | import { mapValues, isObject, getUnparse, getParse, keys } from "../utils" 2 | import { Parser_, UndefinedOptional, Schema, Schema_, Parser, Obj } from "../types" 3 | import { objectP } from "./object" 4 | import { mkParserCallable } from "./factories/core" 5 | import { mkSchema } from "./factories/mkSchema" 6 | import { and } from "../logic" 7 | import { chain as chainA } from '../action' 8 | 9 | export function objectExactP( 10 | vs : { [Key in keyof BS]: Parser } 11 | ) : Parser_> { 12 | const v = objectP(vs) 13 | const ks = keys(vs) 14 | return mkParserCallable(function(a : Obj, inv : boolean) { 15 | return chainA(v.run_(a, inv), c => { 16 | if (isObject(a)) { 17 | const extraKeys = keys(a as any).filter(x => !ks.some(y => y === x)) 18 | const score = ks.length / (ks.length + extraKeys.length) 19 | return { 20 | cnf_: () => chainA(c.cnf_(), e => { 21 | if (extraKeys.length) { 22 | return and(e, [[{ 23 | k: 'p', 24 | m: l => l.keys(extraKeys), 25 | ok: false 26 | }]]) 27 | } 28 | return e 29 | }), 30 | res_: c.res_, 31 | score_: (c.score_ + score) / 2 32 | } 33 | } 34 | return c 35 | }) 36 | }) 37 | } 38 | 39 | export function objectExact( 40 | vs : 41 | { [Key in keyof AS]: Schema } & 42 | { [Key in keyof BS]: Schema } 43 | ) : Schema_, UndefinedOptional> { 44 | return mkSchema( 45 | objectExactP(mapValues(vs, getParse)) as any, 46 | objectExactP(mapValues(vs, getUnparse)) as any 47 | ) 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/schema/objectInexact.ts: -------------------------------------------------------------------------------- 1 | import { Parser, Parser_, UndefinedOptional, Schema, Schema_, Obj } from "../types" 2 | import { objectP } from "./object" 3 | import { mkParserCallable } from "./factories/core" 4 | import { mkSchema } from "./factories/mkSchema" 5 | import { mapValues, isObject, getUnparse, getParse } from "../utils" 6 | import { chain as chainA } from '../action' 7 | 8 | export function objectInexactP( 9 | vs : { [Key in keyof BS]: Parser } 10 | ) : 11 | Parser_> { 12 | const v = objectP(vs) 13 | return mkParserCallable(function(a : Obj, inv : boolean) { 14 | return chainA(v.run_(a, inv), c => ({ 15 | cnf_: c.cnf_, 16 | res_: () => isObject(a) 17 | ? chainA(c.res_(), rc => ({ ...a, ...rc } as any)) 18 | : c.res_, 19 | score_: c.score_ 20 | })) 21 | }) 22 | } 23 | 24 | export function objectInexact( 25 | vs : 26 | { [Key in keyof AS]: Schema } & 27 | { [Key in keyof BS]: Schema } 28 | ) : Schema_ & { [k in string]: any }, UndefinedOptional & { [k in string]: any }> { 29 | return mkSchema( 30 | objectInexactP(mapValues(vs, getParse)) as any, 31 | objectInexactP(mapValues(vs, getUnparse)) as any 32 | ) 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/schema/odd.ts: -------------------------------------------------------------------------------- 1 | import { mkSchema } from "./factories/mkSchema" 2 | import { mkParser_ } from "./factories/mkParser" 3 | import { pipeP } from "./pipe" 4 | import { numberP } from "./number" 5 | 6 | export const oddP = pipeP(numberP, mkParser_((a : number) => ({ 7 | validate_: { msg: l => l.odd, ok: !!(a % 2) }, 8 | result_: a 9 | }))) 10 | 11 | export const odd = 12 | mkSchema(oddP, oddP) 13 | -------------------------------------------------------------------------------- /src/schema/oneOf.ts: -------------------------------------------------------------------------------- 1 | import { Parser_, Schema_ } from "../types" 2 | import { mkParser_ } from "./factories/mkParser" 3 | import { mkSchema } from "./factories/mkSchema" 4 | 5 | export function oneOfP( 6 | ...choices : A[] 7 | ) : Parser_ { 8 | return mkParser_(a => { 9 | const ok = choices.indexOf(a) >= 0 10 | return { 11 | validate_: { 12 | ok, 13 | msg: l => l.oneOf(choices) 14 | }, 15 | result_: ok || choices.length === 0 ? a : choices[0] 16 | } 17 | }) 18 | } 19 | 20 | export function oneOf( 21 | ...choices : A[] 22 | ) : Schema_ { 23 | const v = oneOfP(...choices) 24 | return mkSchema(v, v) 25 | } 26 | -------------------------------------------------------------------------------- /src/schema/optional.ts: -------------------------------------------------------------------------------- 1 | import { exactlyP } from "./exactly" 2 | import { Parser_, Schema, Schema_, Parser } from "../types" 3 | import { eitherP } from "./either" 4 | import { mkSchema } from "./factories/mkSchema" 5 | import { setMessageP } from "./setMessage" 6 | 7 | export function optionalP( 8 | v : Parser 9 | ) : Parser_ { 10 | return eitherP(v, setMessageP(exactlyP(undefined), l => l.leftOut)) 11 | } 12 | 13 | export function optional( 14 | v : Schema 15 | ) : Schema_ { 16 | return mkSchema( 17 | optionalP(v.parse_), 18 | optionalP(v.unparse_), 19 | ) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/schema/optionalTo.ts: -------------------------------------------------------------------------------- 1 | import { swap } from './swap' 2 | import { Schema } from '../types' 3 | 4 | export function optionalTo(to : A) : Schema { 5 | return swap([[null, to], [undefined, to]]) 6 | } 7 | -------------------------------------------------------------------------------- /src/schema/pair.ts: -------------------------------------------------------------------------------- 1 | import { Parser, Parser_, Schema, Schema_ } from "../types" 2 | import { mkParserCallable } from "./factories/core" 3 | import { pairA as pairA } from "../action" 4 | import { and, mapCNF } from "../logic" 5 | import { mapError } from "../Builtins" 6 | import { mkSchema } from "./factories/mkSchema" 7 | 8 | export function pairP( 9 | v : Parser, 10 | w : Parser 11 | ) : Parser_<[A, B], [C, D]> { 12 | return mkParserCallable(function(kv : [A, B], inv) { 13 | const a = kv[0] 14 | const b = kv[1] 15 | return pairA(v.run_(a, inv), w.run_(b, inv), (a, b) => { 16 | return { 17 | cnf_: () => pairA( 18 | a.cnf_(), 19 | b.cnf_(), 20 | (a, b) => and( 21 | mapCNF(mapError(m => l => l.at('key', m(l))))(a), 22 | mapCNF(mapError(m => l => l.at('value', m(l))))(b) 23 | ) 24 | ), 25 | res_: () => pairA(a.res_(), b.res_(), (a, b) => [a, b]), 26 | score_: (a.score_ + b.score_) / 2 27 | } 28 | }) 29 | }) 30 | } 31 | 32 | export function pair( 33 | v : Schema, 34 | w : Schema 35 | ) : Schema_<[A, B], [C, D]> { 36 | return mkSchema( 37 | pairP(v.parse_, w.parse_), 38 | pairP(v.unparse_, w.unparse_), 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/schema/path.ts: -------------------------------------------------------------------------------- 1 | import { Parser, Parser_, Schema } from "../types" 2 | import { updateMessageP } from "./updateMessage" 3 | import { mkSchema } from "./factories/mkSchema" 4 | 5 | export function pathP( 6 | path : string, 7 | v : Parser 8 | ) : Parser_ { 9 | return updateMessageP(m => l => l.at('_#_' + path, m(l)), v) 10 | } 11 | 12 | export function path(path : string, v : Schema) : Schema { 13 | return mkSchema(pathP(path, v.parse_), pathP(path, v.unparse_)) 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/schema/pipe.ts: -------------------------------------------------------------------------------- 1 | export { pipe, pipeP } from './factories/core' 2 | -------------------------------------------------------------------------------- /src/schema/pure.ts: -------------------------------------------------------------------------------- 1 | import { mkParserCallable } from "./factories/core" 2 | import { Parser_, Schema_ } from "../types" 3 | import { mkSchema } from "./factories/mkSchema" 4 | import { constant } from "../utils" 5 | 6 | export function pureP(a : A) : Parser_ { 7 | return mkParserCallable(constant({ 8 | cnf_: constant([]), 9 | res_: constant(a), 10 | score_: 1 11 | })) 12 | } 13 | 14 | export function pure( 15 | a : A 16 | ) : Schema_ { 17 | const p = pureP(a) 18 | return mkSchema(p, p) 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/schema/runners/check.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../../types' 2 | import { Action, chain } from '../../action' 3 | import { ir } from './cnf' 4 | import { Locale } from '../../index' 5 | import { sync } from './sync' 6 | 7 | export function checkAsync( 8 | value : A, 9 | schema : Schema, 10 | locale : Locale 11 | ) : Action { 12 | return chain(ir(value, schema, locale), msgs => { 13 | return msgs[0] 14 | ? locale.renderer.render(msgs[0], []) 15 | : null 16 | }) 17 | } 18 | 19 | export const check = sync(checkAsync) 20 | -------------------------------------------------------------------------------- /src/schema/runners/checkByKey.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Obj } from '../../types' 2 | import { Locale } from '../../index' 3 | import { chain, Action } from '../../action'; 4 | import { ir } from './cnf'; 5 | import { deepMerge } from '../../utils'; 6 | import { sync } from './sync'; 7 | 8 | export declare type Errors = 9 | { [K in keyof Values]?: Values[K] extends any[] ? Values[K][number] extends Obj ? Errors[] | string | string[] : string | string[] : Values[K] extends Obj ? Errors : string; }; 10 | 11 | export function checkPerKeyAsync( 12 | value : A, 13 | schema : Schema, 14 | locale : Locale 15 | ) : Action> { 16 | return chain(ir(value, schema, locale), msgs => { 17 | return deepMerge( 18 | msgs.map(msg => locale.renderer.byKey(msg, [])).reverse() 19 | ) 20 | }) 21 | } 22 | 23 | export const checkPerKey = sync(checkPerKeyAsync) 24 | -------------------------------------------------------------------------------- /src/schema/runners/cnf.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from '../../types' 2 | import { Action, chain } from '../../action' 3 | import { CNF } from '../../logic' 4 | import { Err } from 'bueno/locale' 5 | import { Locale } from '../../index' 6 | import { sync } from './sync' 7 | 8 | export function cnfAsync( 9 | value : A, 10 | schema : Schema 11 | ) : Action> { 12 | const result = schema.parse_.run_(value, false) 13 | return chain(result, r => r.cnf_()) 14 | } 15 | 16 | export const cnf = sync(cnfAsync) 17 | 18 | export function ir( 19 | value : A, 20 | schema : Schema, 21 | language : Locale 22 | ) : Action { 23 | return chain(cnfAsync(value, schema), cnf => { 24 | return cnf 25 | .filter(x => !x.some(y => y.ok)) 26 | .map(disj => language.builder.either(disj.map(x => x.m(language.builder)))) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/schema/runners/result.ts: -------------------------------------------------------------------------------- 1 | import { Schema } from "../../types" 2 | import { chain, Action } from "../../action" 3 | import { sync } from './sync' 4 | 5 | export function resultAsync( 6 | value : A, 7 | schema : Schema 8 | ) : Action { 9 | return chain(schema.parse_.run_(value, false), r => r.res_()) 10 | } 11 | 12 | export const result = sync(resultAsync) 13 | -------------------------------------------------------------------------------- /src/schema/runners/sync.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '../../action' 2 | 3 | export function sync( 4 | a : (...args : B) => Action 5 | ) : (...args : B) => A { 6 | if (IS_DEV) { 7 | if (a instanceof Promise) { 8 | throw new Error('Using sync runner function on an async parser. Use its async version instead.') 9 | } 10 | } 11 | return a as any 12 | } 13 | -------------------------------------------------------------------------------- /src/schema/self.ts: -------------------------------------------------------------------------------- 1 | import { mkSchema } from "./factories/mkSchema" 2 | import { Action, chain } from "../action" 3 | import { Parser, Parser_, Schema, Schema_ } from "../types" 4 | import { mkParserCallable } from "./factories/core" 5 | import { irreversible } from "./irreversible" 6 | 7 | export function selfP(fn : (a : A) => Action>) : Parser_ { 8 | return mkParserCallable(function(a : A, inv : boolean) { 9 | return chain(fn(a), v => v.run_(a, inv)) 10 | }) 11 | } 12 | 13 | export function self( 14 | from : (a : A) => Action>, 15 | to : (b : B) => Action> = irreversible('self') 16 | ) : Schema_ { 17 | return mkSchema( 18 | mkParserCallable(function(a : A, inv : boolean) { 19 | return chain(from(a), p => p.parse_.run_(a, inv)) 20 | }), 21 | mkParserCallable(function(b : B, inv : boolean) { 22 | return chain(to(b), p => p.unparse_.run_(b, inv)) 23 | }), 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/schema/setMessage.ts: -------------------------------------------------------------------------------- 1 | import { Schema_, Parser, Schema } from "../types" 2 | import { Message } from 'bueno/locale' 3 | import { updateMessage, updateMessageP } from "./updateMessage" 4 | import { constant } from "../utils" 5 | 6 | export function setMessageP( 7 | v : Parser, 8 | m : Message | string 9 | ) { 10 | return updateMessageP(constant(m), v) 11 | } 12 | 13 | export function setMessage( 14 | v : Schema, 15 | m : Message | string 16 | ) : Schema_ { 17 | return updateMessage(constant(m), v) 18 | } 19 | -------------------------------------------------------------------------------- /src/schema/size.ts: -------------------------------------------------------------------------------- 1 | import { Schema, ForgetfulValidator, Schema_ } from "../types" 2 | import { mkParserHaving } from "./factories/mkParserHaving" 3 | import { mkSchema } from "./factories/mkSchema" 4 | import { isNumber, getUnparse, getParse } from "../utils" 5 | 6 | export const sizeP : ForgetfulValidator<{ size : number }, number> = 7 | mkParserHaving( 8 | m => l => l.length(m(l)), 9 | (arr : { size : number }) => { 10 | const ok = arr && isNumber(arr.size) 11 | return { 12 | parse: { ok, msg: l => l.either([l.array, l.string]) }, 13 | result: ok ? arr.size : 0 14 | } 15 | }) 16 | 17 | export function size( 18 | ...vs : Schema[] 19 | ) : Schema_ { 20 | return mkSchema( 21 | sizeP(...vs.map(getParse)), 22 | sizeP(...vs.map(getUnparse)), 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/schema/some.ts: -------------------------------------------------------------------------------- 1 | import { Parser, Parser_, Schema, Schema_ } from '../types' 2 | import { mkSchema } from './factories/mkSchema' 3 | import { anyP } from './any' 4 | import { eitherP } from './either' 5 | import { isEmpty, getParse, getUnparse } from '../utils' 6 | 7 | export function someP( 8 | vs : Parser[] 9 | ) : Parser_ { 10 | if (isEmpty(vs)) return anyP 11 | return (vs as any).reduce(eitherP) 12 | } 13 | 14 | export function some( 15 | ...vs : Schema[] 16 | ) : Schema_ { 17 | return mkSchema( 18 | someP(vs.map(getParse)), 19 | someP(vs.map(getUnparse)) 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/string.ts: -------------------------------------------------------------------------------- 1 | import { Schema_ } from '../types' 2 | import { mkSchema } from './factories/mkSchema' 3 | import { mkParser_ } from './factories/mkParser' 4 | import { isString } from '../utils' 5 | 6 | export const stringP = mkParser_((a : string) => { 7 | const ok = isString(a) 8 | return { 9 | parse_: { ok, msg: l => l.string }, 10 | result_: ok ? a : '' 11 | } 12 | }) 13 | 14 | export const string : Schema_ = 15 | mkSchema(stringP, stringP) 16 | -------------------------------------------------------------------------------- /src/schema/sum.ts: -------------------------------------------------------------------------------- 1 | import { ForgetfulValidator, Schema, Schema_ } from "../types" 2 | import { mkParserHaving } from "./factories/mkParserHaving" 3 | import { mkSchema } from "./factories/mkSchema" 4 | import { getParse, getUnparse } from "../utils" 5 | 6 | export const sumP : ForgetfulValidator = 7 | mkParserHaving( 8 | m => l => l.sum(m(l)), 9 | (arr : number[]) => ({ 10 | result: arr.reduce((x, y) => x + y, 0), 11 | })) 12 | 13 | export function sum( 14 | ...vs : Schema[] 15 | ) : Schema_ { 16 | return mkSchema( 17 | sumP(...vs.map(getParse)), 18 | sumP(...vs.map(getUnparse)), 19 | ) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/schema/swap.ts: -------------------------------------------------------------------------------- 1 | import { createSchema } from './factories/createSchema' 2 | import { Schema_ } from '../types' 3 | 4 | export function swap(dict : [B, A][]) : Schema_ { 5 | return createSchema( 6 | (a : A | B) => { 7 | const ix = dict.findIndex(x => x[0] === a) 8 | return { 9 | result: (ix >= 0 ? dict[ix][1] : a) as any 10 | } 11 | }, 12 | (a : A) => { 13 | const ix = dict.findIndex(x => x[1] === a) 14 | return { 15 | result: ix >= 0 ? dict[ix][0] : a 16 | } 17 | }, 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/schema/toDate.ts: -------------------------------------------------------------------------------- 1 | import { pipeP } from "./pipe" 2 | import { stringP } from "./string" 3 | import { mkParser_ } from "./factories/mkParser" 4 | import { mkSchema } from "./factories/mkSchema" 5 | import { dateP, invalidDate } from "./date" 6 | 7 | export const toDateP = pipeP(stringP, mkParser_((s : string) => { 8 | const date = new Date(s) 9 | const ok = !isNaN(date.getTime()) 10 | return { 11 | parse_: { ok, msg: l => l.date }, 12 | result_: ok ? date : invalidDate 13 | } 14 | })) 15 | 16 | export const dateToString = pipeP(dateP, mkParser_((date : Date) => { 17 | const ok = 18 | !isNaN(date.getTime()) 19 | const str = 20 | ok ? date.toISOString().split('T')[0] : '' 21 | return { 22 | parse_: { 23 | ok, 24 | msg: l => l.date 25 | }, 26 | result_: str 27 | } 28 | })) 29 | 30 | export const toDate = mkSchema( 31 | toDateP, 32 | dateToString 33 | ) 34 | -------------------------------------------------------------------------------- /src/schema/toJSON.ts: -------------------------------------------------------------------------------- 1 | import { Parser_, Schema_ } from "../types" 2 | import { pipeP } from "./pipe" 3 | import { stringP } from "./string" 4 | import { toStringP } from "./toString" 5 | import { mkParser_ } from "./factories/mkParser" 6 | import { mkSchema } from "./factories/mkSchema" 7 | import { object } from "./object" 8 | 9 | export const toJSONP : Parser_ = pipeP(stringP, mkParser_((s : string) => { 10 | let result = null 11 | let ok = false 12 | try { 13 | result = JSON.parse(s) 14 | ok = true 15 | } catch (e) { } 16 | return { 17 | parse_: { ok, msg: l => l.json }, 18 | result_: result 19 | } 20 | })) 21 | 22 | export const toJSON : Schema_ = mkSchema( 23 | pipeP(stringP, toJSONP), 24 | pipeP(object.parse_, toStringP()) 25 | ) 26 | -------------------------------------------------------------------------------- /src/schema/toNumber.ts: -------------------------------------------------------------------------------- 1 | import { pipeP } from './pipe' 2 | import { stringP } from './string' 3 | import { numberP } from './number' 4 | import { toStringP } from './toString' 5 | import { Parser_, Schema_ } from '../types' 6 | import { mkSchema } from './factories/mkSchema' 7 | import { mkParser_ } from './factories/mkParser' 8 | 9 | export const toNumberP : Parser_ = 10 | pipeP(stringP, mkParser_((s : string) => { 11 | const result = Number(s) 12 | const ok = s !== '' && !isNaN(result) 13 | return { 14 | parse_: { ok, msg: l => l.number }, 15 | result_: ok ? result : 0 16 | } 17 | })) 18 | 19 | export const toNumber : Schema_ = mkSchema( 20 | toNumberP, 21 | pipeP(numberP, toStringP()) 22 | ) 23 | 24 | -------------------------------------------------------------------------------- /src/schema/toString.ts: -------------------------------------------------------------------------------- 1 | import { mkSchema } from "./factories/mkSchema" 2 | import { mkParser_ } from "./factories/mkParser" 3 | import { Parser_, Schema_ } from "../types" 4 | import { mkParserCallable } from "./factories/core" 5 | import { irreversible } from "./irreversible" 6 | 7 | const _toString = mkParser_((a : any) => ({ 8 | result_: `${a}` 9 | })) 10 | 11 | export function toStringP() : Parser_ { 12 | return _toString 13 | } 14 | 15 | const schema = mkSchema( 16 | toStringP(), 17 | mkParserCallable(irreversible('toString')) 18 | ) 19 | 20 | export function toString() : Schema_ { 21 | return schema 22 | } 23 | -------------------------------------------------------------------------------- /src/schema/toURL.ts: -------------------------------------------------------------------------------- 1 | import { mkSchema } from "./factories/mkSchema" 2 | import { mkParser_ } from "./factories/mkParser" 3 | import { Parser_ } from "../types" 4 | import { toStringP } from "./toString" 5 | 6 | export const toURLP : Parser_ = mkParser_(s => { 7 | let ok = false 8 | let url : URL = new URL('http://www.example.com') 9 | try { 10 | url = new URL(s) 11 | ok = true 12 | } catch (e) { } 13 | return { parse_: { ok, msg: l => l.url }, result_: url } 14 | }) 15 | 16 | export const toURL = 17 | mkSchema(toURLP, toStringP()) 18 | -------------------------------------------------------------------------------- /src/schema/unknown.ts: -------------------------------------------------------------------------------- 1 | import { Schema_ } from '../types' 2 | import { mkSchema } from './factories/mkSchema' 3 | import { anyP } from './factories/core' 4 | 5 | export const unknown : Schema_ = 6 | mkSchema(anyP as any, anyP as any) 7 | -------------------------------------------------------------------------------- /src/schema/updateMessage.ts: -------------------------------------------------------------------------------- 1 | import { Schema_, Parser, ParseResult, Schema } from "../types" 2 | import { Message } from 'bueno/locale' 3 | import { mkSchema } from "./factories/mkSchema" 4 | import { mkParserCallable } from "./factories/core" 5 | import { Action, chain } from "../action" 6 | import { mapCNF } from "../logic" 7 | import { mapError } from "../Builtins" 8 | import { isString } from "../utils" 9 | 10 | function fromString(m : string | Message) : Message { 11 | return isString(m) ? (l => l.fromString(m as string)) : (m as Message) 12 | } 13 | 14 | export function updateMessageP( 15 | f : (m : Message) => (string | Message), 16 | v : Parser 17 | ) { 18 | return mkParserCallable(function(a : A, inv : boolean) : Action> { 19 | 20 | return chain(v.run_(a, inv), x => { 21 | return { 22 | cnf_: () => chain(x.cnf_(), mapCNF(mapError(e => fromString(f(e))))), 23 | res_: x.res_, 24 | score_: x.score_ 25 | } 26 | }) 27 | }) 28 | } 29 | 30 | export function updateMessage( 31 | f : (m : Message) => (string | Message), 32 | v : Schema, 33 | ) : Schema_ { 34 | return mkSchema( 35 | updateMessageP(f, v.parse_), 36 | updateMessageP(f, v.unparse_), 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/schema/updateNestedMessages.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'bueno/locale' 2 | import { Parser_, Schema_ } from "../types" 3 | import { updateMessageP } from "./updateMessage" 4 | import { mkSchema } from "./factories/mkSchema" 5 | 6 | export function updateNestedMessagesP( 7 | f : (m : Message) => Message, 8 | v : Parser_ 9 | ) : Parser_ { 10 | function call(...vs : Parser_[]) : Parser_ { 11 | return v(...vs.map(x => updateMessageP(f, x))) 12 | } 13 | call.run_ = v.run_ 14 | return call 15 | } 16 | 17 | export function updateNestedMessages( 18 | f : (m : Message) => Message, 19 | v : Schema_, 20 | ) : Schema_ { 21 | return mkSchema( 22 | updateNestedMessagesP(f, v.parse_), 23 | updateNestedMessagesP(f, v.unparse_), 24 | ) 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/schema/when.ts: -------------------------------------------------------------------------------- 1 | import { mkParserCallable } from "./factories/core" 2 | import { pairA, Action, chain } from "../action" 3 | import { CNF } from "../logic" 4 | import { Err, Message } from 'bueno/locale' 5 | import { Parser, Parser_, Schema, Schema_ } from "../types" 6 | import { anyP } from "./any" 7 | import { negate } from "../Builtins" 8 | import { mkSchema } from "./factories/mkSchema" 9 | import { id } from "./id" 10 | 11 | const whenMkError = ( 12 | cond : () => Action>, 13 | conseq : () => Action>, 14 | fscore : number 15 | ) : Action> => { 16 | return pairA(cond(), conseq(), (cond, conseq) => { 17 | const reason = 18 | cond 19 | .map(disj => disj.filter(x => x.ok)) 20 | .filter(x => x.length) 21 | return ( 22 | conseq.filter(disj => disj.some(x => !x.ok)).map(disj => { 23 | return [{ 24 | k: 'v', 25 | m: (l => l.when( 26 | l.either(disj.map(x => x.m(l))), 27 | reason.map(r => l.either(r.map(e => e.m(l)))), 28 | )) as Message, 29 | ok: fscore === 1 30 | }] 31 | }) 32 | ) 33 | }) 34 | } 35 | 36 | export function whenP( 37 | cond : Parser, 38 | consequent : Parser, 39 | alternative : Parser = anyP 40 | ) : Parser_ { 41 | return mkParserCallable(function(a : A, inv : boolean) { 42 | return chain(cond.run_(a, inv), cond => { 43 | if (cond.score_ === 1) { 44 | return chain(consequent.run_(a, false), cons => { 45 | return { 46 | cnf_: () => whenMkError(cond.cnf_, cons.cnf_, cons.score_), 47 | res_: cons.res_, 48 | score_: cons.score_ 49 | } 50 | }) 51 | } else { 52 | return chain(alternative.run_(a, false), alter => ({ 53 | cnf_: () => whenMkError( 54 | () => chain(cond.cnf_(), negate), 55 | alter.cnf_, alter.score_), 56 | res_: alter.res_, 57 | score_: alter.score_ 58 | })) 59 | } 60 | }) 61 | }) 62 | } 63 | 64 | export function when(cond : Schema, consequent : Schema) : Schema_ 65 | export function when(cond : Schema, consequent : Schema, alternative : Schema) : Schema_ 66 | export function when(cond : Schema, consequent : Schema, alternative : Schema = id()) : Schema_ { 67 | return mkSchema( 68 | whenP(cond.parse_, consequent.parse_, alternative.parse_), 69 | whenP(cond.unparse_, consequent.unparse_, alternative.unparse_) 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { Action } from './action' 2 | import { CNF } from './logic' 3 | import { Err, Message } from 'bueno/locale' 4 | 5 | export type ParseResult = { 6 | // cnf_ / res_ are "thunks" (i.e. zero-argument functions) in 7 | // order to allow for sufficient laziness when using 8 | // combinators like `fix` to create recursive schemas. 9 | cnf_ : () => Action>, 10 | res_ : () => Action, 11 | // Rather than using a boolean to indicate success / failure, 12 | // validators return a score that must lie between 0 and 1 (1 is 13 | // perfect, 0 is complete failure) to indicate how well a parse 14 | // succeeded. This allows us to choose better return values in 15 | // ambiguous situations (for example when both validators match in 16 | // an `either ) 17 | score_ : number 18 | } 19 | 20 | export interface Parser { 21 | // This method runs the validation on its input... 22 | run_ : (a : A, inverted : boolean) => Action> 23 | } 24 | 25 | export interface Parser_ extends Parser { 26 | // ...we also allow validators to be "chained" using the call 27 | // signature (entirely for syntactic convenience). 28 | (...vs : Parser_[]) : Parser_ 29 | } 30 | 31 | export interface Schema { 32 | parse_ : Parser_ 33 | unparse_ : Parser_ 34 | } 35 | 36 | export interface Schema_ extends Schema { 37 | (...ss : Schema[]) : Schema_ 38 | } 39 | 40 | export type SchemaFactory = (a : A) => Action<{ 41 | parse?: { ok : boolean, msg : Message }, 42 | validate?: { ok : boolean, msg : Message }, 43 | result?: B, 44 | score?: number 45 | }> 46 | 47 | export type UndefinedOptional = 48 | { [P in keyof A]?: A[P] } & 49 | { [L in { [K in keyof A]: undefined extends A[K] ? never : K }[keyof A]]: A[L] } 50 | 51 | export type ForgetfulValidator = 52 | (...vs : Parser_[]) => Parser_ 53 | 54 | export type ForgetfulSchema = 55 | (...vs : Schema[]) => Schema_ 56 | 57 | export type Messages = string | { 58 | [k in string]: Messages 59 | } 60 | 61 | export type Obj = Record 62 | 63 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "bueno/locale" 2 | 3 | export const id = (x : A) => x 4 | 5 | export const isDigits = (x : string) => 6 | x.match(/\d+/) !== null 7 | 8 | export function isEmpty(a : A[]) : boolean { 9 | return !a.length 10 | } 11 | 12 | export function isString(a : any) : boolean { 13 | return typeof a === 'string' 14 | } 15 | 16 | export function isNumber(a : any) : boolean { 17 | return typeof a === 'number' 18 | } 19 | 20 | export function isPromise(a : any) : boolean { 21 | return a instanceof Promise 22 | } 23 | 24 | export function constant(a : A) : () => A { 25 | return () => a 26 | } 27 | 28 | export function push(a : A, as : A[]) : A[] { 29 | as.push(a) 30 | return as 31 | } 32 | 33 | export function entries( 34 | obj : { [key in A]: B }, 35 | ) : [A, B][] { 36 | const result : [A, B][] = [] 37 | for (const key in obj) { 38 | result.push([key, obj[key]] as [A, B]) 39 | } 40 | return result 41 | } 42 | 43 | export function fromEntries( 44 | entries : [A, B][] 45 | ) : { [key in A]: B } { 46 | const result : { [key in A]: B } = {} as { [key in A]: B } 47 | entries.forEach(kv => { 48 | result[kv[0]] = kv[1] 49 | }) 50 | return result 51 | } 52 | 53 | export function mapEntries( 54 | obj : { [key in K]: A }, 55 | f : (key : K, val : A) => [K, B] 56 | ) : { [key in K]: B } { 57 | const result : { [key in K]: B } = {} as any 58 | for (const key in obj) { 59 | const kv = f(key, obj[key]) 60 | result[kv[0]] = kv[1] 61 | } 62 | return result 63 | } 64 | 65 | export function isIterable(a : any) : a is Iterable { 66 | return !!a && typeof a[Symbol.iterator] === 'function' 67 | } 68 | 69 | export function average(scores : number[]) : number { 70 | if (isEmpty(scores)) { 71 | return 1 72 | } 73 | return scores.reduce((x, y) => x + y, 0) / scores.length 74 | } 75 | 76 | export function mapValues( 77 | x : { [key in KS]: A }, 78 | f : (a : A) => B 79 | ) : { [key in KS]: B } { 80 | return mapEntries(x, (k, v) => [k, f(v as any)]) as any 81 | } 82 | 83 | export function assignPath(obj : any, path : string[], val : A) { 84 | if (isEmpty(path)) return obj 85 | if (path.length === 1) { 86 | if (isObject(obj[path[0]])) { 87 | obj[path[0]][''] = val 88 | } else { 89 | obj[path[0]] = val 90 | } 91 | } 92 | if (path.length > 1) { 93 | if (obj[path[0]] === void 0) { 94 | obj[path[0]] = {} 95 | } 96 | if (isString(obj[path[0]])) { 97 | obj[path[0]] = { '': obj[path[0]] } 98 | } 99 | assignPath(obj[path[0]], path.slice(1), val) 100 | } 101 | return obj 102 | } 103 | 104 | export function joinWithCommas( 105 | as : string[], 106 | separator : string 107 | ) : string { 108 | if (isEmpty(as)) { 109 | return '' 110 | } 111 | if (as.length < 2) { 112 | return as[0] 113 | } 114 | const copy = [...as] 115 | return copy.splice(0, as.length - 1).join(', ') 116 | + ' ' 117 | + separator 118 | + ' ' 119 | + copy[0] 120 | } 121 | 122 | export function capFirst(s : string) { 123 | return s.charAt(0).toUpperCase() + s.slice(1) 124 | } 125 | 126 | export function isObject(obj : any) { 127 | return obj !== null && typeof obj === 'object' 128 | } 129 | 130 | export const isArray = 131 | Array.isArray 132 | 133 | export const keys = 134 | Object.keys 135 | 136 | type TUnionToIntersection = ( 137 | U extends any ? (k : U) => void : never 138 | ) extends (k : infer I) => void 139 | ? I 140 | : never 141 | 142 | export function deepMerge( 143 | objects : T 144 | ) : TUnionToIntersection { 145 | return objects.reduce((result, current) => { 146 | keys(current).forEach((key) => { 147 | if (isArray(result[key]) && isArray(current[key])) { 148 | result[key] = result[key].concat(current[key]) 149 | } else if (isObject(result[key]) && isObject(current[key])) { 150 | result[key] = deepMerge([result[key], current[key]]) 151 | } else { 152 | result[key] = current[key] 153 | } 154 | }) 155 | return result 156 | }, {}) as any 157 | } 158 | 159 | export function words(args : string[]) { 160 | return args.filter(Boolean).join(' ') 161 | } 162 | 163 | export function byEmpty(x : string, y : string) : number { 164 | if (x === '') return -1 165 | if (y === '') return 1 166 | return 0 167 | } 168 | 169 | export function getParse(x : { parse_ : any }) { 170 | return x.parse_ 171 | } 172 | 173 | export function getUnparse(x : { unparse_ : any }) { 174 | return x.unparse_ 175 | } 176 | 177 | export function toMessage(s : string | Message) : Message { 178 | return isString(s) ? (l => l.fromString(s as string)) : (s as Message) 179 | } 180 | 181 | export function compose(f : (a : A) => B, g : (b : B) => C) : (a : A) => C { 182 | return x => g(f(x)) 183 | } 184 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*.ts"], 3 | "compilerOptions": { 4 | /* Basic Options */ 5 | "incremental": true, /* Enable incremental compilation */ 6 | "target": "es3", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 7 | "module": "es2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | "lib": ["es2015", "dom"], /* Specify library files to be included in the compilation. */ 9 | // "allowJs": true, /* Allow javascript files to be compiled. */ 10 | // "checkJs": true, /* Report errors in .js files. */ 11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 12 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./dist", /* Redirect output structure to the directory. */ 17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true, /* Enable all strict type-checking options. */ 28 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | "strictNullChecks": true, /* Enable strict null checks. */ 30 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | "resolveJsonModule": true, 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tsfmt.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseIndentSize": 0, 3 | "indentSize": 2, 4 | "tabSize": 2, 5 | "indentStyle": 2, 6 | "newLineCharacter": "\n", 7 | "convertTabsToSpaces": true, 8 | "insertSpaceAfterCommaDelimiter": true, 9 | "insertSpaceAfterSemicolonInForStatements": true, 10 | "insertSpaceBeforeAndAfterBinaryOperators": true, 11 | "insertSpaceAfterConstructor": false, 12 | "insertSpaceAfterKeywordsInControlFlowStatements": true, 13 | "insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, 14 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, 15 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, 16 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true, 17 | "insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": false, 18 | "insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": false, 19 | "insertSpaceAfterTypeAssertion": false, 20 | "insertSpaceBeforeFunctionParenthesis": false, 21 | "insertSpaceBeforeTypeAnnotation": true, 22 | "placeOpenBraceOnNewLineForFunctions": false, 23 | "placeOpenBraceOnNewLineForControlBlocks": false 24 | } 25 | --------------------------------------------------------------------------------