├── .github └── workflows │ └── test.yml ├── .gitignore ├── Makefile ├── README.md ├── dist.ini ├── spec ├── moonscript_spec.moon ├── tableshape_spec.moon ├── tags_spec.moon └── transform_spec.moon ├── tableshape-dev-1.rockspec ├── tableshape.lua ├── tableshape.moon └── tableshape ├── init.lua ├── init.moon ├── luassert.lua ├── luassert.moon ├── moonscript.lua └── moonscript.moon /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | luaVersion: ["5.1", "luajit", "5.2", "5.3", "5.4"] 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@master 16 | 17 | - uses: leafo/gh-actions-lua@master 18 | with: 19 | luaVersion: ${{ matrix.luaVersion }} 20 | 21 | - uses: leafo/gh-actions-luarocks@master 22 | 23 | - name: build 24 | run: | 25 | luarocks install busted 26 | luarocks install moonscript 27 | luarocks make 28 | 29 | - name: test 30 | run: | 31 | busted -o utfTerminal 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leafo/tableshape/88755361cfeab725f193b98fbee3930cb5fb959c/.gitignore -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: local lint build 2 | 3 | local: build 4 | luarocks make --local --lua-version=5.1 tableshape-dev-1.rockspec 5 | 6 | build: 7 | moonc tableshape 8 | 9 | lint: 10 | moonc -l tableshape 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # tableshape 3 | 4 | ![test](https://github.com/leafo/tableshape/workflows/test/badge.svg) 5 | 6 | A Lua library for verifying the shape (schema, structure, etc.) of a table, and 7 | transforming it if necessary. The type checking syntax is inspired by the 8 | [PropTypes module of 9 | React](https://facebook.github.io/react/docs/reusable-components.html#prop-validation). 10 | Complex types & value transformations can be expressed using an operator 11 | overloading syntax similar to [LPeg](http://www.inf.puc-rio.br/~roberto/lpeg/). 12 | 13 | ### Install 14 | 15 | ```bash 16 | $ luarocks install tableshape 17 | ``` 18 | 19 | ### Quick usage 20 | 21 | ```lua 22 | local types = require("tableshape").types 23 | 24 | -- define the shape of our player object 25 | local player_shape = types.shape{ 26 | class = types.one_of{"player", "enemy"}, 27 | name = types.string, 28 | position = types.shape{ 29 | x = types.number, 30 | y = types.number, 31 | }, 32 | inventory = types.array_of(types.shape{ 33 | name = types.string, 34 | id = types.integer 35 | }):is_optional() 36 | } 37 | 38 | 39 | 40 | -- create a valid object to test the shape with 41 | local player = { 42 | class = "player", 43 | name = "Lee", 44 | position = { 45 | x = 2.8, 46 | y = 8.5 47 | }, 48 | } 49 | 50 | -- verify that it matches the shape 51 | assert(player_shape(player)) 52 | 53 | -- let's break the shape to see the error message: 54 | player.position.x = "heck" 55 | assert(player_shape(player)) 56 | 57 | -- error: field `position`: field `x`: got type `string`, expected `number` 58 | ``` 59 | 60 | #### Transforming 61 | 62 | A malformed value can be repaired to the expected shape by using the 63 | transformation operator and method. The input value is cloned and modified 64 | before being returned. 65 | 66 | 67 | ```lua 68 | local types = require("tableshape").types 69 | 70 | -- a type checker that will coerce a value into a number from a string or return 0 71 | local number = types.number + types.string / tonumber + types.any / 0 72 | 73 | number:transform(5) --> 5 74 | number:transform("500") --> 500 75 | number:transform("hi") --> 0 76 | number:transform({}) --> 0 77 | ``` 78 | 79 | Because type checkers are composable objects, we can build more complex types 80 | out of existing types we've written: 81 | 82 | ```lua 83 | 84 | -- here we reference our transforming number type from above 85 | local coordinate = types.shape { 86 | x = number, 87 | y = number 88 | } 89 | 90 | -- a compound type checker that can fill in missing values 91 | local player_shape = types.shape({ 92 | name = types.string + types.any / "unknown", 93 | position = coordinate 94 | }) 95 | 96 | local bad_player = { 97 | position = { 98 | x = "234", 99 | y = false 100 | } 101 | } 102 | 103 | local fixed_player = player_shape:transform(bad_player) 104 | 105 | -- fixed_player --> { 106 | -- name = "unknown", 107 | -- position = { 108 | -- x = 234, 109 | -- y = 0 110 | -- } 111 | -- } 112 | ``` 113 | 114 | ## Tutorial 115 | 116 | To load the library `require` it. The most important part of the library is the 117 | `types` table, which will give you acess to all the type checkers 118 | 119 | ```lua 120 | local types = require("tableshape").types 121 | ``` 122 | 123 | You can use the types table to check the types of simple values, not just 124 | tables. Calling the type checker like a function will test a value to see if it 125 | matches the shape or type. It returns `true` on a match, or `nil` and the error 126 | message if it fails. (This is done with the `__call` metamethod, you can also 127 | use the `check_value` method directly) 128 | 129 | ```lua 130 | types.string("hello!") --> true 131 | types.string(777) --> nil, expected type "string", got "number" 132 | ``` 133 | 134 | You can see the full list of the available types below in the reference. 135 | 136 | The real power of `tableshape` comes from the ability to describe complex types 137 | by nesting the type checkers. 138 | 139 | Here we test for an array of numbers by using `array_of`: 140 | 141 | ```lua 142 | local numbers_shape = types.array_of(types.number) 143 | 144 | assert(numbers_shape({1,2,3})) 145 | 146 | -- error: item 2 in array does not match: got type `string`, expected `number` 147 | assert(numbers_shape({1,"oops",3})) 148 | ``` 149 | 150 | > **Note:** The type checking is strict, a string that looks like a number, 151 | > `"123"`, is not a number and will trigger an error! 152 | 153 | The structure of a generic table can be tested with `types.shape`. It takes a 154 | mapping table where the key is the field to check, and the value is the type 155 | checker: 156 | 157 | ```lua 158 | local object_shape = types.shape{ 159 | id = types.number, 160 | name = types.string:is_optional(), 161 | } 162 | 163 | -- success 164 | assert(object_shape({ 165 | id = 1234, 166 | name = "hello world" 167 | })) 168 | 169 | -- sucess, optional field is not there 170 | assert(object_shape({ 171 | id = 1235, 172 | })) 173 | 174 | 175 | -- error: field `id`: got type `nil`, expected `number` 176 | assert(object_shape({ 177 | name = 424, 178 | })) 179 | ``` 180 | 181 | The `is_optional` method can be called on any type checker to return a new type 182 | checker that can also accept `nil` as a value. (It is equivalent to `t + types['nil']`) 183 | 184 | If multiple fields fail the type check in a shape, the error message will 185 | contain all the failing fields 186 | 187 | You can also use a literal value to match it directly: (This is equivalent to using `types.literal(v)`) 188 | 189 | ```lua 190 | local object_shape = types.shape{ 191 | name = "Cowcat" 192 | } 193 | 194 | -- error: field `name` expected `Cowcat` 195 | assert(object_shape({ 196 | name = "Cowdog" 197 | })) 198 | ``` 199 | 200 | The `one_of` type constructor lets you specify a list of types, and will 201 | succeed if one of them matches. (It works the same as the `+` operator) 202 | 203 | 204 | ```lua 205 | local func_or_bool = types.one_of { types.func, types.boolean } 206 | 207 | assert(func_or_bool(function() end)) 208 | 209 | -- error: expected type "function", or type "boolean" 210 | assert(func_or_bool(2345)) 211 | ``` 212 | 213 | It can also be used with literal values as well: 214 | 215 | ```lua 216 | local limbs = types.one_of{"foot", "arm"} 217 | 218 | assert(limbs("foot")) -- success 219 | assert(limbs("arm")) -- success 220 | 221 | -- error: expected "foot", or "arm" 222 | assert(limbs("baseball")) 223 | ``` 224 | 225 | The `pattern` type can be used to test a string with a Lua pattern 226 | 227 | ```lua 228 | local no_spaces = types.pattern "^[^%s]*$" 229 | 230 | assert(no_spaces("hello!")) 231 | 232 | -- error: doesn't match pattern `^[^%s]*$` 233 | assert(no_spaces("oh no!")) 234 | ``` 235 | 236 | These examples only demonstrate some of the type checkers provided. You can 237 | see all the other type checkers in the reference below. 238 | 239 | ### Type operators 240 | 241 | Type checker objects have the operators `*`, `+`, and `/` overloaded to provide 242 | a quick way to make composite types. 243 | 244 | * `*` — The **all of (and)** operator, both operands must match. 245 | * `+` — The **first of (or)** operator, the operands are checked against the value from left to right 246 | * `/` — The **transform** operator, when using the `transform` method, the value will be converted by what's to the right of the operator 247 | * `%` — The **transform with state** operator, same as transform, but state is passed as second argument 248 | 249 | #### The 'all of' operator 250 | 251 | The **all of** operator checks if a value matches multiple types. Types are 252 | checked from left to right, and type checking will abort on the first failed 253 | check. It works the same as `types.all_of`. 254 | 255 | ```lua 256 | local s = types.pattern("^hello") * types.pattern("world$") 257 | 258 | s("hello 777 world") --> true 259 | s("good work") --> nil, "doesn't match pattern `^hello`" 260 | s("hello, umm worldz") --> nil, "doesn't match pattern `world$`" 261 | ``` 262 | 263 | #### The 'first of' operator 264 | 265 | The **first of** operator checks if a value matches one of many types. Types 266 | are checked from left to right, and type checking will succeed on the first 267 | matched type. It works the same as `types.one_of`. 268 | 269 | Once a type has been matched, no additional types are checked. If you use a 270 | greedy type first, like `types.any`, then it will not check any additional 271 | ones. This is important to realize if your subsequent types have any side 272 | effects like transformations or tags. 273 | 274 | 275 | ```lua 276 | local s = types.number + types.string 277 | 278 | s(44) --> true 279 | s("hello world") --> true 280 | s(true) --> nil, "no matching option (got type `boolean`, expected `number`; got type `boolean`, expected `string`)" 281 | ``` 282 | 283 | #### The 'transform' operator 284 | 285 | In type matching mode, the transform operator has no effect. When using the 286 | `transform` method, however, the value will be modified by a callback or 287 | changed to a fixed value. 288 | 289 | The following syntax is used: `type / transform_callback --> transformable_type` 290 | 291 | ```lua 292 | local t = types.string + types.any / "unknown" 293 | ``` 294 | 295 | The proceeding type can be read as: "Match any string, or for any other type, 296 | transform it into the string 'unknown'". 297 | 298 | ```lua 299 | t:transform("hello") --> "hello" 300 | t:transform(5) --> "unknown" 301 | ``` 302 | 303 | Because this type checker uses `types.any`, it will pass for whatever value is 304 | handed to it. A transforming type can fail also fail, here's an example: 305 | 306 | ```lua 307 | local n = types.number + types.string / tonumber 308 | 309 | n:transform("5") --> 5 310 | n:transform({}) --> nil, "no matching option (got type `table`, expected `number`; got type `table`, expected `string`)" 311 | ``` 312 | 313 | The transform callback can either be a function, or a literal value. If a 314 | function is used, then the function is called with the current value being 315 | transformed, and the result of the transformation should be returned. If a 316 | literal value is used, then the transformation always turns the value into the 317 | specified value. 318 | 319 | A transform function is not a predicate, and can't directly cause the type 320 | checking to fail. Returning `nil` is valid and will change the value to `nil`. 321 | If you wish to fail based on a function you can use the `custom` type or chain 322 | another type checker after the transformation: 323 | 324 | 325 | ```lua 326 | -- this will fail unless `tonumber` returns a number 327 | local t = (types.string / tonumber) * types.number 328 | t:transform("nothing") --> nil, "got type `nil`, expected `number`" 329 | ``` 330 | 331 | A common pattern for repairing objects involves testing for the types you know 332 | how to fix followed by ` + types.any`, followed by a type check of the final 333 | type you want: 334 | 335 | Here we attempt to repair a value to the expected format for an x,y coordinate: 336 | 337 | 338 | ```lua 339 | local types = require("tableshape").types 340 | 341 | local str_to_coord = types.string / function(str) 342 | local x,y = str:match("(%d+)[^%d]+(%d+)") 343 | if not x then return end 344 | return { 345 | x = tonumber(x), 346 | y = tonumber(y) 347 | } 348 | end 349 | 350 | local array_to_coord = types.shape{types.number, types.number} / function(a) 351 | return { 352 | x = a[1], 353 | y = a[2] 354 | } 355 | end 356 | 357 | local cord = (str_to_coord + array_to_coord + types.any) * types.shape { 358 | x = types.number, 359 | y = types.number 360 | } 361 | 362 | cord:transform("100,200") --> { x = 100, y = 200} 363 | cord:transform({5, 23}) --> { x = 5, y = 23} 364 | cord:transform({ x = 9, y = 10}) --> { x = 9, y = 10} 365 | ``` 366 | 367 | ### Tags 368 | 369 | Tags can be used to extract values from a type as it's checked. A tag is only 370 | saved if the type it wraps matches. If a tag type wraps type checker that 371 | transforms a value, then the tag will store the result of the transformation 372 | 373 | 374 | ```lua 375 | local t = types.shape { 376 | a = types.number:tag("x"), 377 | b = types.number:tag("y"), 378 | } + types.shape { 379 | types.number:tag("x"), 380 | types.number:tag("y"), 381 | } 382 | 383 | t({1,2}) --> { x = 1, y = 2} 384 | t({a = 3, b = 9}) --> { x = 3, y = 9} 385 | ``` 386 | 387 | The values captured by tags are stored in the *state* object, a table that is 388 | passed throughout the entire type check. When invoking a type check, on success 389 | the return value will be the state object if any state is used, via tags or any 390 | of the state APIs listed below. If no state is used, `true` is returned on a 391 | successful check. 392 | 393 | If a tag name ends in `"[]"` (eg. `"items[]"`), then repeated use of the tag 394 | name will cause each value to accumulate into an array. Otherwise, re-use of a 395 | tag name will cause the value to be overwritten at that name. 396 | 397 | ### Scopes 398 | 399 | You can use scopes to nest state objects (which includes the result of tags). A 400 | scope can be created with `types.scope`. A scope works by pushing a new state 401 | on the state stack. After the scope is completed, it is assigned to the 402 | previous scope at the specified tag name. 403 | 404 | ```lua 405 | local obj = types.shape { 406 | id = types.string:tag("name"), 407 | age = types.number 408 | } 409 | 410 | local many = types.array_of(types.scope(obj, { tag = "results[]"})) 411 | 412 | many({ 413 | { id = "leaf", age = 2000 }, 414 | { id = "amos", age = 15 } 415 | }) --> { results = {name = "leaf"}, {name = "amos"}} 416 | ``` 417 | 418 | > Note: In this example, we use the special `[]` syntax in the tag name to accumulate 419 | > all values that are tagged into an array. If the `[]` was left out, then each 420 | > tagged value would overwrite the previous. 421 | 422 | If the tag of the `types.scope` is left out, then an anonymous scope is 423 | created. An anonymous scope is thrown away after the scope is exited. This 424 | style is useful if you use state for a local transformation, and don't need 425 | those values to affect the enclosing state object. 426 | 427 | ### Transforming 428 | 429 | The `transform` method on a type object is a special way to invoke a type check 430 | that allows the value to be changed into something else during the type 431 | checking process. This can be usful for repairing or normalizing input into an 432 | expected shape. 433 | 434 | The simplest way to tranform a value is using the transform operator, `/`: 435 | 436 | For example, we can type checker for URLs that will either accept a valid url, 437 | or convert any other string into a valid URL: 438 | 439 | ```lua 440 | local url_shape = types.pattern("^https?://") + types.string / function(val) 441 | return "http://" .. val 442 | end 443 | ``` 444 | 445 | ```lua 446 | url_shape:transform("https://itch.io") --> https://itch.io 447 | url_shape:transform("leafo.net") --> http://leafo.net 448 | url_shape:transform({}) --> nil, "no matching option (expected string for value; got type `table`)" 449 | ``` 450 | 451 | We can compose transformable type checkers. Now that we know how to fix a URL, 452 | we can fix an array of URLs: 453 | 454 | ```lua 455 | local urls_array = types.array_of(url_shape + types.any / nil) 456 | 457 | local fixed_urls = urls_array:transform({ 458 | "https://itch.io", 459 | "leafo.net", 460 | {} 461 | "www.streak.club", 462 | }) 463 | 464 | -- will return: 465 | -- { 466 | -- "https://itch.io", 467 | -- "http://leafo.net", 468 | -- "http://www.streak.club" 469 | -- } 470 | ``` 471 | 472 | The `transform` method of the `array_of` type will transform each value of the 473 | array. A special property of the `array_of` transform is to exclude any values 474 | that get turned into `nil` in the final output. You can use this to filter out 475 | any bad data without having holes in your array. (You can override this with 476 | the `keep_nils` option. 477 | 478 | Note how we add the `types.any / nil` alternative after the URL shape. This 479 | will ensure any unrecognized values are turned to `nil` so that they can be 480 | filtered out from the `array_of` shape. If this was not included, then the URL 481 | shape will fail on invalid values and the the entire transformation would be 482 | aborted. 483 | 484 | ### Transformation and mutable objects 485 | 486 | Special care must be made when writing a transformation function when working 487 | with mutable objects like tables. You should never modify the object, instead 488 | make a clone of it, make the changes, then return the new object. 489 | 490 | Because types can be deeply nested, it's possible that transformation may be 491 | called on a value, but the type check later fails. If you mutated the input 492 | value then there's no way to undo that change, and you've created a side effect 493 | that may break your program. 494 | 495 | **Never do this:** 496 | 497 | ```lua 498 | local types = require("tableshape").types 499 | 500 | -- WARNING: READ CAREFULLY 501 | local add_id = types.table / function(t) 502 | -- NEVER DO THIS 503 | t.id = 100 504 | -- I repeat, don't do what's in the line above 505 | return t 506 | end 507 | 508 | -- This is why, imagine we create a new compund type: 509 | 510 | local array_of_add_id = types.array_of(add_id) 511 | 512 | -- And now we pass in the following malformed object: 513 | 514 | local items = { 515 | { entry = 1}, 516 | "entry2", 517 | { entry = 3}, 518 | } 519 | 520 | 521 | -- And attempt to verify it by transforming it: 522 | 523 | local result,err = array_of_add_id:transform(items) 524 | 525 | -- This will fail because there's an value in items that will fail validation for 526 | -- add_id. Since types are processed incrementally, the first entry would have 527 | -- been permanently changed by the transformation. Even though the check failed, 528 | -- the data is partially modified and may result in a hard-to-catch bug. 529 | 530 | print items[1] --> = { id = 100, entry = 1} 531 | print items[3] --> = { entry = 3} 532 | ``` 533 | 534 | Luckily, tableshape provides a helper type that is designed to clone objects, 535 | `types.clone`. Here's the correct way to write the transformation: 536 | 537 | ```lua 538 | local types = require("tableshape").types 539 | 540 | local add_id = types.table / function(t) 541 | local new_t = assert(types.clone:transform(t)) 542 | new_t.id = 100 543 | return new_t 544 | end 545 | ``` 546 | 547 | > **Advanced users only:** Since `types.clone` is a type itself, you can chain 548 | > it before any *dirty* functions you may have to ensure that mutations don't 549 | > cause side effects to persist during type validation: `types.table * types.clone / my_messy_function` 550 | 551 | The built in composite types that operate on objects will automatically clone 552 | an object if any of the nested types have transforms that return new values. 553 | This includes composite type constructors like `types.shape`, `types.array_of`, 554 | `types.map_of`, etc. You only need to be careful about mutations when using 555 | custom transformation functions. 556 | 557 | ## Reference 558 | 559 | ```lua 560 | local types = require("tableshape").types 561 | ``` 562 | 563 | ### Type constructors 564 | 565 | Type constructors build a type checker configured by the parameters you pass. 566 | Here are all the available ones, full documentation is below. 567 | 568 | * `types.shape` - checks the shape of a table 569 | * `types.partial` - shorthand for an *open* `types.shape` 570 | * `types.one_of` - checks if value matches one of the types provided 571 | * `types.pattern` - checks if Lua pattern matches value 572 | * `types.array_of` - checks if value is array containing a type 573 | * `types.array_contains` - checks if value is an array that contains a type (short circuits by default) 574 | * `types.map_of` - checks if value is table that matches key and value types 575 | * `types.literal` - checks if value matches the provided value with `==` 576 | * `types.custom` - lets you provide a function to check the type 577 | * `types.equivalent` - checks if values deeply compare to one another 578 | * `types.range` - checks if value is between two other values 579 | * `types.proxy` - dynamically load a type checker 580 | 581 | #### `types.shape(table_dec, options={})` 582 | 583 | Returns a type checker tests for a table where every key in `table_dec` has a 584 | type matching the associated value. The associated value can also be a literal 585 | value. 586 | 587 | ```lua 588 | local t = types.shape{ 589 | category = "object", -- matches the literal value `"object"` 590 | id = types.number, 591 | name = types.string 592 | } 593 | ``` 594 | 595 | The following options are supported: 596 | 597 | * `open` — The shape will accept any additional fields without failing 598 | * `extra_fields` — A type checker for use with extra keys. For each extra field in the table, the value `{key = value}` is passed to the `extra_fields` type checker. During transformation, the table can be transformed to change either the key or value. Transformers that return `nil` will clear the field. See below for examples. The extra keys shape can also use tags. 599 | 600 | Examples with `extra_fields`: 601 | 602 | Basic type test for extra fields: 603 | 604 | ```lua 605 | local t = types.shape({ 606 | name = types.string 607 | }, { 608 | extra_fields = types.map_of(types.string, types.number) 609 | }) 610 | 611 | t({ 612 | name = "lee", 613 | height = "10cm", 614 | friendly = false, 615 | }) --> nil, "field `height` value in table does not match: got type `string`, expected `number`" 616 | 617 | ``` 618 | 619 | A transform can be used on `extra_fields` as well. In this example all extra fields are removed: 620 | 621 | ```lua 622 | local t = types.shape({ 623 | name = types.string 624 | }, { 625 | extra_fields = types.any / nil 626 | }) 627 | 628 | t:transform({ 629 | name = "amos", 630 | color = "blue", 631 | 1,2,3 632 | }) --> { name = "amos"} 633 | ``` 634 | 635 | Modifying the extra keys using a transform: 636 | 637 | ```lua 638 | local types = require("tableshape").types 639 | 640 | local t = types.shape({ 641 | name = types.string 642 | }, { 643 | extra_fields = types.map_of( 644 | -- prefix all extra keys with _ 645 | types.string / function(str) return "_" .. str end, 646 | 647 | -- leave values as is 648 | types.any 649 | ) 650 | }) 651 | 652 | t:transform({ 653 | name = "amos", 654 | color = "blue" 655 | }) --> { name = "amos", _color = "blue" } 656 | ``` 657 | 658 | #### `types.partial(table_dec, options={})` 659 | 660 | The same as `types.shape` but sets `open = true` by default. This alias 661 | function was added because open shape objects are common when using tableshape. 662 | 663 | ```lua 664 | local types = require("tableshape").types 665 | 666 | local t = types.partial { 667 | name = types.string\tag "player_name" 668 | } 669 | 670 | t({ 671 | t: "character" 672 | name: "Good Friend" 673 | }) --> { player_name: "Good Friend" } 674 | ``` 675 | 676 | #### `types.array_of(item_type, options={})` 677 | 678 | Returns a type checker that tests if the value is an array where each item 679 | matches the provided type. 680 | 681 | ```lua 682 | local t = types.array_of(types.shape{ 683 | id = types.number 684 | }) 685 | ``` 686 | 687 | The following options are supported: 688 | 689 | * `keep_nils` — By default, if a value is transformed into a nil then it won't be kept in the output array. If you need to keep these holes then set this option to `true` 690 | * `length` — Provide a type checker to be used on the length of the array. The length is calculated with the `#` operator. It's typical to use `types.range` to test for a range 691 | 692 | 693 | #### `types.array_contains(item_type, options={})` 694 | 695 | Returns a type checker that tests if `item_type` exists in the array. By 696 | default, `short_circuit` is enabled. It will search until it finds the first 697 | instance of `item_type` in the array then stop with a success. This impacts 698 | transforming types, as only the first match will be transformed by default. To 699 | process every entry in the array, set `short_circuit = false` in the options. 700 | 701 | 702 | ```lua 703 | local t = types.array_contains(types.number) 704 | 705 | t({"one", "two", 3, "four"}) --> true 706 | t({"hello", true}) --> fails 707 | ``` 708 | 709 | The following options are supported: 710 | 711 | * `short_circuit` — (default `true`) Will stop scanning over the array if a single match is found 712 | * `keep_nils` — By default, if a value is transformed into a nil then it won't be kept in the output array. If you need to keep these holes then set this option to `true` 713 | 714 | 715 | #### `types.map_of(key_type, value_type)` 716 | 717 | Returns a type checker that tests for a table where every key and value matches 718 | the respective type checkers provided as arguments. 719 | 720 | ```lua 721 | local t = types.map_of(types.string, types.any) 722 | ``` 723 | 724 | When transforming a `map_of`, you can remove fields from the table by 725 | transforming either the key or value to `nil`. 726 | 727 | ```lua 728 | -- this will remove all fields with non-string keys 729 | local t = types.map_of(types.string + types.any / nil, types.any) 730 | 731 | t:transform({ 732 | 1,2,3, 733 | hello = "world" 734 | }) --> { hello = "world" } 735 | ``` 736 | 737 | #### `types.one_of({type1, type2, ...})` 738 | 739 | Returns a type checker that tests if the value matches one of the provided 740 | types. A literal value can also be passed as a type. 741 | 742 | ```lua 743 | local t = types.one_of{"none", types.number} 744 | ``` 745 | 746 | #### `types.pattern(lua_pattern)` 747 | 748 | Returns a type checker that tests if a string matches the provided Lua pattern 749 | 750 | ```lua 751 | local t = types.pattern("^#[a-fA-F%d]+$") 752 | ``` 753 | 754 | #### `types.literal(value)` 755 | 756 | Returns a type checker that checks if value is equal to the one provided. When 757 | using shape this is normally unnecessary since non-type checker values will be 758 | checked literally with `==`. This lets you attach a repair function to a 759 | literal check. 760 | 761 | ```lua 762 | local t = types.literal "hello world" 763 | assert(t("hello world") == true) 764 | assert(t("jello world") == false) 765 | ``` 766 | 767 | #### `types.custom(fn)` 768 | 769 | Returns a type checker that calls the function provided to verify the value. 770 | The function will receive the value being tested as the first argument, and the 771 | type checker as the second. 772 | 773 | The function should return true if the value passes, or `nil` and an error 774 | message if it fails. 775 | 776 | ```lua 777 | local is_even = types.custom(function(val) 778 | if type(val) == "number" then 779 | if val % 2 == 0 then 780 | return true 781 | else 782 | return nil, "number is not even" 783 | end 784 | else 785 | return nil, "expected number" 786 | end 787 | end) 788 | ``` 789 | 790 | #### types.equivalent(val) 791 | 792 | Returns a type checker that will do a deep compare between val and the input. 793 | 794 | ```lua 795 | local t = types.equivalent { 796 | color = {255,100,128}, 797 | name = "leaf" 798 | } 799 | 800 | -- although we're testing a different instance of the table, the structure is 801 | -- the same so it passes 802 | t { 803 | name = "leaf" 804 | color = {255,100,128}, 805 | } --> true 806 | 807 | ``` 808 | 809 | #### types.range(left, right) 810 | 811 | Creates a type checker that will check if a value is beween `left` and `right` 812 | inclusive. The type of the value is checked before doing the comparison: 813 | passing a string to a numeric type checker will fail up front. 814 | 815 | ```lua 816 | local nums = types.range 1, 20 817 | local letters = types.range "a", "f" 818 | 819 | nums(4) --> true 820 | letters("c") --> true 821 | letters("n") --> true 822 | ``` 823 | 824 | This checker works well with the length checks for strings and arrays. 825 | 826 | #### types.proxy(fn) 827 | 828 | The proxy type checker will execute the provided function, `fn`, when called 829 | and use the return value as the type checker. The `fn` function must return a 830 | valid tableshape type checker object. 831 | 832 | This can be used to have types that circularly depend on one another, or handle 833 | recursive types. `fn` is called every time the proxy checks a value, if you 834 | want to optimize for performance then you are responsible for caching type 835 | checker that is returned. 836 | 837 | 838 | An example recursive type checker: 839 | 840 | ```lua 841 | local entity_type = types.shape { 842 | name = types.string, 843 | child = types['nil'] + types.proxy(function() return entity_type end) 844 | } 845 | ``` 846 | 847 | A proxy is needed above because the value of `entity_type` is `nil` while the 848 | type checker is being constructed. By using the proxy, we can create a closure 849 | to the variable that will eventually hold the `entity_type` checker. 850 | 851 | ### Built in types 852 | 853 | Built in types can be used directly without being constructed. 854 | 855 | * `types.string` - checks for `type(val) == "string"` 856 | * `types.number` - checks for `type(val) == "number"` 857 | * `types['function']` - checks for `type(val) == "function"` 858 | * `types.func` - alias for `types['function']` 859 | * `types.boolean` - checks for `type(val) == "boolean"` 860 | * `types.userdata` - checks for `type(val) == "userdata"` 861 | * `types.table` - checks for `type(val) == "table"` 862 | * `types['nil']` - checks for `type(val) == "nil"` 863 | * `types.null` - alias for `types['nil']` 864 | * `types.array` - checks for table of numerically increasing indexes 865 | * `types.integer` - checks for a number with no decimal component 866 | * `types.clone` - creates a shallow copy of the input, fails if value is not cloneable (eg. userdata, function) 867 | 868 | Additionally there's the special *any* type: 869 | 870 | * `types.any` - succeeds no matter value is passed, including `nil` 871 | 872 | ### Type methods 873 | 874 | Every type checker has the follow methods: 875 | 876 | #### `type(value)` or `type:check_value(value)` 877 | 878 | Calling `check_value` is equivalent to calling the type checker object like a 879 | function. The `__call` metamethod is provided on all type checker objects to 880 | allow you easily test a value by treating them like a function. 881 | 882 | Tests `value` against the type checker. Returns `true` (or the current state 883 | object) if the value passes the check. Returns `nil` and an error message as a 884 | string if there is a mismatch. The error message will identify where the 885 | mismatch happened as best it can. 886 | 887 | `check_value` will abort on the first error found, and only that error message is returned. 888 | 889 | > Note: Under the hood, checking a value will always execute the full 890 | > transformation, but the resulting object is thrown away, and only the state 891 | > is returned. Keep this in mind because there is no performance benefit to 892 | > calling `check_value` over `transform` 893 | 894 | #### `type:transform(value, initial_state=nil)` 895 | 896 | Will apply transformation to the `value` with the provided type. If the type 897 | does not include any transformations then the same object will be returned 898 | assuming it matches the type check. If transformations take place then a new 899 | object will be returned with all other fields copied over. 900 | 901 | > You can use the *transform operator* (`/`) to specify how values are transformed. 902 | 903 | A second argument can optionally be provided for the initial state. This should 904 | be a Lua table. 905 | 906 | If no state is provided, an empty Lua table will automatically will 907 | automatically be created if any of the type transformations make changes to the 908 | state. 909 | 910 | > The state object is used to store the result of any tagged types. The state 911 | > object can also be used to store data across the entire type checker for more 912 | > advanced functionality when using the custom state operators and types. 913 | 914 | ```lua 915 | local t = types.number + types.string / tonumber 916 | 917 | t:transform(10) --> 10 918 | t:transform("15") --> 15 919 | ``` 920 | 921 | On success, this method will return the resulting value and the resulting 922 | state. If no state is used, then no state will be returned. On failure, the 923 | method will return `nil` and a string error message. 924 | 925 | #### `type:repair(value)` 926 | 927 | > This method is deprecated, use the `type:transform` instead 928 | 929 | An alias for `type:transform(value)` 930 | 931 | #### `type:is_optional()` 932 | 933 | Returns a new type checker that matches the same type, or `nil`. This is 934 | effectively the same as using the expression: 935 | 936 | 937 | ```lua 938 | local optional_my_type = types["nil"] + my_type 939 | ```` 940 | 941 | Internally, though, `is_optional` creates new *OptionalType* node in the type 942 | hierarchy to make printing summaries and error messages more clear. 943 | 944 | #### `type:describe(description)` 945 | 946 | Returns a wrapped type checker that will use `description` to describe the type 947 | when an error message is returned. `description` can either be a string 948 | literal, or a function. When using a function, it must return the description 949 | of the type as a string. 950 | 951 | 952 | #### `type:tag(name_or_fn)` 953 | 954 | Causes the type checker to save matched values into the state object. If 955 | `name_or_fn` is a string, then the tested value is stored into the state with 956 | key `name_or_fn`. 957 | 958 | If `name_or_fn` is a function, then you provide a callback to control how the 959 | state is updated. The function takes as arguments the state object and the 960 | value that matched: 961 | 962 | ```lua 963 | -- an example tag function that accumulates an array 964 | types.number:tag(function(state, value) 965 | -- nested objects should be treated as read only, so modifications are done to a copy 966 | if state.numbers then 967 | state.numbers = { unpack state.numbers } 968 | else 969 | state.numbers = { } 970 | end 971 | 972 | table.insert(state.numbers, value) 973 | end) 974 | ``` 975 | 976 | > This is illustrative example. If you need to accumulate a list of values then 977 | > use the `[]` syntax for tag names. 978 | 979 | You can mutate the `state` argument with any changes. The return value of this 980 | function is ignored. 981 | 982 | Note that state objects are generally immutable. Whenever a state modifying 983 | operation takes place, the modification is done to a copy of the state object. 984 | This is to prevent changes to the state object from being kept around when a 985 | failing type is tested. 986 | 987 | A `function` tag gets a copy of the current state as its first argument ready 988 | for editing. The copy is a shallow copy. If you have any nested objects then 989 | it's necessary to clone them before making any modifications, as seen in the 990 | example above. 991 | 992 | #### `type:scope(name)` 993 | 994 | Pushes a new state object on top of the stack. After the scoped type matches, 995 | the state it created is assigned to the previous scope with the key `name`. 996 | 997 | It is equivalent to using the `types.scope` constructor like so: 998 | 999 | ```lua 1000 | -- The following two lines are equivalent 1001 | type:scope(name) --> scoped type 1002 | types.scope(type, { tag = name }) --> scoped type 1003 | ``` 1004 | 1005 | #### `shape_type:is_open()` 1006 | 1007 | > This method is deprecated, use the `open = true` constructor option on shapes instead 1008 | 1009 | This method is only available on a type checker generated by `types.shape`. 1010 | 1011 | Returns a new shape type checker that won't fail if there are extra fields not 1012 | specified. 1013 | 1014 | #### `type:on_repair(func)` 1015 | 1016 | An alias for the transform pattern: 1017 | 1018 | ```lua 1019 | type + types.any / func * type 1020 | ``` 1021 | 1022 | In English, this will let a value that matches `type` pass through, otherwise 1023 | for anything else call `func(value)` and let the return value pass through if 1024 | it matches `type`, otherwise fail. 1025 | 1026 | ## Changelog 1027 | 1028 | **Jan 25 2021** - 2.2.0 1029 | 1030 | * Fixed bug where state could be overidden when tagging in `array_contains` 1031 | * Expose (and add docs for) for `types.proxy` 1032 | * Add experimental `Annotated` type 1033 | * Update test suite to GitHub Actions 1034 | 1035 | **Oct 19 2019** - 2.1.0 1036 | 1037 | * Add `types.partial` alias for open shape 1038 | * Add `types.array_contains` 1039 | * Add `not` type, and unary minus operator 1040 | * Add MoonScript module: `class_type`, `instance_type`, `instance_type` checkers 1041 | 1042 | **Aug 09 2018** - 2.0.0 1043 | 1044 | * Add overloaded operators to compose types 1045 | * Add transformation interface 1046 | * Add support for tagging 1047 | * Add `state` parameter that's passed through type checks 1048 | * Replace repair interface with simple transform 1049 | * Error messages will never re-output the value 1050 | * Type objects have a new interface to describe their shape 1051 | 1052 | **Feb 10 2016** - 1.2.1 1053 | 1054 | * Fix bug where literal fields with no dot operator could not be checked 1055 | * Better failure message when field doesn't match literal value 1056 | * Add `types.nil` 1057 | 1058 | **Feb 04 2016** - 1.2.0 1059 | 1060 | * Add the repair interface 1061 | 1062 | **Jan 25 2016** - 1.1.0 1063 | 1064 | * Add `types.map_of` 1065 | * Add `types.any` 1066 | 1067 | **Jan 24 2016** 1068 | 1069 | * Initial release 1070 | 1071 | ## License (MIT) 1072 | 1073 | Copyright (C) 2022 by Leaf Corcoran 1074 | 1075 | Permission is hereby granted, free of charge, to any person obtaining a copy 1076 | of this software and associated documentation files (the "Software"), to deal 1077 | in the Software without restriction, including without limitation the rights 1078 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 1079 | copies of the Software, and to permit persons to whom the Software is 1080 | furnished to do so, subject to the following conditions: 1081 | 1082 | The above copyright notice and this permission notice shall be included in 1083 | all copies or substantial portions of the Software. 1084 | 1085 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1086 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1087 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1088 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1089 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1090 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 1091 | THE SOFTWARE. 1092 | 1093 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name=tableshape 2 | abstract=Validate and transform the structure of a Lua table 3 | author=Leaf Corcoran (leafo) 4 | is_original=yes 5 | license=mit 6 | lib_dir=. 7 | doc_dir=. 8 | repo_link=https://github.com/leafo/tableshape 9 | main_module=tableshape/init.lua 10 | exclude_files=test*.lua, example*.lua, scrap*.lua -------------------------------------------------------------------------------- /spec/moonscript_spec.moon: -------------------------------------------------------------------------------- 1 | 2 | import instance_of, class_type, instance_type, subclass_of from require "tableshape.moonscript" 3 | 4 | describe "tableshape.moonscript", -> 5 | class Other 6 | class Hello 7 | class World extends Hello 8 | class Zone extends World 9 | 10 | describe "instance_type", -> 11 | it "describes", -> 12 | assert.same "class", tostring class_type 13 | 14 | it "tests values", -> 15 | assert instance_type Other! 16 | assert instance_type Zone! 17 | 18 | assert.same { 19 | nil 20 | "expecting table" 21 | }, { instance_type true } 22 | 23 | assert.same { 24 | nil 25 | "expecting table" 26 | }, { instance_type -> } 27 | 28 | assert.same { 29 | nil 30 | "expecting table" 31 | }, { instance_type nil } 32 | 33 | -- random table 34 | assert.same { 35 | nil 36 | "table is not instance (missing metatable)" 37 | }, { instance_type {} } 38 | 39 | -- a class object (is not an instance) 40 | assert.same { 41 | nil 42 | "table is not instance (metatable does not have __class)" 43 | }, { instance_type World } 44 | 45 | describe "class_type", -> 46 | it "describes type checker", -> 47 | assert.same "class", tostring class_type 48 | 49 | it "tests values", -> 50 | assert.same { 51 | nil 52 | "table is not class (missing __base)" 53 | }, { class_type Hello! } 54 | 55 | assert.true, class_type Hello 56 | 57 | assert.same { 58 | nil 59 | "expecting table" 60 | }, { class_type false } 61 | 62 | assert.same { 63 | nil 64 | "table is not class (missing __base)" 65 | }, { class_type {} } 66 | 67 | assert.same { 68 | nil 69 | "table is not class (__base not table)" 70 | }, { class_type { __base: "world" } } 71 | 72 | assert.same { 73 | nil 74 | "table is not class (missing metatable)" 75 | }, { class_type { __base: {}} } 76 | 77 | assert.same { 78 | nil 79 | "table is not class (no constructor)" 80 | }, { class_type setmetatable { __base: {}}, {} } 81 | 82 | describe "instance_of", -> 83 | it "describes type checker", -> 84 | assert.same "instance of Other", tostring instance_of Other 85 | assert.same "instance of World", tostring instance_of "World" 86 | 87 | it "handles invalid types", -> 88 | t = instance_of(Other) 89 | assert.same {nil, "expecting table"}, { t -> } 90 | assert.same {nil, "expecting table"}, { t false } 91 | assert.same {nil, "expecting table"}, { t 22 } 92 | assert.same {nil, "table is not instance (missing metatable)"}, { t {} } 93 | assert.same {nil, "table is not instance (metatable does not have __class)"}, { t setmetatable {}, {} } 94 | 95 | it "checks instance of class by name", -> 96 | -- by zone 97 | assert.true instance_of("Zone") Zone! 98 | 99 | assert.same { 100 | nil, "table is not instance of Zone" 101 | }, { instance_of("Zone") World! } 102 | 103 | assert.same { 104 | nil, "table is not instance of Zone" 105 | }, { instance_of("Zone") Hello! } 106 | 107 | assert.same { 108 | nil, "table is not instance of Zone" 109 | }, { instance_of("Zone") Other! } 110 | 111 | -- by world 112 | assert.true instance_of("World") Zone! 113 | assert.true instance_of("World") World! 114 | 115 | assert.same { 116 | nil, "table is not instance of World" 117 | }, { instance_of("World") Hello! } 118 | 119 | assert.same { 120 | nil, "table is not instance of World" 121 | }, { instance_of("World") Other! } 122 | 123 | -- by hello 124 | assert.true instance_of("Hello") Zone! 125 | assert.true instance_of("Hello") World! 126 | assert.true instance_of("Hello") Hello! 127 | 128 | assert.same { 129 | nil, "table is not instance of Hello" 130 | }, { instance_of("Hello") Other! } 131 | 132 | -- by other 133 | assert.same { 134 | nil, "table is not instance of Other" 135 | }, { instance_of("Other") World! } 136 | 137 | assert.same { 138 | nil, "table is not instance of Other" 139 | }, { instance_of("Other") World! } 140 | 141 | assert.same { 142 | nil, "table is not instance of Other" 143 | }, { instance_of("Other") Hello! } 144 | 145 | assert.true instance_of("Other") Other! 146 | 147 | 148 | it "checks instance of class by object", -> 149 | -- by zone 150 | assert.true instance_of(Zone) Zone! 151 | 152 | -- it should not think a class object is an instance 153 | assert.same { 154 | nil, "table is not instance (metatable does not have __class)" 155 | }, { instance_of(Zone) Zone } 156 | 157 | assert.same { 158 | nil, "table is not instance of Zone" 159 | }, { instance_of(Zone) World! } 160 | 161 | assert.same { 162 | nil, "table is not instance of Zone" 163 | }, { instance_of(Zone) Hello! } 164 | 165 | assert.same { 166 | nil, "table is not instance of Zone" 167 | }, { instance_of(Zone) Other! } 168 | 169 | -- by world 170 | assert.true instance_of(World) Zone! 171 | assert.true instance_of(World) World! 172 | 173 | assert.same { 174 | nil, "table is not instance of World" 175 | }, { instance_of(World) Hello! } 176 | 177 | assert.same { 178 | nil, "table is not instance of World" 179 | }, { instance_of(World) Other! } 180 | 181 | -- by hello 182 | assert.true instance_of(Hello) Zone! 183 | assert.true instance_of(Hello) World! 184 | assert.true instance_of(Hello) Hello! 185 | 186 | assert.same { 187 | nil, "table is not instance of Hello" 188 | }, { instance_of(Hello) Other! } 189 | 190 | -- by other 191 | assert.same { 192 | nil, "table is not instance of Other" 193 | }, { instance_of(Other) World! } 194 | 195 | assert.same { 196 | nil, "table is not instance of Other" 197 | }, { instance_of(Other) World! } 198 | 199 | assert.same { 200 | nil, "table is not instance of Other" 201 | }, { instance_of(Other) Hello! } 202 | 203 | assert.true instance_of(Other) Other! 204 | 205 | describe "subclass_of", -> 206 | it "describes type checker", -> 207 | assert.same "subclass of Other", tostring subclass_of Other 208 | assert.same "subclass of World", tostring subclass_of "World" 209 | 210 | it "handles invalid types", -> 211 | t = subclass_of(Other) 212 | 213 | assert.same {nil, "expecting table"}, { t -> } 214 | assert.same {nil, "expecting table"}, { t false } 215 | assert.same {nil, "expecting table"}, { t 22 } 216 | assert.same {nil, "table is not class (missing __base)"}, { t {} } 217 | assert.same {nil, "table is not class (missing __base)"}, { t setmetatable {}, {} } 218 | 219 | -- fails with instance 220 | assert.same {nil, "table is not class (missing __base)"}, { t Other! } 221 | 222 | it "checks sublcass by name", -> 223 | hello_t = subclass_of "Hello" 224 | world_t = subclass_of "World" 225 | other_t = subclass_of "Other" 226 | 227 | assert.same {true}, { hello_t Zone } 228 | assert.same {true}, { hello_t World } 229 | assert.same {nil, "table is not subclass of Hello"}, { hello_t Hello } 230 | assert.same {nil, "table is not subclass of Hello"}, { hello_t Other } 231 | 232 | assert.same {true}, { world_t Zone } 233 | assert.same {nil, "table is not subclass of World"}, { world_t World } 234 | assert.same {nil, "table is not subclass of World"}, { world_t Hello } 235 | assert.same {nil, "table is not subclass of World"}, { world_t Other } 236 | 237 | assert.same {nil, "table is not subclass of Other"}, { other_t Zone } 238 | assert.same {nil, "table is not subclass of Other"}, { other_t World } 239 | assert.same {nil, "table is not subclass of Other"}, { other_t Hello } 240 | assert.same {nil, "table is not subclass of Other"}, { other_t Other } 241 | 242 | it "checks sublcass by class reference", -> 243 | hello_t = subclass_of Hello 244 | world_t = subclass_of World 245 | other_t = subclass_of Other 246 | 247 | assert.same {true}, { hello_t Zone } 248 | assert.same {true}, { hello_t World } 249 | assert.same {nil, "table is not subclass of Hello"}, { hello_t Hello } 250 | assert.same {nil, "table is not subclass of Hello"}, { hello_t Other } 251 | 252 | assert.same {true}, { world_t Zone } 253 | assert.same {nil, "table is not subclass of World"}, { world_t World } 254 | assert.same {nil, "table is not subclass of World"}, { world_t Hello } 255 | assert.same {nil, "table is not subclass of World"}, { world_t Other } 256 | 257 | assert.same {nil, "table is not subclass of Other"}, { other_t Zone } 258 | assert.same {nil, "table is not subclass of Other"}, { other_t World } 259 | assert.same {nil, "table is not subclass of Other"}, { other_t Hello } 260 | assert.same {nil, "table is not subclass of Other"}, { other_t Other } 261 | 262 | 263 | describe "allow_same", -> 264 | it "checks sublcass by name", -> 265 | hello_t = subclass_of "Hello", allow_same: true 266 | world_t = subclass_of "World", allow_same: true 267 | other_t = subclass_of "Other", allow_same: true 268 | 269 | assert.same {true}, { hello_t Zone } 270 | assert.same {true}, { hello_t World } 271 | assert.same {true}, { hello_t Hello } 272 | assert.same {nil, "table is not subclass of Hello"}, { hello_t Other } 273 | 274 | assert.same {true}, { world_t Zone } 275 | assert.same {true}, { world_t World } 276 | assert.same {nil, "table is not subclass of World"}, { world_t Hello } 277 | assert.same {nil, "table is not subclass of World"}, { world_t Other } 278 | 279 | assert.same {nil, "table is not subclass of Other"}, { other_t Zone } 280 | assert.same {nil, "table is not subclass of Other"}, { other_t World } 281 | assert.same {nil, "table is not subclass of Other"}, { other_t Hello } 282 | assert.same {true}, { other_t Other } 283 | 284 | it "checks sublcass by class reference", -> 285 | hello_t = subclass_of Hello, allow_same: true 286 | world_t = subclass_of World, allow_same: true 287 | other_t = subclass_of Other, allow_same: true 288 | 289 | assert.same {true}, { hello_t Zone } 290 | assert.same {true}, { hello_t World } 291 | assert.same {true}, { hello_t Hello } 292 | assert.same {nil, "table is not subclass of Hello"}, { hello_t Other } 293 | 294 | assert.same {true}, { world_t Zone } 295 | assert.same {true}, { world_t World } 296 | assert.same {nil, "table is not subclass of World"}, { world_t Hello } 297 | assert.same {nil, "table is not subclass of World"}, { world_t Other } 298 | 299 | assert.same {nil, "table is not subclass of Other"}, { other_t Zone } 300 | assert.same {nil, "table is not subclass of Other"}, { other_t World } 301 | assert.same {nil, "table is not subclass of Other"}, { other_t Hello } 302 | assert.same {true}, { other_t Other } 303 | -------------------------------------------------------------------------------- /spec/tableshape_spec.moon: -------------------------------------------------------------------------------- 1 | 2 | {check_shape: check, :types} = require "tableshape" 3 | 4 | describe "tableshape.is_type", -> 5 | it "detects type", -> 6 | import is_type from require "tableshape" 7 | assert.false is_type! 8 | assert.false is_type "hello" 9 | assert.false is_type {} 10 | assert.false is_type -> 11 | 12 | assert.true is_type types.string 13 | assert.true is_type types.shape {} 14 | assert.true is_type types.array_of { types.string } 15 | 16 | -- type constructors are not types 17 | assert.false is_type types.shape 18 | assert.false is_type types.array_of 19 | 20 | describe "tableshape.types", -> 21 | basic_types = { 22 | {"any", valid: 1234} 23 | {"any", valid: "hello"} 24 | {"any", valid: ->} 25 | {"any", valid: true} 26 | {"any", valid: nil} 27 | 28 | {"number", valid: 1234, invalid: "hello"} 29 | {"function", valid: (->), invalid: {}} 30 | {"func", valid: (->), invalid: {}} 31 | {"string", valid: "234", invalid: 777} 32 | {"boolean", valid: true, invalid: 24323} 33 | 34 | {"table", valid: { hi: "world" }, invalid: "{}"} 35 | {"array", valid: { 1,2,3,4 }, invalid: {hi: "yeah"}, check_errors: false} 36 | {"array", valid: {}, check_errors: false} 37 | 38 | {"integer", valid: 1234, invalid: 1.1} 39 | {"integer", valid: 0, invalid: "1243"} 40 | 41 | {"nil", valid: nil} 42 | {"null", valid: nil} 43 | } 44 | 45 | for {type_name, :valid, :invalid, :check_errors} in *basic_types 46 | it "type #{type_name}", -> 47 | t = types[type_name] 48 | 49 | assert.same {true}, {check valid, t} 50 | 51 | if invalid 52 | failure = {check invalid, t} 53 | if check_errors 54 | assert.same {nil, "got type #{type invalid}, expected #{type_name}"}, failure 55 | else 56 | assert.nil failure[1] 57 | 58 | failure = {check nil, t} 59 | if check_errors 60 | assert.same {nil, "got type nil, expected #{type_name}"}, failure 61 | else 62 | assert.nil failure[1] 63 | 64 | -- optional 65 | t = t\is_optional! 66 | assert.same {true}, {check valid, t} 67 | 68 | if invalid 69 | failure = {check invalid, t} 70 | if check_errors 71 | assert.same {nil, "got type #{type invalid}, expected #{type_name}"}, failure 72 | else 73 | assert.nil failure[1] 74 | 75 | assert.same {true}, {check nil, t} 76 | 77 | describe "length", -> 78 | it "checks string length", -> 79 | s = types.string\length 5,20 80 | 81 | assert.same { 82 | nil 83 | "string length not in range from 5 to 20, got 4" 84 | }, {s "heck"} 85 | 86 | assert.same { 87 | true 88 | }, {s "hello!"} 89 | 90 | assert.same { 91 | nil 92 | "string length not in range from 5 to 20, got 120" 93 | }, {s "hello!"\rep 20} 94 | 95 | it "checks string length with base type", -> 96 | s = types.string\length types.literal 5 97 | 98 | assert.same { 99 | true 100 | }, {s "hello"} 101 | 102 | assert.same { 103 | nil 104 | 'string length expected 5, got 6' 105 | }, {s "hello!"} 106 | 107 | describe "one_of", -> 108 | it "check value", -> 109 | ab = types.one_of {"a", "b"} 110 | 111 | assert.same nil, (ab "c") 112 | assert.same true, (ab "a") 113 | assert.same true, (ab "b") 114 | assert.same nil, (ab nil) 115 | 116 | more = types.one_of {true, 123} 117 | assert.same nil, (more "c") 118 | assert.same nil, (more false) 119 | assert.same nil, (more 124) 120 | assert.same true, (more 123) 121 | assert.same true, (more true) 122 | 123 | ab = types.one_of { 124 | types.literal("a") 125 | types.literal("b") 126 | } 127 | 128 | assert.same nil, (ab "c") 129 | assert.same true, (ab "a") 130 | assert.same true, (ab "b") 131 | assert.same nil, (ab nil) 132 | 133 | more = types.one_of {true, 123} 134 | assert.same nil, (more "c") 135 | assert.same nil, (more false) 136 | assert.same nil, (more 124) 137 | assert.same true, (more 123) 138 | assert.same true, (more true) 139 | 140 | it "check value optional", -> 141 | ab = types.one_of {"a", "b"} 142 | ab_opt = ab\is_optional! 143 | 144 | assert.same nil, (ab_opt "c") 145 | assert.same true, (ab_opt "a") 146 | assert.same true, (ab_opt "b") 147 | assert.same true, (ab_opt nil) 148 | 149 | it "check value with sub types", -> 150 | -- with sub type checkers 151 | misc = types.one_of { "g", types.number, types.function } 152 | 153 | assert.same nil, (misc "c") 154 | assert.same true, (misc 2354) 155 | assert.same true, (misc ->) 156 | assert.same true, (misc "g") 157 | assert.same nil, (misc nil) 158 | 159 | it "renders error message", -> 160 | t = types.one_of { 161 | "a", "b" 162 | types.literal("MY THING")\describe "(my thing)" 163 | } 164 | 165 | assert.same { 166 | nil 167 | 'expected "a", "b", or (my thing)' 168 | }, {t "wow"} 169 | 170 | it "shape errors", -> 171 | s = types.one_of { 172 | types.shape { 173 | type: "car" 174 | wheels: types.number 175 | } 176 | types.shape { 177 | type: "house" 178 | windows: types.number 179 | } 180 | } 181 | 182 | assert.true (s { 183 | type: "car" 184 | wheels: 10 185 | }) 186 | 187 | -- for undefined has ordering 188 | errors = setmetatable { 189 | ['expected { "type" = "car", "wheels" = type "number" }, or { "type" = "house", "windows" = type "number" }']: true 190 | ['expected { "wheels" = type "number", "type" = "car" }, or { "windows" = type "number", "type" = "house" }']: true 191 | }, __index: (v) => 192 | error "expected one of \n#{table.concat [k for k in pairs @], "\n"}\n got #{v}" 193 | 194 | assert.true errors[select 2, s { 195 | type: "car" 196 | wheels: "blue" 197 | }] 198 | 199 | it "creates an optimized type checker", -> 200 | t = types.one_of { 201 | "hello", "world", 5 202 | } 203 | 204 | assert.same { 205 | [5]: true 206 | "hello": true 207 | "world": true 208 | }, t.options_hash 209 | 210 | 211 | describe "all_of", -> 212 | it "checks value", -> 213 | t = types.all_of { 214 | types.string 215 | types.custom (k) -> k == "hello", "#{k} is not hello" 216 | } 217 | 218 | assert.same {nil, "zone is not hello"}, {t "zone"} 219 | assert.same {nil, 'expected type "string", got "number"'}, {t 5} 220 | 221 | describe "partial", -> 222 | it "tests partial shape", -> 223 | check = types.partial { color: "red" } 224 | 225 | -- extra data 226 | assert.same {true}, { 227 | check { 228 | color: "red" 229 | weight: 9 230 | age: 3 231 | } 232 | } 233 | 234 | assert.same {true}, { 235 | check { 236 | color: "red" 237 | } 238 | } 239 | 240 | -- extra data 241 | assert.same {nil, 'field "color": expected "red"'}, { 242 | check { 243 | color: "blue" 244 | weight: 9 245 | age: 3 246 | } 247 | } 248 | 249 | describe "shape", -> 250 | it "gets errors for multiple fields", -> 251 | t = types.shape { 252 | "blue" 253 | "red" 254 | } 255 | 256 | assert.same { 257 | nil 258 | 'field 1: expected "blue"' 259 | }, { 260 | t { 261 | "orange", "blue", "purple" 262 | } 263 | } 264 | 265 | assert.same { 266 | nil 267 | "extra fields: 3, 4" 268 | }, { 269 | t { 270 | "blue", "red", "purple", "yello" 271 | } 272 | } 273 | 274 | t = types.shape { 275 | "blue" 276 | "red" 277 | }, check_all: true 278 | 279 | assert.same { 280 | nil 281 | 'field 1: expected "blue"; field 2: expected "red"; extra fields: 3' 282 | }, { 283 | t { 284 | "orange", "blue", "purple" 285 | } 286 | } 287 | 288 | it "checks value", -> 289 | check = types.shape { color: "red" } 290 | assert.same nil, (check color: "blue") 291 | assert.same true, (check color: "red") 292 | 293 | check = types.shape { 294 | color: types.one_of {"red", "blue"} 295 | weight: types.number 296 | } 297 | 298 | -- correct 299 | assert.same {true}, { 300 | check { 301 | color: "blue" 302 | weight: 234 303 | } 304 | } 305 | 306 | -- failed sub type 307 | assert.same nil, ( 308 | check { 309 | color: "green" 310 | weight: 234 311 | } 312 | ) 313 | 314 | -- missing data 315 | assert.same nil, ( 316 | check { 317 | color: "green" 318 | } 319 | ) 320 | 321 | -- extra data 322 | assert.same {true}, { 323 | check\is_open! { 324 | color: "red" 325 | weight: 9 326 | age: 3 327 | } 328 | } 329 | 330 | -- extra data 331 | assert.same nil, ( 332 | check { 333 | color: "red" 334 | weight: 9 335 | age: 3 336 | } 337 | ) 338 | 339 | it "checks value with literals", -> 340 | check = types.shape { 341 | color: "green" 342 | weight: 123 343 | ready: true 344 | } 345 | 346 | assert.same nil, ( 347 | check { 348 | color: "greenz" 349 | weight: 123 350 | ready: true 351 | } 352 | ) 353 | 354 | assert.same nil, ( 355 | check { 356 | color: "greenz" 357 | weight: 125 358 | ready: true 359 | } 360 | ) 361 | 362 | assert.same nil, ( 363 | check { 364 | color: "greenz" 365 | weight: 125 366 | ready: false 367 | } 368 | ) 369 | 370 | assert.same nil, ( 371 | check { 372 | free: true 373 | } 374 | ) 375 | 376 | assert.same true, ( 377 | check { 378 | color: "green" 379 | weight: 123 380 | ready: true 381 | } 382 | ) 383 | 384 | it "checks extra fields", -> 385 | s = types.shape { }, { 386 | extra_fields: types.map_of(types.string, types.string) 387 | } 388 | 389 | assert.same { 390 | true 391 | }, { 392 | s { 393 | hello: "world" 394 | } 395 | } 396 | 397 | assert.same { 398 | nil 399 | 'field "hello": map value expected type "string", got "number"' 400 | }, { 401 | s { 402 | hello: 10 403 | } 404 | } 405 | 406 | 407 | it "pattern", -> 408 | t = types.pattern "^hello" 409 | 410 | assert.same nil, (t 123) 411 | assert.same {true}, {t "hellowolr"} 412 | assert.same nil, (t "hell") 413 | 414 | t = types.pattern "^%d+$", coerce: true 415 | 416 | assert.same {true}, {t 123} 417 | assert.same {true}, {t "123"} 418 | assert.same nil, (t "2.5") 419 | 420 | 421 | it "map_of", -> 422 | stringmap = types.map_of types.string, types.string 423 | assert.same {true}, {stringmap {}} 424 | 425 | assert.same {true}, {stringmap { 426 | hello: "world" 427 | }} 428 | 429 | assert.same {true}, {stringmap { 430 | hello: "world" 431 | butt: "zone" 432 | }} 433 | 434 | assert.same {true}, {stringmap\is_optional! nil} 435 | assert.same nil, (stringmap nil) 436 | 437 | assert.same nil, (stringmap { hello: 5 }) 438 | assert.same nil, (stringmap { "okay" }) 439 | assert.same nil, (stringmap { -> }) 440 | 441 | static = types.map_of "hello", "world" 442 | assert.same {true}, {static {}} 443 | assert.same {true}, {static { hello: "world" }} 444 | 445 | assert.same nil, (static { helloz: "world" }) 446 | assert.same nil, (static { hello: "worldz" }) 447 | 448 | describe "array_contains", -> 449 | it "contains number type", -> 450 | numbers = types.array_contains types.number 451 | err = 'expected array containing type "number"' 452 | 453 | assert.same {nil, err}, {numbers {}} 454 | assert.same {true}, {numbers {1}} 455 | assert.same {true}, {numbers {1.5}} 456 | assert.same {nil, err}, {numbers {"one", "two"}} 457 | assert.same {nil, err}, {numbers {one: 75, "ok"}} 458 | assert.same {true}, {numbers {"one", 73, "two"}} 459 | assert.same {true}, {numbers {"one", 73, 88, "two"}} 460 | 461 | assert.same {true}, {numbers\is_optional! nil} 462 | assert.same {nil, 'expected type "table", got "nil"'}, {numbers nil} 463 | 464 | it "contains literal number 77", -> 465 | has_77 = types.array_contains 77 466 | err = 'expected array containing 77' 467 | 468 | assert.same {nil, err}, {has_77 {}} 469 | assert.same {nil, err}, {has_77 {7}} 470 | assert.same {true}, {has_77 {77}} 471 | assert.same {true}, {has_77 {"one", 77, "two"}} 472 | assert.same {nil, err}, {has_77 {thing: 77}} 473 | 474 | it "contains shape", -> 475 | shapes = types.array_contains types.shape { 476 | color: types.one_of {"orange", "blue"} 477 | } 478 | err = 'expected array containing { "color" = "orange", or "blue" }' 479 | 480 | assert.same {true}, { 481 | shapes { 482 | {color: "orange"} 483 | {color: "blue"} 484 | {color: "orange"} 485 | } 486 | } 487 | 488 | assert.same {nil, err}, { 489 | shapes { 490 | {color: "green"} 491 | {color: "yellow"} 492 | 55 493 | } 494 | } 495 | 496 | 497 | describe "array_of", -> 498 | it "of number type", -> 499 | numbers = types.array_of types.number 500 | 501 | assert.same {true}, {numbers {}} 502 | assert.same {true}, {numbers {1}} 503 | assert.same {true}, {numbers {1.5}} 504 | assert.same {true}, {numbers {1.5,2,3,4}} 505 | 506 | assert.same {true}, {numbers\is_optional! nil} 507 | assert.same {nil, 'expected type "table", got "nil"'}, {numbers nil} 508 | 509 | it "of literal string", -> 510 | hellos = types.array_of "hello" 511 | 512 | assert.same {true}, {hellos {}} 513 | assert.same {true}, {hellos {"hello"}} 514 | assert.same {true}, {hellos {"hello", "hello"}} 515 | 516 | assert.same {nil, 'array item 2: expected "hello"'}, {hellos {"hello", "world"}} 517 | 518 | it "of literal number", -> 519 | twothreefours = types.array_of 234 520 | 521 | assert.same {true}, {twothreefours {}} 522 | assert.same {true}, {twothreefours {234}} 523 | assert.same {true}, {twothreefours {234, 234}} 524 | assert.same {nil, 'array item 1: expected 234'}, {twothreefours {"uh"}} 525 | 526 | it "of shape", -> 527 | shapes = types.array_of types.shape { 528 | color: types.one_of {"orange", "blue"} 529 | } 530 | 531 | assert.same {true}, { 532 | shapes { 533 | {color: "orange"} 534 | {color: "blue"} 535 | {color: "orange"} 536 | } 537 | } 538 | 539 | assert.same { 540 | nil, 'array item 3: field "color": expected "orange", or "blue"' 541 | }, { 542 | shapes { 543 | {color: "orange"} 544 | {color: "blue"} 545 | {color: "purple"} 546 | } 547 | } 548 | 549 | it "tests length", -> 550 | t = types.array_of types.string, length: types.range(1,3) 551 | 552 | assert.same { 553 | nil 554 | 'array length not in range from 1 to 3, got 0' 555 | }, { 556 | t {} 557 | } 558 | 559 | assert.same { 560 | nil 561 | 'expected type "table", got "string"' 562 | }, { 563 | t "hi" 564 | } 565 | 566 | assert.same { 567 | true 568 | }, { 569 | t {"one", "two"} 570 | } 571 | 572 | assert.same { 573 | nil 574 | 'array length not in range from 1 to 3, got 4' 575 | }, { 576 | t {"one", "two", "nine", "10"} 577 | } 578 | 579 | t_fixed = types.array_of types.string, length: 2 580 | 581 | assert.same { 582 | nil 583 | 'array length expected 2, got 0' 584 | }, { 585 | t_fixed {} 586 | } 587 | 588 | assert.same { 589 | true 590 | }, { 591 | t_fixed {"hello", "world"} 592 | } 593 | 594 | describe "literal", -> 595 | it "checks value", -> 596 | t = types.literal "hello world" 597 | 598 | assert.same {true}, {t "hello world"} 599 | assert.same {true}, {t\check_value "hello world"} 600 | 601 | assert.same { 602 | nil, 'expected "hello world"' 603 | }, { t "hello zone" } 604 | 605 | assert.same { 606 | nil, 'expected "hello world"' 607 | }, { t\check_value "hello zone" } 608 | 609 | assert.same {nil, 'expected "hello world"'}, { t nil } 610 | assert.same {nil, 'expected "hello world"'}, { t\check_value nil } 611 | 612 | it "checks value when optional", -> 613 | t = types.literal("hello world")\is_optional! 614 | assert.same {true}, { t nil } 615 | assert.same {true}, { t\check_value nil} 616 | 617 | describe "custom", -> 618 | it "checks value", -> 619 | check = types.custom (v) -> 620 | if v == 1 621 | true 622 | else 623 | nil, "v is not 1" 624 | 625 | assert.same {nil, "v is not 1"}, { check 2 } 626 | assert.same {nil, "v is not 1"}, { check\check_value 2 } 627 | 628 | assert.same {nil, "v is not 1"}, { check nil } 629 | assert.same {nil, "v is not 1"}, { check\check_value nil } 630 | 631 | assert.same {true}, { check 1 } 632 | assert.same {true}, { check\check_value 1 } 633 | 634 | it "checks with default error message", -> 635 | t = types.custom (n) -> n % 2 == 0 636 | 637 | assert.same {nil, "failed custom check"}, {t 5} 638 | 639 | it "checks optional", -> 640 | check = types.custom( 641 | (v) -> 642 | if v == 1 643 | true 644 | else 645 | nil, "v is not 1" 646 | )\is_optional! 647 | 648 | assert.same {nil, "v is not 1"}, { check 2 } 649 | assert.same {nil, "v is not 1"}, { check\check_value 2 } 650 | 651 | assert.same {true}, { check nil } 652 | assert.same {true}, { check\check_value nil } 653 | 654 | assert.same {true}, { check 1 } 655 | assert.same {true}, { check\check_value 1 } 656 | 657 | describe "equivalent", -> 658 | it "chekcs nil", -> 659 | assert.same true, (types.equivalent(nil) nil) 660 | assert.same nil, (types.equivalent(nil) false) 661 | assert.same nil, (types.equivalent(nil) true) 662 | 663 | it "chekcs literal", -> 664 | assert.same nil, (types.equivalent("hi") nil) 665 | assert.same nil, (types.equivalent("hi") false) 666 | assert.same true, (types.equivalent("hi") "hi") 667 | 668 | it "checks table", -> 669 | assert.same true, (types.equivalent({}) {}) 670 | assert.same true, (types.equivalent({1}) {1}) 671 | assert.same true, (types.equivalent({hello: "world"}) {hello: "world"}) 672 | assert.falsy (types.equivalent({hello: "world"}) {hello: "worlds"}) 673 | 674 | assert.same nil, (types.equivalent({1}) "um") 675 | assert.same nil, (types.equivalent({1}) nil) 676 | 677 | 678 | check = types.equivalent { 679 | "great" 680 | color: { 681 | {}, {2}, { no: true} 682 | } 683 | } 684 | 685 | assert.same nil, (check\check_value "hello") 686 | assert.same nil, (check\check_value {}) 687 | 688 | assert.same nil, (check\check_value { 689 | "great" 690 | color: { 691 | {}, {4}, { no: true} 692 | } 693 | }) 694 | 695 | assert.same true, (check\check_value { 696 | "great" 697 | color: { 698 | {}, {2}, { no: true} 699 | } 700 | }) 701 | 702 | describe "range", -> 703 | it "handles numeric range", -> 704 | r = types.range 5, 10 705 | 706 | assert.same { 707 | nil 708 | 'range expected type "number", got "nil"' 709 | }, { r nil } 710 | 711 | assert.same { true }, { r 10 } 712 | assert.same { true }, { r 5 } 713 | assert.same { true }, { r 8 } 714 | 715 | assert.same { 716 | nil 717 | 'not in range from 5 to 10' 718 | }, { r 2 } 719 | 720 | assert.same { 721 | nil 722 | 'not in range from 5 to 10' 723 | }, { r 100 } 724 | 725 | it "handles string range", -> 726 | r = types.range "a", "f" 727 | 728 | assert.same { 729 | nil 730 | 'range expected type "string", got "nil"' 731 | }, { r nil } 732 | 733 | assert.same { true }, { r "a" } 734 | assert.same { true }, { r "f" } 735 | assert.same { true }, { r "c" } 736 | 737 | assert.same { 738 | nil 739 | 'not in range from a to f' 740 | }, { r "A" } 741 | 742 | assert.same { 743 | nil 744 | 'not in range from a to f' 745 | }, { r "g" } 746 | 747 | describe "tableshape.operators", -> 748 | it "sequence", -> 749 | t = types.pattern("^hello") * types.pattern("world$") 750 | assert.same {nil, 'doesn\'t match pattern "^hello"'}, {t("good work")} 751 | assert.same {nil, 'doesn\'t match pattern "world$"'}, {t("hello zone")} 752 | assert.same {true}, {t("hello world")} 753 | 754 | it "first of", -> 755 | t = types.pattern("^hello") + types.pattern("world$") 756 | assert.same {nil, 'expected pattern "^hello", or pattern "world$"'}, {t("good work")} 757 | assert.same {true}, {t("hello zone")} 758 | assert.same {true}, {t("zone world")} 759 | assert.same {true}, {t("hello world")} 760 | 761 | it "transform", -> 762 | -- is a noop when there is no transform 763 | t = types.string / "hello" 764 | assert.same {true}, {t("hello")} 765 | assert.same {nil, 'expected type "string", got "boolean"'}, {t(false)} 766 | 767 | it "operand coersion", -> 768 | test_hello = types.string * "hello" 769 | assert test_hello "hello" 770 | 771 | -- merges all options into single node 772 | addition_test = types.function + "world" + 5 + true 773 | assert addition_test "world" 774 | assert addition_test 5 775 | assert addition_test true 776 | assert addition_test -> 777 | 778 | -- all options merged into flat array 779 | assert.same #addition_test.options, 4 780 | 781 | rhs_merge = "thing" + addition_test 782 | assert.same #rhs_merge.options, 5 783 | 784 | it "invalid operands", -> 785 | assert.has_error -> 786 | -- forgot to instantiate array of 787 | types.string * types.array_of 788 | 789 | assert.has_error -> 790 | -- same thing flipped 791 | types.array_of * types.string 792 | 793 | -- incorrect operator for transformer function 794 | assert.has_error -> 795 | types.string * (hello) -> "world" 796 | 797 | assert.has_error -> 798 | -- forgot to instantiate array of 799 | types.string + types.array_of 800 | 801 | assert.has_error -> 802 | -- same thing flipped 803 | types.array_of + types.string 804 | 805 | -- incorrect operator for transformer function 806 | assert.has_error -> 807 | types.string + (hello) -> "world" 808 | 809 | describe "tableshape.repair", -> 810 | local t 811 | before_each -> 812 | t = types.array_of( 813 | types.literal("nullify") / nil + types.string\on_repair (v) -> "swap" 814 | )\on_repair (v) -> 815 | if v == false 816 | nil 817 | else 818 | {"FAIL"} 819 | 820 | it "repairs array_of", -> 821 | assert.same { 822 | { "swap", "you"} 823 | }, { 824 | t\repair {22, "you"} 825 | } 826 | 827 | assert.same { 828 | {"FAIL"} 829 | }, { 830 | t\repair "friends" 831 | } 832 | 833 | assert.same { 834 | nil 835 | 'expected array of "nullify", or type "string"' 836 | }, { 837 | t\repair false 838 | } 839 | 840 | assert.same { 841 | { "one", "swap", "swap", "last" } 842 | }, { 843 | t\repair {"one", 2, "nullify", true, "last"} 844 | } 845 | 846 | describe "tableshape.describe", -> 847 | it "describes a compound type with function", -> 848 | s = types.nil + types.literal("hello world") 849 | s = s\describe -> "str('hello world')" 850 | 851 | assert.same { true }, {s nil} 852 | assert.same { true }, {s "hello world"} 853 | assert.same { nil, "expected str('hello world')" }, {s "cool"} 854 | 855 | s = types.nil / false + types.literal("hello world") / "cool" 856 | s = s\describe -> "str('hello world')" 857 | 858 | assert.same { false }, {s\transform nil} 859 | assert.same { "cool" }, {s\transform "hello world"} 860 | assert.same { nil, "expected str('hello world')" }, {s\transform "cool"} 861 | 862 | it "describes some common types", -> 863 | assert.same [[array of type "string", or type "nil"]], types.array_of(types.string + types.nil)\_describe! 864 | assert.same [[map of type "string" -> type "number"]], types.map_of(types.string, types.number)\_describe! 865 | assert.same [[type "string"]], types.proxy(-> types.string)\_describe! 866 | 867 | assert.same [[{ "hello" = "world" }]], types.shape({ hello: "world" })\_describe! 868 | assert.same [[{ "hello" = type "function" }]], types.shape({ hello: types.function })\_describe! 869 | 870 | assert.same [[{ "hello" = "world" }]], types.partial({ hello: "world" })\_describe! 871 | assert.same [[{ "hello" = type "function" }]], types.partial({ hello: types.function })\_describe! 872 | 873 | assert.same [[custom checker function: 0xFF]], types.custom(-> false)\_describe!\gsub "0x.+$", "0xFF" 874 | 875 | assert.same [[equivalent to "hi"]], types.equivalent("hi")\_describe! 876 | 877 | assert.same [[type "string" tagged "hi"]], types.scope(types.string, tag: "hi")\_describe! 878 | 879 | 880 | 881 | it "describes a compound type with string literal", -> 882 | s = (types.nil + types.literal("hello world"))\describe "thing" 883 | 884 | assert.same { true }, {s nil} 885 | assert.same { true }, {s "hello world"} 886 | assert.same { nil, "expected thing" }, {s "cool"} 887 | 888 | s = (types.nil / false + types.literal("hello world") / "cool")\describe "thing" 889 | 890 | assert.same { false }, {s\transform nil} 891 | assert.same { "cool" }, {s\transform "hello world"} 892 | assert.same { nil, "expected thing" }, {s\transform "cool"} 893 | 894 | it "changes error message to string", -> 895 | t = (types.nil + types.string)\describe { 896 | error: "you messed up" 897 | type: "nil + string" 898 | } 899 | 900 | assert.same "nil + string", t\_describe! 901 | assert.same {nil, "you messed up"}, { t 5 } 902 | 903 | it "changes error message to function", -> 904 | called = false 905 | t = (types.nil + types.string)\describe { 906 | error: (val, err) -> 907 | assert.same 'expected type "nil", or type "string"', err 908 | called = true 909 | "okay" 910 | 911 | type: -> "ns" 912 | } 913 | 914 | assert.same "ns", t\_describe! 915 | assert.same {nil, "okay"}, { t 5 } 916 | assert.true called 917 | 918 | 919 | describe "assert type", -> 920 | it "tests for asserted type", -> 921 | s = types.assert(types.number) 922 | assert s 10 923 | assert.has_error( 924 | -> s "hello" 925 | [[expected type "number", got "string"]] 926 | ) 927 | 928 | ss = s * types.one_of { 5, 7 } 929 | 930 | assert.same { 931 | nil 932 | 'expected 5, or 7' 933 | }, { ss 10 } 934 | 935 | assert.true (ss 7) 936 | 937 | assert.has_error( 938 | -> ss "hello" 939 | [[expected type "number", got "string"]] 940 | ) 941 | 942 | it "transforms asserted type", -> 943 | s = types.assert(types.number) 944 | ss = s * types.one_of({ 5, 7 }) / (n) -> -n 945 | assert.same -5, (ss\transform 5) 946 | 947 | assert.has_error( 948 | -> ss -> 949 | [[expected type "number", got "function"]] 950 | ) 951 | 952 | it "describes asserted type", -> 953 | s = types.assert(types.number) 954 | ss = s * types.one_of { 5, 7 } 955 | assert.same 'assert type "number"', s\_describe! 956 | assert.same 'assert type "number" then 5, or 7', ss\_describe! 957 | 958 | describe "not", -> 959 | it "inverts type checker", -> 960 | not_a_string = -types.string 961 | assert.true not_a_string 10 962 | assert.nil (not_a_string "hello") 963 | 964 | it "inverted type checker ignores transform", -> 965 | wrapped = types.string / (str) -> "wow#{str}wow" 966 | not_wrapped = -wrapped 967 | 968 | assert.same { 969 | nil 970 | 'expected not type "string"' 971 | }, {not_wrapped\transform "hello"} 972 | 973 | assert.same { 974 | 200 975 | }, {not_wrapped\transform 200} 976 | 977 | it "describes inverted type checker", -> 978 | not_a_string = -types.string 979 | assert.same 'not type "string"', not_a_string\_describe! 980 | 981 | describe "clone", -> 982 | it "detects clonable object", -> 983 | assert types.clone "hello" 984 | assert types.clone nil 985 | assert types.clone true 986 | assert types.clone {} 987 | assert types.clone 303 988 | assert types.clone 303.248 989 | 990 | if newproxy 991 | assert.nil types.clone newproxy! 992 | 993 | assert.nil types.clone => 994 | 995 | it "describes clone", -> 996 | assert.same "array of cloneable value", types.array_of(types.clone)\_describe! 997 | 998 | 999 | describe "metatable_is", -> 1000 | it "describes", -> 1001 | empty_mt_type = types.metatable_is(types.nil) 1002 | 1003 | mt_type = types.metatable_is types.shape { 1004 | __index: types.table + types.function 1005 | } 1006 | 1007 | assert.same [[has metatable type "nil"]], empty_mt_type\_describe! 1008 | assert.same [[has metatable { "__index" = type "table", or type "function" }]], mt_type\_describe! 1009 | 1010 | it "detects empty metatable", -> 1011 | empty_mt_type = types.metatable_is(types.nil) 1012 | assert empty_mt_type {} 1013 | 1014 | assert.same {nil, 'metatable expected: expected type "nil", got "table"'}, { empty_mt_type setmetatable {}, {} } 1015 | 1016 | -- non tables not eligible for metatables 1017 | assert.nil (empty_mt_type false) 1018 | assert.nil (empty_mt_type 2349824) 1019 | 1020 | it "detects metatable", -> 1021 | mt_type = types.metatable_is types.shape { 1022 | __index: types.table + types.function 1023 | } 1024 | 1025 | assert mt_type (setmetatable {}, { 1026 | __index: {} 1027 | }) 1028 | 1029 | assert.same { 1030 | nil 1031 | 'metatable expected: field "__index": expected type "table", or type "function"' 1032 | }, { 1033 | mt_type (setmetatable {}, { 1034 | __index: false 1035 | }) 1036 | } 1037 | 1038 | it "fails when trying to mutate metatable", -> 1039 | mt_type = types.metatable_is types.shape { 1040 | cool: types.any / "five" 1041 | } 1042 | 1043 | obj = {} 1044 | mt = {} 1045 | 1046 | assert.same { 1047 | nil 1048 | 'metatable was modified by a type but { allow_metatable_update = true } is not enabled' 1049 | }, { 1050 | mt_type (setmetatable obj, mt) 1051 | } 1052 | 1053 | assert.same mt, {} 1054 | assert mt == getmetatable(obj) 1055 | 1056 | it "allows mutation when activated", -> 1057 | mt_type = types.metatable_is types.shape({ 1058 | cool: types.any / "five" 1059 | }), { 1060 | allow_metatable_update: true 1061 | } 1062 | 1063 | obj = {} 1064 | mt = {} 1065 | assert mt_type (setmetatable obj, mt) 1066 | 1067 | assert.same {}, mt 1068 | assert.same { 1069 | cool: "five" 1070 | }, getmetatable obj 1071 | 1072 | it "allows state change in metatable type", -> 1073 | mt_type = types.metatable_is types.shape { 1074 | __index: types.any\tag "index" 1075 | } 1076 | 1077 | obj = {} 1078 | mt = { 1079 | __index: "hello" 1080 | } 1081 | 1082 | setmetatable obj, mt 1083 | 1084 | s = mt_type obj 1085 | assert.same {index: "hello"}, s 1086 | 1087 | describe "__tostring", -> 1088 | it "describes all types", -> 1089 | import is_type from require "tableshape" 1090 | for name, t in pairs types 1091 | if is_type(t) 1092 | tostring t 1093 | 1094 | it "describes composite type", -> 1095 | t = types.nil + types.shape({ hello: "world"})\tag "hi" 1096 | assert.same( 1097 | [[type "nil", or { "hello" = "world" } tagged "hi"]] 1098 | tostring t 1099 | ) 1100 | tt = t\describe "strange object" 1101 | assert.same [[strange object]], tostring tt 1102 | 1103 | 1104 | -------------------------------------------------------------------------------- /spec/tags_spec.moon: -------------------------------------------------------------------------------- 1 | import types from require "tableshape" 2 | 3 | -- test tags output from both check_value and transform 4 | assert_tags = (t, arg, expected)-> 5 | assert.same expected, (t(arg)) 6 | out, tags = t\transform arg 7 | assert.same expected, out and tags or nil 8 | 9 | describe "tableshape.tags", -> 10 | it "literal", -> 11 | t = types.literal("hi")\tag "what" 12 | assert_tags t, "hi", { 13 | what: "hi" 14 | } 15 | 16 | assert_tags t, "no", nil 17 | 18 | it "number", -> 19 | t = types.number\tag "val" 20 | assert_tags t, 15, { 21 | val: 15 22 | } 23 | 24 | assert_tags t, "no", nil 25 | 26 | it "string", -> 27 | t = types.string\length(types.range(1,2)\tag "len")\tag "val" 28 | assert_tags t, "hi", { 29 | val: "hi" 30 | len: 2 31 | } 32 | 33 | describe "function tag", -> 34 | it "tags basic object", -> 35 | t = types.array_of types.literal("hi")\tag (state, val) -> 36 | assert.same "hi", val 37 | 38 | if state.count 39 | state.count += 1 40 | else 41 | state.count = 1 42 | 43 | assert_tags t, {"hi"}, { 44 | count: 1 45 | } 46 | 47 | assert_tags t, {"hi", "hi", "hi"}, { 48 | count: 3 49 | } 50 | 51 | describe "scope", -> 52 | it "stores scoped tags into array", -> 53 | obj = types.shape { 54 | name: types.string\tag "name" 55 | }, open: true 56 | 57 | larp = types.literal "larp" 58 | bart = types.literal("bart")\tag "other" 59 | 60 | s = types.shape { 61 | things: types.array_of types.scope(obj + larp + bart, tag: "names[]") 62 | color: types.string\tag "color" 63 | } 64 | 65 | assert_tags s, { 66 | color: "blue" 67 | things: { 68 | { name: "bart", height: 10 } 69 | "larp" 70 | { name: "narf", color: "blue" } 71 | "larp" 72 | "bart" 73 | { name: "woopsie"} 74 | "larp" 75 | } 76 | }, { 77 | color: "blue" 78 | names: { 79 | { name: "bart" } 80 | { name: "narf" } 81 | { other: "bart" } 82 | { name: "woopsie" } 83 | } 84 | } 85 | 86 | it "drops scope with no name", -> 87 | scoped = types.scope types.shape({ 88 | hello: types.string\tag "thing" 89 | }) % (value, state) -> 90 | "value: #{state.thing}" 91 | 92 | t = types.shape { 93 | name: types.string\tag "the_name" 94 | items: types.array_of scoped 95 | } 96 | 97 | obj, state = assert t\transform { 98 | name: "dodo" 99 | items: { 100 | { hello: "world" } 101 | { hello: "zone" } 102 | } 103 | } 104 | 105 | assert.same { 106 | name: "dodo" 107 | items: { 108 | "value: world" 109 | "value: zone" 110 | } 111 | }, obj 112 | 113 | assert.same { 114 | the_name: "dodo" 115 | }, state 116 | 117 | 118 | it "scope and function tag", -> 119 | o = types.shape { 120 | [1]: types.number\tag "id" 121 | name: types.string\tag "name" 122 | } 123 | 124 | called = 0 125 | 126 | s = types.shape { 127 | types.string\tag "color" 128 | types.scope o, tag: (state, val, val_state) -> 129 | called += 1 130 | 131 | -- the state of the current scope 132 | assert.same { color: "red" }, state 133 | 134 | -- the value being checked 135 | assert.same { 136 | 1234 137 | name: "hello" 138 | }, val 139 | 140 | -- the state captured from the value 141 | assert.same { name: "hello", id: 1234 }, val_state 142 | 143 | state.the_name = val_state.name 144 | } 145 | 146 | assert_tags s, { 147 | "red" 148 | { 149 | 1234 150 | name: "hello" 151 | } 152 | }, { 153 | color: "red" 154 | the_name: "hello" 155 | } 156 | 157 | assert.same 2, called 158 | 159 | describe "one_of", -> 160 | it "takes matching tag", -> 161 | s = types.one_of { 162 | types.string\tag "str" 163 | types.number\tag "num" 164 | types.function\tag "func" 165 | } 166 | 167 | assert_tags s, "hello", { 168 | str: "hello" 169 | } 170 | 171 | assert_tags s, 5, { 172 | num: 5 173 | } 174 | 175 | fn = -> print "hi" 176 | assert_tags s, fn, { 177 | func: fn 178 | } 179 | 180 | assert_tags s, {}, nil 181 | 182 | describe "all_of", -> 183 | it "matches multi", -> 184 | s = types.all_of { 185 | types.table\tag "table" 186 | types.shape { 187 | a: types.number\tag "x" 188 | }, open: true 189 | types.shape { 190 | b: types.number\tag "y" 191 | }, open: true 192 | } 193 | 194 | assert_tags s, { 195 | a: 43 196 | b: 2 197 | what: "ok" 198 | }, { 199 | table: { 200 | a: 43 201 | b: 2 202 | what: "ok" 203 | } 204 | x: 43 205 | y: 2 206 | } 207 | 208 | tags = {} 209 | assert.nil (s {}, tags) 210 | assert.same {}, tags 211 | 212 | tags = {} 213 | assert.nil (s { a: 443}, tags) 214 | assert.same {}, tags 215 | 216 | tags = {} 217 | assert.nil (s { a: 443, b: "no"}, tags) 218 | assert.same {}, tags 219 | 220 | describe "array_of", -> 221 | it "matches array", -> 222 | t = types.array_of types.shape { 223 | s: types.string\tag "thing" 224 | } 225 | 226 | assert_tags t, { 227 | { s: "hello" } 228 | { s: "world" } 229 | }, { 230 | thing: "world" 231 | } 232 | 233 | it "matches array length", -> 234 | t = types.array_of types.string, length: types.range(1,2)\tag "len" 235 | 236 | assert_tags t, { 237 | "one" 238 | "two" 239 | }, { 240 | len: 2 241 | } 242 | 243 | assert_tags t, { 244 | "one" 245 | }, { 246 | len: 1 247 | } 248 | 249 | assert_tags t, { 250 | "one" 251 | "one1" 252 | "one2" 253 | }, nil 254 | 255 | it "matches many items from array", -> 256 | t1 = types.array_of types.number\tag "hi[]" 257 | 258 | assert_tags t1, { 1,2,3,4 }, { 259 | hi: {1,2,3,4} 260 | } 261 | 262 | 263 | t = types.array_of types.shape { 264 | s: types.string\tag "thing[]" 265 | } 266 | 267 | assert_tags t, { 268 | { s: "hello" } 269 | { s: "world" } 270 | }, { 271 | thing: { 272 | "hello" 273 | "world" 274 | } 275 | } 276 | 277 | describe "map_of", -> 278 | it "matches regular map", -> 279 | t = types.map_of "hello", types.string\tag "world" 280 | 281 | assert_tags t, { 282 | hello: "something" 283 | }, { 284 | world: "something" 285 | } 286 | 287 | 288 | describe "shape", -> 289 | it "basic shape", -> 290 | s = types.shape { 291 | types.number\tag "x" 292 | types.number\tag "y" 293 | types.number 294 | t: types.string 295 | color: types.string\tag "color" 296 | } 297 | 298 | assert_tags s, { 299 | 1 300 | 2 301 | 3 302 | t: "board" 303 | color: "blue" 304 | }, { 305 | x: 1 306 | y: 2 307 | color: "blue" 308 | } 309 | 310 | it "doesn't write partial tags", -> 311 | t = types.shape { 312 | types.string\tag "hello" 313 | types.string\tag "world" 314 | } 315 | 316 | out = t { "one", "two" }, {} 317 | 318 | assert.same { 319 | hello: "one" 320 | world: "two" 321 | }, out 322 | 323 | it "doesn't mutate state object", -> 324 | t = types.shape { 325 | types.string\tag "hello" 326 | types.string\tag "world" 327 | } 328 | 329 | -- doesn't mutate state 330 | s = {} 331 | t { "one", 5 }, s 332 | assert.same {}, s 333 | 334 | it "throws out state from partial match", -> 335 | t = types.shape { 336 | types.string\tag "str[]" 337 | types.one_of { 338 | types.array_of(types.string\tag "str[]") 339 | types.array_of((types.string + types.number)\tag "str_or_number[]") 340 | } 341 | } 342 | 343 | out = t { 344 | "alpha" 345 | { "one", "two", 3 } 346 | } 347 | 348 | assert.same { 349 | str: {"alpha"} 350 | str_or_number: {"one", "two", 3} 351 | }, out 352 | 353 | -- sanity check, for when it matches 354 | out = t { 355 | "alpha" 356 | { "one", "two", "three" } 357 | } 358 | 359 | assert.same { 360 | str: {"alpha", "one", "two", "three"} 361 | }, out 362 | 363 | it "gets tagged extra fields", -> 364 | s = types.shape { 365 | color: types.string 366 | }, { 367 | extra_fields: types.map_of types.string\tag("extra_key[]"), types.string\tag("extra_val[]") 368 | } 369 | 370 | assert_tags s, { 371 | color: "blue" 372 | height: "10cm" 373 | }, { 374 | extra_key: {"height"} 375 | extra_val: {"10cm"} 376 | } 377 | 378 | describe "array_contains", -> 379 | object = { 380 | header: { 381 | subject: "Hello world" 382 | date: "June 99th" 383 | from: "leafo.net" 384 | } 385 | body: { 386 | { 387 | content: "Hi there" 388 | content_type: "text/plain" 389 | } 390 | { 391 | content: "Oh there?" 392 | content_type: "text/html" 393 | } 394 | } 395 | } 396 | 397 | it "gets tags where first item passes", -> 398 | extract = types.partial { 399 | header: types.partial { subject: types.string\tag "subject" } 400 | 401 | body: types.one_of { 402 | types.array_contains types.partial { 403 | content: types.string\tag "body" 404 | content_type: types.literal("text/html")\tag "content_type" 405 | } 406 | } 407 | } 408 | 409 | assert.same { 410 | subject: "Hello world" 411 | body: "Oh there?" 412 | content_type: "text/html" 413 | }, extract object 414 | 415 | it "gets tags where second item passes", -> 416 | extract = types.partial { 417 | header: types.partial { subject: types.string\tag "subject" } 418 | 419 | body: types.one_of { 420 | types.array_contains types.partial { 421 | content: types.string\tag "body" 422 | content_type: types.literal("text/plain")\tag "content_type" 423 | } 424 | } 425 | } 426 | 427 | assert.same { 428 | subject: "Hello world" 429 | body: "Hi there" 430 | content_type: "text/plain" 431 | }, extract object 432 | 433 | -------------------------------------------------------------------------------- /spec/transform_spec.moon: -------------------------------------------------------------------------------- 1 | import types from require "tableshape" 2 | 3 | describe "tableshape.transform", -> 4 | it "transform node", -> 5 | n = types.string / (str) -> "--#{str}--" 6 | assert.same { 7 | "--hello--" 8 | }, {n\transform "hello"} 9 | 10 | assert.same { 11 | nil 12 | 'expected type "string", got "number"' 13 | }, {n\transform 5} 14 | 15 | r = types.range(1,5) / (n) -> n * 10 16 | assert.same { 40 }, {r\transform 4} 17 | assert.same { nil, 'not in range from 1 to 5' }, {r\transform 20} 18 | 19 | it "sequnce node", -> 20 | n = types.string * types.literal "hello" 21 | 22 | assert.same { 23 | "hello" 24 | }, { n\transform "hello" } 25 | 26 | assert.same { 27 | nil 28 | 'expected "hello"' 29 | }, { n\transform "world" } 30 | 31 | assert.same { 32 | nil 33 | 'expected type "string", got "boolean"' 34 | }, { n\transform true } 35 | 36 | it "first of node", -> 37 | n = types.literal(55) + types.string + types.array 38 | 39 | assert.same { 40 | nil 41 | 'expected 55, type "string", or an array' 42 | }, { n\transform 65 } 43 | 44 | assert.same { 45 | "does this work?" 46 | }, { 47 | n\transform "does this work?" 48 | } 49 | 50 | assert.same { 51 | 55 52 | }, { 53 | n\transform 55 54 | } 55 | 56 | assert.same { 57 | {1,2,3} 58 | }, { 59 | n\transform {1,2,3} 60 | } 61 | 62 | describe "shape", -> 63 | it "handles literal", -> 64 | n = types.shape { 65 | color: "blue" 66 | } 67 | 68 | assert.same { 69 | nil 70 | 'field "color": expected "blue"' 71 | }, { n\transform { color: "red" } } 72 | 73 | assert.same { 74 | { 75 | color: "blue" 76 | } 77 | }, { n\transform { color: "blue" } } 78 | 79 | it "returns same object", -> 80 | n = types.shape { 81 | color: "red" 82 | } 83 | 84 | input = { color: "red" } 85 | 86 | output = assert n\transform input 87 | assert input == output, "expected output to be same object as input" 88 | 89 | it "handles dirty key in extra_fields", -> 90 | n = types.shape { 91 | height: types.number 92 | }, extra_fields: types.map_of((types.literal("hello") / "world") + types.string, types.string) 93 | 94 | input = { height: 55 } 95 | output = assert n\transform input 96 | assert input == output, "expected output to be same object as input" 97 | 98 | input = { height: 55, one: "two" } 99 | output = assert n\transform input 100 | assert input == output, "expected output to be same object as input" 101 | 102 | input = { height: 55, one: "two", hello: "thing" } 103 | output = assert n\transform input 104 | assert input != output, "expected output different object" 105 | assert.same { 106 | world: "thing" 107 | one: "two" 108 | height: 55 109 | }, output 110 | 111 | it "handles dirty value in extra_fields", -> 112 | n = types.shape { 113 | height: types.number 114 | }, extra_fields: types.map_of(types.string, (types.literal("n") / "b") + types.string) 115 | 116 | input = { height: 55 } 117 | output = assert n\transform input 118 | assert input == output, "expected output to be same object as input" 119 | 120 | input = { height: 55, hi: "hi" } 121 | output = assert n\transform input 122 | assert input == output, "expected output to be same object as input" 123 | 124 | input = { height: 55, hi: "n" } 125 | output = assert n\transform input 126 | assert input != output, "expected output to be different object from input" 127 | assert.same { 128 | height: 55 129 | hi: "b" 130 | }, output 131 | 132 | it "handles dirty value when removing field from extra_fields", -> 133 | n = types.shape { 134 | one: types.string 135 | }, extra_fields: types.map_of(types.number, types.any) + types.any / nil 136 | 137 | input = { one: "two" } 138 | output = assert n\transform input 139 | assert input == output, "expected output to be same object as input" 140 | 141 | input = { one: "two", "yes" } 142 | output = assert n\transform input 143 | assert input == output, "expected output to be same object as input" 144 | 145 | input = { one: "two", some: "thing" } 146 | output = assert n\transform input 147 | assert input != output, "expected output to be different object as input" 148 | assert.same { 149 | one: "two" 150 | }, output 151 | 152 | it "handles non table", -> 153 | n = types.shape { 154 | color: types.literal "red" 155 | } 156 | 157 | assert.same { 158 | nil 159 | 'expected type "table", got "boolean"' 160 | }, { 161 | n\transform true 162 | } 163 | 164 | it "single field", -> 165 | n = types.shape { 166 | color: types.one_of { "blue", "green", "red"} 167 | } 168 | 169 | assert.same { 170 | nil 171 | 'field "color": expected "blue", "green", or "red"' 172 | },{ 173 | n\transform { color: "purple" } 174 | } 175 | 176 | assert.same { 177 | { color: "green" } 178 | },{ 179 | n\transform { color: "green" } 180 | } 181 | 182 | assert.same { 183 | nil 184 | 'extra fields: "height"' 185 | },{ 186 | n\transform { color: "green", height: "10" } 187 | } 188 | 189 | assert.same { 190 | nil 191 | 'extra fields: 1, 2, "cool"' 192 | },{ 193 | n\transform { color: "green", cool: "10", "a", "b" } 194 | } 195 | 196 | it "single field nil", -> 197 | n = types.shape { 198 | friend: types.nil 199 | } 200 | 201 | assert.same { 202 | {} 203 | },{ 204 | n\transform {} 205 | } 206 | 207 | assert.same { 208 | nil 209 | 'field "friend": expected type "nil", got "string"' 210 | },{ 211 | n\transform { friend: "what up" } 212 | } 213 | 214 | it "single field with transform", -> 215 | n = types.shape { 216 | value: types.one_of({ "blue", "green", "red"}) + types.string / "unknown" + types.number / (n) -> n + 5 217 | } 218 | 219 | assert.same { 220 | nil 221 | 'field "value": expected "blue", "green", or "red", type "string", or type "number"' 222 | }, { 223 | n\transform { } 224 | } 225 | 226 | 227 | assert.same { 228 | { value: "red" } 229 | }, { 230 | n\transform { 231 | value: "red" 232 | } 233 | } 234 | 235 | assert.same { 236 | { value: "unknown" } 237 | }, { 238 | n\transform { 239 | value: "purple" 240 | } 241 | } 242 | 243 | assert.same { 244 | { value: 15 } 245 | }, { 246 | n\transform { 247 | value: 10 248 | } 249 | } 250 | 251 | it "single field open table", -> 252 | n = types.shape { 253 | age: (types.table + types.number / (v) -> {seconds: v}) * types.shape { 254 | seconds: types.number 255 | } 256 | }, open: true 257 | 258 | assert.same { 259 | { 260 | age: { 261 | seconds: 10 262 | } 263 | } 264 | }, { 265 | n\transform { 266 | age: 10 267 | } 268 | } 269 | 270 | assert.same { 271 | { 272 | age: { 273 | seconds: 12 274 | } 275 | } 276 | }, { 277 | n\transform { 278 | age: 12 279 | } 280 | } 281 | 282 | assert.same { 283 | nil 284 | 'field "age": expected type "table", or type "number"' 285 | }, { 286 | n\transform { 287 | age: "hello" 288 | } 289 | } 290 | 291 | assert.same { 292 | nil 293 | 'field "age": expected type "table", or type "number"' 294 | }, { 295 | n\transform { 296 | age: "hello" 297 | another: "one" 298 | } 299 | } 300 | 301 | assert.same { 302 | { 303 | age: { 304 | seconds: 10 305 | } 306 | one: "two" 307 | } 308 | }, { 309 | n\transform { 310 | age: 10 311 | one: "two" 312 | } 313 | } 314 | 315 | assert.same { 316 | { 317 | color: "red" 318 | age: { seconds: 12 } 319 | another: { 320 | 1,2,4 321 | } 322 | } 323 | }, { 324 | n\transform { 325 | color: "red" 326 | age: 12 327 | another: {1,2,4} 328 | } 329 | } 330 | 331 | it "multiple failures & check_all", -> 332 | t = types.shape { 333 | "blue" 334 | "red" 335 | } 336 | 337 | assert.same { 338 | nil 339 | 'field 1: expected "blue"' 340 | }, { 341 | t\transform { 342 | "orange", "blue", "purple" 343 | } 344 | } 345 | 346 | assert.same { 347 | nil 348 | "extra fields: 3, 4" 349 | }, { 350 | t\transform { 351 | "blue", "red", "purple", "yello" 352 | } 353 | } 354 | 355 | t = types.shape { 356 | "blue" 357 | "red" 358 | }, check_all: true 359 | 360 | assert.same { 361 | nil 362 | 'field 1: expected "blue"; field 2: expected "red"; extra fields: 3' 363 | }, { 364 | t\transform { 365 | "orange", "blue", "purple" 366 | } 367 | } 368 | 369 | it "extra field", -> 370 | s = types.shape { }, { 371 | extra_fields: types.map_of(types.string, types.string) 372 | } 373 | 374 | assert.same { 375 | { hello: "world" } 376 | }, { 377 | s\transform { 378 | hello: "world" 379 | } 380 | } 381 | 382 | assert.same { 383 | nil 384 | -- TODO: this error message not good 385 | 'field "hello": map value expected type "string", got "number"' 386 | }, { 387 | s\transform { 388 | hello: 10 389 | } 390 | } 391 | 392 | s = types.shape { }, { 393 | extra_fields: types.map_of(types.string, types.string / tonumber) 394 | } 395 | 396 | assert.same { 397 | { } 398 | }, { 399 | s\transform { hello: "world" } 400 | } 401 | 402 | assert.same { 403 | { hello: 15 } 404 | }, { 405 | s\transform { hello: "15" } 406 | } 407 | 408 | s = types.shape { }, { 409 | extra_fields: types.map_of( 410 | (types.string / (s) -> "junk_#{s}") + types.any / nil 411 | types.any 412 | ) 413 | } 414 | 415 | 416 | assert.same { 417 | { junk_hello: true } 418 | }, { 419 | s\transform { hello: true, 1,2,3, [false]: "yes" } 420 | } 421 | 422 | s = types.shape { 423 | color: types.string 424 | }, extra_fields: types.any / nil 425 | 426 | assert.same { 427 | {color: "red"} 428 | }, { 429 | s\transform { 430 | color: "red" 431 | 1,2,3 432 | another: "world" 433 | } 434 | 435 | } 436 | describe "array_contains", -> 437 | it "handles non table", -> 438 | n = types.array_of types.literal "world" 439 | 440 | assert.same { 441 | nil 442 | 'expected type "table", got "boolean"' 443 | }, { 444 | n\transform true 445 | } 446 | 447 | it "doesn't mutate object when failing", -> 448 | n = types.array_contains types.number + types.string / "YA" 449 | 450 | arr = { true, false, {} } 451 | assert.same { 452 | nil 453 | 'expected array containing type "number", or type "string"' 454 | }, { n\transform arr } 455 | 456 | assert.same { true, false, {} }, arr 457 | 458 | it "returns same object if no transforms happen", -> 459 | n = types.array_contains types.number + types.string / "YA" 460 | arr = { true, false, 7, {} } 461 | res = n\transform arr 462 | assert.true arr == res 463 | assert.same { 464 | true, false, 7, {} 465 | }, res 466 | 467 | it "returns different object if transform happens", -> 468 | n = types.array_contains types.number + types.string / "YA" 469 | arr = { true, "okay", 7, {} } 470 | res = n\transform arr 471 | assert.true arr != res 472 | 473 | assert.same { 474 | true, "YA", 7, {} 475 | }, res 476 | 477 | assert.same { 478 | true, "okay", 7, {} 479 | }, arr 480 | 481 | -- reuses same object 482 | assert.true res[4] == arr[4] 483 | 484 | it "short circuits by default", -> 485 | n = types.array_contains types.number + types.string / "YA" 486 | arr = {false, 1, "a", "b"} 487 | 488 | assert.true arr == n\transform(arr) 489 | 490 | arr2 = {false, "a", "b"} 491 | 492 | assert.same {false, "YA", "b"}, n\transform arr2 493 | 494 | it "processes everything with short_circuit disabled", -> 495 | n = types.array_contains types.number + types.string / "YA", { 496 | short_circuit: false 497 | } 498 | assert.false n.short_circuit 499 | 500 | arr = {false, 1, "a", "b"} 501 | 502 | assert.same {false, 1, "YA", "YA"}, n\transform(arr) 503 | assert.same {false, 1, "a", "b"}, arr 504 | 505 | arr2 = {false, "a", "b"} 506 | 507 | assert.same {false, "YA", "YA"}, n\transform arr2 508 | 509 | it "strips nils by default (respecting short_circuit)", -> 510 | n = types.array_contains types.boolean / nil 511 | 512 | assert.same false, n.keep_nils 513 | assert.same {1,2,3, true}, n\transform { 514 | 1,2, false, 3, true 515 | } 516 | 517 | n = types.array_contains types.boolean / nil, { 518 | short_circuit: false 519 | } 520 | 521 | assert.same {1,2,3}, n\transform { 522 | 1,2, false, 3, true 523 | } 524 | 525 | -- TODO: it should probably be able to loop over whole array 526 | it "handles array that contains nil", -> 527 | n = types.array_contains types.boolean / nil 528 | assert.same { 529 | nil 530 | 'expected array containing type "boolean"' 531 | }, { 532 | n\transform { 533 | 1, nil, 2, false, 4 534 | } 535 | } 536 | 537 | n = types.array_contains types.boolean / nil 538 | assert.same { 539 | {1,nil, 2,false,4} 540 | }, { 541 | n\transform { 542 | 1, false, nil, 2, false, 4 543 | } 544 | } 545 | 546 | it "respects keep_nils", -> 547 | n = types.array_contains types.boolean / nil, { 548 | keep_nils: true 549 | } 550 | 551 | assert.same {1,2,nil, 3, true}, n\transform { 552 | 1,2, false, 3, true 553 | } 554 | 555 | n = types.array_contains types.boolean / nil, { 556 | keep_nils: true 557 | short_circuit: false 558 | } 559 | 560 | assert.same {1,2, nil, 3}, n\transform { 561 | 1,2, false, 3, true 562 | } 563 | 564 | 565 | describe "array_of", -> 566 | it "handles non table", -> 567 | n = types.array_of types.literal "world" 568 | 569 | assert.same { 570 | nil 571 | 'expected type "table", got "boolean"' 572 | }, { 573 | n\transform true 574 | } 575 | 576 | it "returns same object if no transforms happen", -> 577 | n = types.array_of types.number + types.string / "YA" 578 | arr = {5, 2, 1.7, 2} 579 | res = n\transform arr 580 | assert.true arr == res 581 | assert.same { 582 | 5, 2, 1.7, 2 583 | }, res 584 | 585 | it "returns new object if when transforming", -> 586 | n = types.array_of types.number + types.string / "YA" 587 | 588 | arr = {5,"hello",7,8} 589 | res = n\transform arr 590 | 591 | assert.false arr == res 592 | assert.same { 5,"hello",7,8 }, arr 593 | assert.same { 5, "YA", 7, 8}, res 594 | 595 | arr = {"hello",7,"world"} 596 | res = n\transform arr 597 | 598 | assert.false arr == res 599 | assert.same {"hello",7,"world"}, arr 600 | assert.same {"YA", 7, "YA"}, res 601 | 602 | -- TODO: is this correct behavior? 603 | it "transformed object doesn't contain non-array keys", -> 604 | n = types.array_of types.number + types.string / "YA" 605 | 606 | arr = {5,"hello",7,8, color: "green"} 607 | res = n\transform arr 608 | 609 | assert.false arr == res 610 | assert.same { 5,"hello",7,8, color: "green" }, arr 611 | assert.same { 5, "YA", 7, 8}, res 612 | 613 | 614 | it "returns new object when stripping nils", -> 615 | n = types.array_of types.number + types.string / nil 616 | 617 | arr = {5,"hello",7,8} 618 | res = n\transform arr 619 | 620 | assert.false arr == res 621 | assert.same { 5,"hello",7,8 }, arr 622 | assert.same { 5, 7, 8}, res 623 | 624 | n2 = types.array_of types.number + types.string / nil, { 625 | keep_nils: true 626 | } 627 | 628 | arr = {5,"hello",7,8} 629 | res = n2\transform arr 630 | 631 | assert.false arr == res 632 | assert.same { 5, "hello", 7, 8 }, arr 633 | assert.same { 5, nil, 7, 8 }, res 634 | 635 | 636 | it "transforms array items", -> 637 | n = types.array_of types.string + types.number / (n) -> "number: #{n}" 638 | 639 | assert.same { 640 | { 641 | "number: 1" 642 | "one" 643 | "number: 3" 644 | } 645 | }, { 646 | n\transform { 1,"one",3 } 647 | } 648 | 649 | assert.same { 650 | nil 651 | 'array item 2: expected type "string", or type "number"' 652 | }, { 653 | n\transform {1, true} 654 | } 655 | 656 | it "transforms array with literals", -> 657 | n = types.array_of 5 658 | 659 | assert.same { 660 | { 5,5 } 661 | },{ 662 | n\transform { 5, 5 } 663 | } 664 | 665 | assert.same { 666 | nil 667 | 'array item 2: expected 5' 668 | },{ 669 | n\transform { 5, 6 } 670 | } 671 | 672 | it "transforms empty array", -> 673 | n = types.array_of types.string 674 | assert.same { 675 | {} 676 | }, { n\transform {} } 677 | 678 | it "strips nil values", -> 679 | filter = types.array_of types.string + types.any / nil 680 | 681 | assert.same { 682 | { "one", "hello" } 683 | }, { 684 | filter\transform { 685 | "one", 5, (->), "hello", true 686 | } 687 | } 688 | 689 | it "keeps nil values", -> 690 | filter = types.array_of types.string + types.any / nil, { 691 | keep_nils: true 692 | } 693 | 694 | assert.same { 695 | { "one", nil, nil, "hello", nil } 696 | }, { 697 | filter\transform { 698 | "one", 5, (->), "hello", true 699 | } 700 | } 701 | 702 | it "tests length", -> 703 | f = types.array_of types.string + types.any / nil, length: types.range 2,3 704 | assert.same { 705 | {"one", "two"} 706 | }, { 707 | f\transform {"one", true, "two"} 708 | } 709 | 710 | assert.same { 711 | nil 712 | 'array length not in range from 2 to 3, got 4' 713 | }, { 714 | f\transform {"one", true, "two", false} 715 | } 716 | 717 | describe "map_of", -> 718 | it "non table", -> 719 | n = types.map_of types.string, types.string 720 | 721 | assert.same { 722 | nil 723 | 'expected type "table", got "boolean"' 724 | }, { 725 | n\transform true 726 | } 727 | 728 | it "returns same object", -> 729 | n = types.map_of types.string, types.string + types.number / (v) -> tostring v 730 | 731 | input = { one: 5 } 732 | output = assert n\transform input 733 | assert input != output, "expected output to be same object as input" 734 | assert.same { one: "5" }, output 735 | 736 | input = { one: "two" } 737 | output = assert n\transform input 738 | assert input == output, "expected output to be same object as input" 739 | 740 | it "empty table", -> 741 | n = types.map_of types.string, types.string 742 | input = {} 743 | output = assert n\transform input 744 | -- it returns same object 745 | assert.true input == output 746 | 747 | it "transforms keys & values", -> 748 | n = types.map_of( 749 | types.string + types.number / tostring 750 | types.number + types.string / tonumber 751 | ) 752 | 753 | input = { 754 | "10" 755 | "20" 756 | } 757 | 758 | output = assert n\transform input 759 | 760 | assert.false input == output 761 | 762 | assert.same { 763 | "1": 10 764 | "2": 20 765 | }, output 766 | 767 | --input is unchanged 768 | assert.same { 769 | "10", "20" 770 | }, input 771 | 772 | assert.same { 773 | nil 774 | 'map value expected type "number", or type "string"' 775 | }, { 776 | n\transform { 777 | hello: true 778 | } 779 | } 780 | 781 | assert.same { 782 | nil 783 | 'map key expected type "string", or type "number"' 784 | }, { 785 | n\transform { 786 | [true]: 10 787 | } 788 | } 789 | 790 | it "transforms to new object with nested transform", -> 791 | t = types.map_of types.string, types.array_of types.number + types.string / nil 792 | 793 | input = { 794 | hello: {} 795 | world: {1} 796 | zone: {1,2} 797 | } 798 | 799 | output = assert t\transform input 800 | assert.true input == output 801 | 802 | input = { 803 | hello: {} 804 | world: {1} 805 | one: {"one",2} 806 | zone: {1,2} 807 | } 808 | output = assert t\transform input 809 | 810 | assert.false input == output 811 | assert.same { 812 | hello: {} 813 | world: {1} 814 | one: {2} 815 | zone: {1,2} 816 | }, output 817 | 818 | assert.same { 819 | hello: {} 820 | world: {1} 821 | one: {"one",2} 822 | zone: {1,2} 823 | }, input 824 | 825 | assert.true input.zone == output.zone 826 | assert.true input.hello == output.hello 827 | 828 | it "transforms key & value literals", -> 829 | n = types.map_of 5, "hello" 830 | 831 | assert.same { 832 | { [5]: "hello" } 833 | }, { 834 | n\transform { 835 | [5]: "hello" 836 | } 837 | } 838 | 839 | assert.same { 840 | nil 841 | 'map value expected "hello"' 842 | }, { 843 | n\transform { 844 | [5]: "helloz" 845 | } 846 | } 847 | 848 | assert.same { 849 | nil 850 | "map key expected 5" 851 | }, { 852 | n\transform { 853 | "5": "hello" 854 | } 855 | } 856 | 857 | it "removies fields by transforming to nil", -> 858 | t = types.map_of types.string, types.number + types.any / nil 859 | 860 | assert.same { 861 | age: 10 862 | id: 99.9 863 | }, t\transform { 864 | color: "blue" 865 | age: 10 866 | id: 99.9 867 | } 868 | 869 | t = types.map_of types.string + types.any / nil, types.any 870 | 871 | assert.same { 872 | color: "blue" 873 | }, t\transform { 874 | color: "blue" 875 | [5]: 10 876 | [true]: "okay" 877 | } 878 | 879 | describe "tags", -> 880 | it "assigns tags when transforming", -> 881 | n = types.shape { 882 | (types.number / tostring + types.string)\tag "hello" 883 | (types.number / tostring + types.string)\tag "world" 884 | } 885 | 886 | assert.same { 887 | { 888 | "5" 889 | "world" 890 | } 891 | { 892 | hello: "5" 893 | world: "world" 894 | } 895 | }, { 896 | n\transform { 897 | 5 898 | "world" 899 | } 900 | } 901 | 902 | describe "clone", -> 903 | it "clones simple literals", -> 904 | assert.equal 5, types.clone\transform 5 905 | assert.equal true, types.clone\transform true 906 | assert.equal false, types.clone\transform false 907 | assert.equal nil, types.clone\transform nil 908 | assert.equal "hello world!", types.clone\transform "hello world!" 909 | 910 | it "clones simple tables", -> 911 | for input in *{ 912 | {} 913 | {1,2,3} 914 | {one: "two"} 915 | {hello: true, 4,5} 916 | } 917 | output = types.clone\transform input 918 | assert output != input, "new object should be returned with clone" 919 | assert.same output, input 920 | 921 | it "clones with nested tables (shallow)", -> 922 | input = { one: {} } 923 | output = types.clone\transform input 924 | assert output != input, "new object should be returned with clone" 925 | assert.same output, input 926 | assert input.one == output.one, "nested objects should be the same" 927 | 928 | it "clones table with metatable", -> 929 | mt = {} 930 | input = setmetatable { hi: true }, mt 931 | output = types.clone\transform input 932 | assert output != input, "new object should be returned with clone" 933 | assert.same output, input 934 | assert getmetatable(output) == mt, "metatables should be the same" 935 | 936 | -------------------------------------------------------------------------------- /tableshape-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "tableshape" 2 | version = "dev-1" 3 | 4 | source = { 5 | url = "git+https://github.com/leafo/tableshape.git", 6 | } 7 | 8 | description = { 9 | summary = "Test the shape or structure of a Lua table", 10 | homepage = "https://github.com/leafo/tableshape", 11 | license = "MIT" 12 | } 13 | 14 | dependencies = { 15 | "lua >= 5.1" 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | ["tableshape"] = "tableshape/init.lua", 22 | ["tableshape.luassert"] = "tableshape/luassert.lua", 23 | ["tableshape.moonscript"] = "tableshape/moonscript.lua", 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /tableshape.lua: -------------------------------------------------------------------------------- 1 | return require("tableshape.init") 2 | -------------------------------------------------------------------------------- /tableshape.moon: -------------------------------------------------------------------------------- 1 | require "tableshape.init" 2 | -------------------------------------------------------------------------------- /tableshape/init.moon: -------------------------------------------------------------------------------- 1 | local OptionalType, TaggedType, types, is_type 2 | local BaseType, TransformNode, SequenceNode, FirstOfNode, DescribeNode, NotType, Literal 3 | 4 | -- Naming convention 5 | -- Type: Something that checks the type/shape of something 6 | -- Node: Something that adds additional information or does an operation on existing type(s) 7 | 8 | -- unique object to identify failure case for return value from _transform 9 | FailedTransform = {} 10 | 11 | unpack = unpack or table.unpack 12 | 13 | -- make a clone of state for insertion 14 | clone_state = (state_obj) -> 15 | -- uninitialized state 16 | if type(state_obj) != "table" 17 | return {} 18 | 19 | -- shallow copy 20 | out = {k, v for k, v in pairs state_obj} 21 | if mt = getmetatable state_obj 22 | setmetatable out, mt 23 | 24 | out 25 | 26 | -- Either use the describe method of the type, or print the literal value 27 | describe_type = (val) -> 28 | if type(val) == "string" 29 | if not val\match '"' 30 | "\"#{val}\"" 31 | elseif not val\match "'" 32 | "'#{val}'" 33 | else 34 | "`#{val}`" 35 | elseif BaseType\is_base_type val 36 | val\_describe! 37 | else 38 | tostring val 39 | 40 | coerce_literal = (value) -> 41 | switch type value 42 | when "string", "number", "boolean" 43 | return Literal value 44 | when "table" 45 | -- already is a type 46 | if BaseType\is_base_type value 47 | return value 48 | 49 | nil, "failed to coerce literal into type, use types.literal() to test for literal value" 50 | 51 | join_names = (items, sep=", ", last_sep) -> 52 | count = #items 53 | chunks = {} 54 | for idx, name in ipairs items 55 | if idx > 1 56 | current_sep = if idx == count 57 | last_sep or sep 58 | else 59 | sep 60 | table.insert chunks, current_sep 61 | 62 | table.insert chunks, name 63 | 64 | table.concat chunks 65 | 66 | 67 | -- This is the base class that all types must inherit from. 68 | -- Implementing types must provide the following methods: 69 | -- _transform(value, state): => value, state 70 | -- Transform the value and state. No mutation must happen, return copies of 71 | -- values if they change. On failure return FailedTransform, "error message". 72 | -- Ensure that even on error no mutations happen to state or value. 73 | -- _describe(): => string 74 | -- Return a string describing what the type should expect to get. This is 75 | -- used to generate error messages for complex types that bail out of value 76 | -- specific error messages due to complexity. 77 | class BaseType 78 | -- detects if value is *instance* of base type 79 | @is_base_type: (val) => 80 | if mt = type(val) == "table" and getmetatable val 81 | if mt.__class 82 | return mt.__class.is_base_type == BaseType.is_base_type 83 | 84 | false 85 | 86 | @__inherited: (cls) => 87 | cls.__base.__call = cls.__call 88 | cls.__base.__div = @__div 89 | cls.__base.__mod = @__mod 90 | cls.__base.__mul = @__mul 91 | cls.__base.__add = @__add 92 | cls.__base.__unm = @__unm 93 | cls.__base.__tostring = @__tostring 94 | 95 | -- TODO: ensure things implement describe to prevent hard error when 96 | -- parsing inputs that don't pass the shape 97 | -- unless rawget cls.__base, "_describe" 98 | -- print "MISSING _describe", cls.__name 99 | 100 | __div: (fn) => 101 | TransformNode @, fn 102 | 103 | __mod: (fn) => 104 | with TransformNode @, fn 105 | .with_state = true 106 | 107 | __mul: (_left, _right) -> 108 | left, err = coerce_literal _left 109 | unless left 110 | error "left hand side of multiplication: #{_left}: #{err}" 111 | 112 | right, err = coerce_literal _right 113 | unless right 114 | error "right hand side of multiplication: #{_right}: #{err}" 115 | 116 | SequenceNode left, right 117 | 118 | __add: (_left, _right) -> 119 | left, err = coerce_literal _left 120 | unless left 121 | error "left hand side of addition: #{_left}: #{err}" 122 | 123 | right, err = coerce_literal _right 124 | unless right 125 | error "right hand side of addition: #{_right}: #{err}" 126 | 127 | if left.__class == FirstOfNode 128 | options = { unpack left.options } 129 | table.insert options, right 130 | FirstOfNode unpack options 131 | elseif right.__class == FirstOfNode 132 | FirstOfNode left, unpack right.options 133 | else 134 | FirstOfNode left, right 135 | 136 | __unm: (right) => 137 | NotType right 138 | 139 | __tostring: => 140 | @_describe! 141 | 142 | _describe: => 143 | error "Node missing _describe: #{@@__name}" 144 | 145 | new: (opts) => 146 | -- does nothing, implementing classes not expected to call super 147 | -- this is only here in case someone was calling super at some point 148 | 149 | -- test if value matches type, returns true on success 150 | -- if state is used, then the state object is returned instead 151 | check_value: (...) => 152 | value, state_or_err = @_transform ... 153 | 154 | if value == FailedTransform 155 | return nil, state_or_err 156 | 157 | if type(state_or_err) == "table" 158 | state_or_err 159 | else 160 | true 161 | 162 | transform: (...) => 163 | value, state_or_err = @_transform ... 164 | 165 | if value == FailedTransform 166 | return nil, state_or_err 167 | 168 | if type(state_or_err) == "table" 169 | value, state_or_err 170 | else 171 | value 172 | 173 | -- alias for transform 174 | repair: (...) => @transform ... 175 | 176 | on_repair: (fn) => 177 | (@ + types.any / fn * @)\describe -> @_describe! 178 | 179 | is_optional: => 180 | OptionalType @ 181 | 182 | describe: (...) => 183 | DescribeNode @, ... 184 | 185 | tag: (name) => 186 | TaggedType @, { 187 | tag: name 188 | } 189 | 190 | clone_opts: => 191 | error "clone_opts is not longer supported" 192 | 193 | __call: (...) => 194 | @check_value ... 195 | 196 | -- done with the division operator 197 | class TransformNode extends BaseType 198 | new: (@node, @t_fn) => 199 | assert @node, "missing node for transform" 200 | 201 | _describe: => 202 | @.node\_describe! 203 | 204 | _transform: (value, state) => 205 | value, state_or_err = @.node\_transform value, state 206 | 207 | if value == FailedTransform 208 | FailedTransform, state_or_err 209 | else 210 | out = switch type @.t_fn 211 | when "function" 212 | if @with_state 213 | @.t_fn(value, state_or_err) 214 | else 215 | @.t_fn(value) 216 | else 217 | @.t_fn 218 | 219 | out, state_or_err 220 | 221 | class SequenceNode extends BaseType 222 | new: (...) => 223 | @sequence = {...} 224 | 225 | _describe: => 226 | item_names = [describe_type i for i in *@sequence] 227 | join_names item_names, " then " 228 | 229 | _transform: (value, state) => 230 | for node in *@sequence 231 | value, state = node\_transform value, state 232 | if value == FailedTransform 233 | break 234 | 235 | value, state 236 | 237 | class FirstOfNode extends BaseType 238 | new: (...) => 239 | @options = {...} 240 | 241 | _describe: => 242 | item_names = [describe_type i for i in *@options] 243 | join_names item_names, ", ", ", or " 244 | 245 | _transform: (value, state) => 246 | unless @options[1] 247 | return FailedTransform, "no options for node" 248 | 249 | for node in *@options 250 | new_val, new_state = node\_transform value, state 251 | 252 | unless new_val == FailedTransform 253 | return new_val, new_state 254 | 255 | FailedTransform, "expected #{@_describe!}" 256 | 257 | class DescribeNode extends BaseType 258 | new: (@node, describe) => 259 | local err_message 260 | if type(describe) == "table" 261 | {type: describe, error: err_message} = describe 262 | 263 | @_describe = if type(describe) == "string" 264 | -> describe 265 | else 266 | describe 267 | 268 | @err_handler = if err_message 269 | if type(err_message) == "string" 270 | -> err_message 271 | else 272 | err_message 273 | 274 | _transform: (input, ...) => 275 | value, state = @node\_transform input, ... 276 | 277 | if value == FailedTransform 278 | err = if @err_handler 279 | @.err_handler input, state 280 | else 281 | "expected #{@_describe!}" 282 | 283 | return FailedTransform, err 284 | 285 | value, state 286 | 287 | describe: (...) => 288 | DescribeNode @node, ... 289 | 290 | -- annotates failures with the value that failed 291 | -- TODO: should this be part of describe? 292 | class AnnotateNode extends BaseType 293 | new: (base_type, opts) => 294 | @base_type = assert coerce_literal base_type 295 | if opts 296 | -- replace the format error method 297 | if opts.format_error 298 | @format_error = assert types.func\transform opts.format_error 299 | 300 | format_error: (value, err) => 301 | "#{tostring value}: #{err}" 302 | 303 | _transform: (value, state) => 304 | new_value, state_or_err = @base_type\_transform value, state 305 | if new_value == FailedTransform 306 | FailedTransform, @format_error value, state_or_err 307 | else 308 | new_value, state_or_err 309 | 310 | _describe: => 311 | if @base_type._describe 312 | @base_type\_describe! 313 | 314 | class TaggedType extends BaseType 315 | new: (@base_type, opts={}) => 316 | @tag_name = assert opts.tag, "tagged type missing tag" 317 | 318 | @tag_type = type @tag_name 319 | 320 | if @tag_type == "string" 321 | if @tag_name\match "%[%]$" 322 | @tag_name = @tag_name\sub 1, -3 323 | @tag_array = true 324 | 325 | update_state: (state, value, ...) => 326 | out = clone_state state 327 | 328 | if @tag_type == "function" 329 | if select("#", ...) > 0 330 | @.tag_name out, ..., value 331 | else 332 | @.tag_name out, value 333 | else 334 | if @tag_array 335 | existing = out[@tag_name] 336 | 337 | if type(existing) == "table" 338 | copy = {k,v for k,v in pairs existing} 339 | table.insert copy, value 340 | out[@tag_name] = copy 341 | else 342 | out[@tag_name] = { value } 343 | else 344 | out[@tag_name] = value 345 | 346 | out 347 | 348 | _transform: (value, state) => 349 | value, state = @base_type\_transform value, state 350 | 351 | if value == FailedTransform 352 | return FailedTransform, state 353 | 354 | state = @update_state state, value 355 | value, state 356 | 357 | _describe: => 358 | base_description = @base_type\_describe! 359 | "#{base_description} tagged #{describe_type @tag_name}" 360 | 361 | class TagScopeType extends TaggedType 362 | new: (base_type, opts) => 363 | if opts 364 | super base_type, opts 365 | else 366 | @base_type = base_type 367 | 368 | -- override to control how empty state is created for existing state 369 | create_scope_state: (state) => 370 | nil 371 | 372 | _transform: (value, state) => 373 | value, scope = @base_type\_transform value, @create_scope_state(state) 374 | 375 | if value == FailedTransform 376 | return FailedTransform, scope 377 | 378 | if @tag_name 379 | state = @update_state state, scope, value 380 | 381 | value, state 382 | 383 | class OptionalType extends BaseType 384 | new: (@base_type) => 385 | assert BaseType\is_base_type(@base_type), "expected a type checker" 386 | 387 | _transform: (value, state) => 388 | return value, state if value == nil 389 | @base_type\_transform value, state 390 | 391 | is_optional: => @ 392 | 393 | _describe: => 394 | if @base_type._describe 395 | base_description = @base_type\_describe! 396 | "optional #{base_description}" 397 | 398 | class AnyType extends BaseType 399 | _transform: (v, state) => v, state 400 | _describe: => "anything" 401 | 402 | -- any type is already optional (accepts nil) 403 | is_optional: => @ 404 | 405 | -- basic type check 406 | class Type extends BaseType 407 | new: (@t, opts) => 408 | if opts 409 | if opts.length 410 | @length_type = assert coerce_literal opts.length 411 | 412 | _transform: (value, state) => 413 | got = type(value) 414 | 415 | if @t != got 416 | return FailedTransform, "expected type #{describe_type @t}, got #{describe_type got}" 417 | 418 | if @length_type 419 | len = #value 420 | res, state = @length_type\_transform len, state 421 | 422 | if res == FailedTransform 423 | return FailedTransform, "#{@t} length #{state}, got #{len}" 424 | 425 | value, state 426 | 427 | -- creates a clone of this type with the length operator replaced 428 | length: (left, right) => 429 | l = if BaseType\is_base_type left 430 | left 431 | else 432 | types.range left, right 433 | 434 | Type @t, length: l 435 | 436 | _describe: => 437 | t = "type #{describe_type @t}" 438 | if @length_type 439 | t ..= " length_type #{@length_type\_describe!}" 440 | 441 | t 442 | 443 | class ArrayType extends BaseType 444 | _describe: => "an array" 445 | 446 | _transform: (value, state) => 447 | return FailedTransform, "expecting table" unless type(value) == "table" 448 | 449 | k = 1 450 | for i,v in pairs value 451 | unless type(i) == "number" 452 | return FailedTransform, "non number field: #{i}" 453 | 454 | unless i == k 455 | return FailedTransform, "non array index, got #{describe_type i} but expected #{describe_type k}" 456 | 457 | k += 1 458 | 459 | value, state 460 | 461 | class OneOf extends BaseType 462 | new: (@options) => 463 | assert type(@options) == "table", 464 | "expected table for options in one_of" 465 | 466 | -- optimize types 467 | fast_opts = types.array_of types.number + types.string 468 | if fast_opts @options 469 | @options_hash = {v, true for v in *@options} 470 | 471 | _describe: => 472 | item_names = for i in *@options 473 | if type(i) == "table" and i._describe 474 | i\_describe! 475 | else 476 | describe_type i 477 | 478 | "#{join_names item_names, ", ", ", or "}" 479 | 480 | _transform: (value, state) => 481 | if @options_hash 482 | if @options_hash[value] 483 | return value, state 484 | else 485 | for item in *@options 486 | return value, state if item == value 487 | 488 | if BaseType\is_base_type item 489 | new_value, new_state = item\_transform value, state 490 | continue if new_value == FailedTransform 491 | 492 | return new_value, new_state 493 | 494 | FailedTransform, "expected #{@_describe!}" 495 | 496 | class AllOf extends BaseType 497 | new: (@types) => 498 | assert type(@types) == "table", "expected table for first argument" 499 | 500 | for checker in *@types 501 | assert BaseType\is_base_type(checker), "all_of expects all type checkers" 502 | 503 | _describe: => 504 | item_names = [describe_type i for i in *@types] 505 | join_names item_names, " and " 506 | 507 | _transform: (value, state) => 508 | for t in *@types 509 | value, state = t\_transform value, state 510 | 511 | if value == FailedTransform 512 | return FailedTransform, state 513 | 514 | value, state 515 | 516 | class ArrayOf extends BaseType 517 | @type_err_message: "expecting table" 518 | 519 | new: (@expected, opts) => 520 | if opts 521 | @keep_nils = opts.keep_nils and true 522 | if opts.length 523 | @length_type = assert coerce_literal opts.length 524 | 525 | _describe: => 526 | "array of #{describe_type @expected}" 527 | 528 | _transform: (value, state) => 529 | pass, err = types.table value 530 | 531 | unless pass 532 | return FailedTransform, err 533 | 534 | if @length_type 535 | len = #value 536 | res, state = @length_type\_transform len, state 537 | if res == FailedTransform 538 | return FailedTransform, "array length #{state}, got #{len}" 539 | 540 | is_literal = not BaseType\is_base_type @expected 541 | 542 | local copy, k 543 | 544 | for idx, item in ipairs value 545 | skip_item = false 546 | 547 | transformed_item = if is_literal 548 | if @expected != item 549 | return FailedTransform, "array item #{idx}: expected #{describe_type @expected}" 550 | else 551 | item 552 | else 553 | item_val, state = @expected\_transform item, state 554 | 555 | if item_val == FailedTransform 556 | return FailedTransform, "array item #{idx}: #{state}" 557 | 558 | if item_val == nil and not @keep_nils 559 | skip_item = true 560 | else 561 | item_val 562 | 563 | if transformed_item != item or skip_item 564 | unless copy 565 | copy = [i for i in *value[1, idx - 1]] 566 | k = idx 567 | 568 | if copy and not skip_item 569 | copy[k] = transformed_item 570 | k += 1 571 | 572 | copy or value, state 573 | 574 | class ArrayContains extends BaseType 575 | @type_err_message: "expecting table" 576 | short_circuit: true 577 | keep_nils: false 578 | 579 | new: (@contains, opts) => 580 | assert @contains, "missing contains" 581 | 582 | if opts 583 | @short_circuit = opts.short_circuit and true 584 | @keep_nils = opts.keep_nils and true 585 | 586 | _describe: => 587 | "array containing #{describe_type @contains}" 588 | 589 | _transform: (value, state) => 590 | pass, err = types.table value 591 | 592 | unless pass 593 | return FailedTransform, err 594 | 595 | is_literal = not BaseType\is_base_type @contains 596 | 597 | contains = false 598 | 599 | local copy, k 600 | 601 | for idx, item in ipairs value 602 | skip_item = false 603 | 604 | transformed_item = if is_literal 605 | -- literal can't transform 606 | if @contains == item 607 | contains = true 608 | 609 | item 610 | else 611 | item_val, new_state = @contains\_transform item, state 612 | if item_val == FailedTransform 613 | item 614 | else 615 | state = new_state 616 | contains = true 617 | if item_val == nil and not @keep_nils 618 | skip_item = true 619 | else 620 | item_val 621 | 622 | if transformed_item != item or skip_item 623 | unless copy 624 | copy = [i for i in *value[1, idx - 1]] 625 | k = idx 626 | 627 | if copy and not skip_item 628 | copy[k] = transformed_item 629 | k += 1 630 | 631 | if contains and @short_circuit 632 | if copy 633 | -- copy the rest 634 | for kdx=idx+1,#value 635 | copy[k] = value[kdx] 636 | k += 1 637 | 638 | break 639 | 640 | unless contains 641 | return FailedTransform, "expected #{@_describe!}" 642 | 643 | copy or value, state 644 | 645 | 646 | class MapOf extends BaseType 647 | new: (expected_key, expected_value) => 648 | @expected_key = coerce_literal expected_key 649 | @expected_value = coerce_literal expected_value 650 | 651 | _describe: => 652 | "map of #{@expected_key\_describe!} -> #{@expected_value\_describe!}" 653 | 654 | _transform: (value, state) => 655 | pass, err = types.table value 656 | unless pass 657 | return FailedTransform, err 658 | 659 | key_literal = not BaseType\is_base_type @expected_key 660 | value_literal = not BaseType\is_base_type @expected_value 661 | 662 | transformed = false 663 | 664 | out = {} 665 | for k,v in pairs value 666 | new_k = k 667 | new_v = v 668 | 669 | if key_literal 670 | if k != @expected_key 671 | return FailedTransform, "map key expected #{describe_type @expected_key}" 672 | else 673 | new_k, state = @expected_key\_transform k, state 674 | if new_k == FailedTransform 675 | return FailedTransform, "map key #{state}" 676 | 677 | if value_literal 678 | if v != @expected_value 679 | return FailedTransform, "map value expected #{describe_type @expected_value}" 680 | else 681 | new_v, state = @expected_value\_transform v, state 682 | if new_v == FailedTransform 683 | return FailedTransform, "map value #{state}" 684 | 685 | if new_k != k or new_v != v 686 | transformed = true 687 | 688 | continue if new_k == nil 689 | out[new_k] = new_v 690 | 691 | transformed and out or value, state 692 | 693 | class Shape extends BaseType 694 | @type_err_message: "expecting table" 695 | open: false 696 | check_all: false 697 | 698 | new: (@shape, opts) => 699 | assert type(@shape) == "table", "expected table for shape" 700 | if opts 701 | if opts.extra_fields 702 | assert BaseType\is_base_type(opts.extra_fields), "extra_fields_type must be type checker" 703 | @extra_fields_type = opts.extra_fields 704 | 705 | @open = opts.open and true 706 | @check_all = opts.check_all and true 707 | 708 | if @open 709 | assert not @extra_fields_type, "open can not be combined with extra_fields" 710 | 711 | if @extra_fields_type 712 | assert not @open, "extra_fields can not be combined with open" 713 | 714 | -- NOTE: the extra_fields_type is stripped 715 | is_open: => 716 | Shape @shape, { 717 | open: true 718 | check_all: @check_all or nil 719 | } 720 | 721 | _describe: => 722 | parts = for k, v in pairs @shape 723 | "#{describe_type k} = #{describe_type v}" 724 | 725 | "{ #{table.concat parts, ", "} }" 726 | 727 | _transform: (value, state) => 728 | pass, err = types.table value 729 | unless pass 730 | return FailedTransform, err 731 | 732 | check_all = @check_all 733 | remaining_keys = {key, true for key in pairs value} 734 | 735 | local errors 736 | dirty = false 737 | out = {} 738 | 739 | for shape_key, shape_val in pairs @shape 740 | item_value = value[shape_key] 741 | 742 | if remaining_keys 743 | remaining_keys[shape_key] = nil 744 | 745 | new_val, state = if BaseType\is_base_type shape_val 746 | shape_val\_transform item_value, state 747 | else 748 | if shape_val == item_value 749 | item_value, state 750 | else 751 | FailedTransform, "expected #{describe_type shape_val}" 752 | 753 | if new_val == FailedTransform 754 | err = "field #{describe_type shape_key}: #{state}" 755 | if check_all 756 | if errors 757 | table.insert errors, err 758 | else 759 | errors = {err} 760 | else 761 | return FailedTransform, err 762 | else 763 | if new_val != item_value 764 | dirty = true 765 | 766 | out[shape_key] = new_val 767 | 768 | if remaining_keys and next remaining_keys 769 | if @open 770 | -- copy the remaining keys to out 771 | for k in pairs remaining_keys 772 | out[k] = value[k] 773 | elseif @extra_fields_type 774 | for k in pairs remaining_keys 775 | item_value = value[k] 776 | tuple, state = @extra_fields_type\_transform {[k]: item_value}, state 777 | if tuple == FailedTransform 778 | err = "field #{describe_type k}: #{state}" 779 | if check_all 780 | if errors 781 | table.insert errors, err 782 | else 783 | errors = {err} 784 | else 785 | return FailedTransform, err 786 | else 787 | if nk = tuple and next tuple 788 | -- the tuple key changed 789 | if nk != k 790 | dirty = true 791 | -- the value changed 792 | elseif tuple[nk] != item_value 793 | dirty = true 794 | 795 | out[nk] = tuple[nk] 796 | else 797 | -- value was removed, dirty 798 | dirty = true 799 | else 800 | names = for key in pairs remaining_keys 801 | describe_type key 802 | 803 | err = "extra fields: #{table.concat names, ", "}" 804 | 805 | if check_all 806 | if errors 807 | table.insert errors, err 808 | else 809 | errors = {err} 810 | else 811 | return FailedTransform, err 812 | 813 | if errors and next errors 814 | return FailedTransform, table.concat errors, "; " 815 | 816 | dirty and out or value, state 817 | 818 | class Partial extends Shape 819 | open: true 820 | 821 | is_open: => 822 | error "is_open has no effect on Partial" 823 | 824 | class Pattern extends BaseType 825 | new: (@pattern, opts) => 826 | -- TODO: we could support an lpeg object, or something that implements a match method 827 | assert type(@pattern) == "string", "Pattern must be a string" 828 | 829 | if opts 830 | @coerce = opts.coerce 831 | assert opts.initial_type == nil, "initial_type has been removed from types.pattern (got: #{opts.initial_type})" 832 | 833 | _describe: => 834 | "pattern #{describe_type @pattern}" 835 | 836 | -- TODO: should we remove coerce? it can be done with operators 837 | _transform: (value, state) => 838 | -- the value to match against, but not the value returned 839 | test_value = if @coerce 840 | if BaseType\is_base_type @coerce 841 | c_res, err = @coerce\_transform value 842 | 843 | if c_res == FailedTransform 844 | return FailedTransform, err 845 | 846 | c_res 847 | else 848 | tostring value 849 | else 850 | value 851 | 852 | t_res, err = types.string test_value 853 | 854 | unless t_res 855 | return FailedTransform, err 856 | 857 | if test_value\match @pattern 858 | value, state 859 | else 860 | FailedTransform, "doesn't match #{@_describe!}" 861 | 862 | class Literal extends BaseType 863 | new: (@value) => 864 | 865 | _describe: => 866 | describe_type @value 867 | 868 | _transform: (value, state) => 869 | if @value != value 870 | return FailedTransform, "expected #{@_describe!}" 871 | 872 | value, state 873 | 874 | class Custom extends BaseType 875 | new: (@fn) => 876 | assert type(@fn) == "function", "custom checker must be a function" 877 | 878 | _describe: => 879 | "custom checker #{@fn}" 880 | 881 | _transform: (value, state) => 882 | pass, err = @.fn value, state 883 | 884 | unless pass 885 | return FailedTransform, err or "failed custom check" 886 | 887 | value, state 888 | 889 | class Equivalent extends BaseType 890 | values_equivalent = (a,b) -> 891 | return true if a == b 892 | 893 | if type(a) == "table" and type(b) == "table" 894 | seen_keys = {} 895 | 896 | for k,v in pairs a 897 | seen_keys[k] = true 898 | return false unless values_equivalent v, b[k] 899 | 900 | for k,v in pairs b 901 | continue if seen_keys[k] 902 | return false unless values_equivalent v, a[k] 903 | 904 | true 905 | else 906 | false 907 | 908 | new: (@val) => 909 | 910 | _describe: => 911 | "equivalent to #{describe_type @val}" 912 | 913 | _transform: (value, state) => 914 | if values_equivalent @val, value 915 | value, state 916 | else 917 | FailedTransform, "not equivalent to #{@val}" 918 | 919 | class Range extends BaseType 920 | new: (@left, @right) => 921 | assert @left <= @right, "left range value should be less than right range value" 922 | @value_type = assert types[type(@left)], "couldn't figure out type of range boundary" 923 | 924 | _transform: (value, state) => 925 | res, state = @.value_type\_transform value, state 926 | 927 | if res == FailedTransform 928 | return FailedTransform, "range #{state}" 929 | 930 | if value < @left 931 | return FailedTransform, "not in #{@_describe!}" 932 | 933 | if value > @right 934 | return FailedTransform, "not in #{@_describe!}" 935 | 936 | value, state 937 | 938 | _describe: => 939 | "range from #{@left} to #{@right}" 940 | 941 | class Proxy extends BaseType 942 | new: (@fn) => 943 | 944 | _transform: (...) => 945 | assert(@.fn!, "proxy missing transformer")\_transform ... 946 | 947 | _describe: (...) => 948 | assert(@.fn!, "proxy missing transformer")\_describe ... 949 | 950 | class AssertType extends BaseType 951 | assert: assert 952 | 953 | new: (@base_type) => 954 | assert BaseType\is_base_type(@base_type), "expected a type checker" 955 | 956 | _transform: (value, state) => 957 | value, state_or_err = @base_type\_transform value, state 958 | @.assert value != FailedTransform, state_or_err 959 | value, state_or_err 960 | 961 | _describe: => 962 | if @base_type._describe 963 | base_description = @base_type\_describe! 964 | "assert #{base_description}" 965 | 966 | class NotType extends BaseType 967 | new: (@base_type) => 968 | assert BaseType\is_base_type(@base_type), "expected a type checker" 969 | 970 | _transform: (value, state) => 971 | out, _ = @base_type\_transform value, state 972 | if out == FailedTransform 973 | value, state 974 | else 975 | FailedTransform, "expected #{@_describe!}" 976 | 977 | _describe: => 978 | if @base_type._describe 979 | base_description = @base_type\_describe! 980 | "not #{base_description}" 981 | 982 | class CloneType extends BaseType 983 | _transform: (value, state) => 984 | switch type value 985 | -- literals that don't need cloning 986 | when "nil", "string", "number", "boolean" 987 | return value, state 988 | when "table" 989 | -- shallow copy 990 | clone_value = {k, v for k, v in pairs value} 991 | if mt = getmetatable value 992 | setmetatable clone_value, mt 993 | 994 | return clone_value, state 995 | else 996 | return FailedTransform, "#{describe_type value} is not cloneable" 997 | 998 | _describe: => 999 | "cloneable value" 1000 | 1001 | class MetatableIsType extends BaseType 1002 | allow_metatable_update: false 1003 | 1004 | new: (metatable_type, opts) => 1005 | @metatable_type = if BaseType\is_base_type metatable_type 1006 | metatable_type 1007 | else 1008 | Literal metatable_type 1009 | 1010 | if opts 1011 | @allow_metatable_update = opts.allow_metatable_update and true 1012 | 1013 | _transform: (value, state) => 1014 | -- verify that type is a table 1015 | value, state_or_err = types.table\_transform value, state 1016 | if value == FailedTransform 1017 | return FailedTransform, state_or_err 1018 | 1019 | mt = getmetatable value 1020 | new_mt, state_or_err = @metatable_type\_transform mt, state_or_err 1021 | 1022 | if new_mt == FailedTransform 1023 | return FailedTransform, "metatable expected: #{state_or_err}" 1024 | 1025 | if new_mt != mt 1026 | if @allow_metatable_update 1027 | setmetatable value, new_mt 1028 | else 1029 | -- NOTE: changing a metatable is unsafe since if a parent type ends up 1030 | -- failing validation we can not undo the change. The only safe way to 1031 | -- avoid the issue would be to shallow clone value but that may come 1032 | -- with it's own consquences. Hence, you must explicitly enable 1033 | -- metatable mutation, and you should probably pass a clone into the 1034 | -- transform: types.clone * types.metatable_is 1035 | return FailedTransform, "metatable was modified by a type but { allow_metatable_update = true } is not enabled" 1036 | 1037 | value, state_or_err 1038 | 1039 | _describe: => 1040 | "has metatable #{describe_type @metatable_type}" 1041 | 1042 | 1043 | type_nil = Type "nil" 1044 | type_function = Type "function" 1045 | type_number = Type "number" 1046 | 1047 | types = setmetatable { 1048 | any: AnyType! 1049 | string: Type "string" 1050 | number: type_number 1051 | function: type_function 1052 | func: type_function 1053 | boolean: Type "boolean" 1054 | userdata: Type "userdata" 1055 | nil: type_nil 1056 | null: type_nil 1057 | table: Type "table" 1058 | array: ArrayType! 1059 | clone: CloneType! 1060 | 1061 | -- compound 1062 | integer: Pattern "^%d+$", coerce: type_number / tostring 1063 | 1064 | -- type constructors 1065 | one_of: OneOf 1066 | all_of: AllOf 1067 | shape: Shape 1068 | partial: Partial 1069 | pattern: Pattern 1070 | array_of: ArrayOf 1071 | array_contains: ArrayContains 1072 | map_of: MapOf 1073 | literal: Literal 1074 | range: Range 1075 | equivalent: Equivalent 1076 | custom: Custom 1077 | scope: TagScopeType 1078 | proxy: Proxy 1079 | assert: AssertType 1080 | annotate: AnnotateNode 1081 | metatable_is: MetatableIsType 1082 | }, __index: (fn_name) => 1083 | error "Type checker does not exist: `#{fn_name}`" 1084 | 1085 | check_shape = (value, shape) -> 1086 | assert shape.check_value, "missing check_value method from shape" 1087 | shape\check_value value 1088 | 1089 | is_type = (val) -> 1090 | BaseType\is_base_type val 1091 | 1092 | { :check_shape, :types, :is_type, :BaseType, :FailedTransform, VERSION: "2.6.0" } 1093 | -------------------------------------------------------------------------------- /tableshape/luassert.lua: -------------------------------------------------------------------------------- 1 | local say = require("say") 2 | local assert = require("luassert") 3 | say:set("assertion.shape.positive", "Expected %s to match shape:\n%s") 4 | say:set("assertion.shape.negative", "Expected %s to not match shape:\n%s") 5 | assert:register("assertion", "shape", function(state, arguments) 6 | local input, expected 7 | input, expected = arguments[1], arguments[2] 8 | assert(is_type(expected), "Expected tableshape type for second argument to assert.shape") 9 | if expected(input) then 10 | return true 11 | else 12 | return false 13 | end 14 | end, "assertion.shape.positive", "assertion.shape.negative") 15 | assert:add_formatter(function(v) 16 | if is_type(v) then 17 | return tostring(v) 18 | end 19 | end) 20 | return true 21 | -------------------------------------------------------------------------------- /tableshape/luassert.moon: -------------------------------------------------------------------------------- 1 | 2 | -- this installs luassert assertion and formatter for tableshape types 3 | 4 | say = require "say" 5 | assert = require "luassert" 6 | 7 | say\set "assertion.shape.positive", 8 | "Expected %s to match shape:\n%s" 9 | 10 | say\set "assertion.shape.negative", 11 | "Expected %s to not match shape:\n%s" 12 | 13 | assert\register( 14 | "assertion", 15 | "shape" 16 | 17 | (state, arguments) -> 18 | { input, expected } = arguments 19 | assert is_type(expected), "Expected tableshape type for second argument to assert.shape" 20 | if expected input 21 | true 22 | else 23 | false 24 | 25 | "assertion.shape.positive" 26 | "assertion.shape.negative" 27 | ) 28 | 29 | assert\add_formatter (v) -> 30 | if is_type v 31 | return tostring v 32 | 33 | true 34 | -------------------------------------------------------------------------------- /tableshape/moonscript.lua: -------------------------------------------------------------------------------- 1 | local BaseType, FailedTransform 2 | do 3 | local _obj_0 = require("tableshape") 4 | BaseType, FailedTransform = _obj_0.BaseType, _obj_0.FailedTransform 5 | end 6 | local ClassType 7 | do 8 | local _class_0 9 | local _parent_0 = BaseType 10 | local _base_0 = { 11 | _transform = function(self, value, state) 12 | if not (type(value) == "table") then 13 | return FailedTransform, "expecting table" 14 | end 15 | local base = value.__base 16 | if not (base) then 17 | return FailedTransform, "table is not class (missing __base)" 18 | end 19 | if not (type(base) == "table") then 20 | return FailedTransform, "table is not class (__base not table)" 21 | end 22 | local mt = getmetatable(value) 23 | if not (mt) then 24 | return FailedTransform, "table is not class (missing metatable)" 25 | end 26 | if not (mt.__call) then 27 | return FailedTransform, "table is not class (no constructor)" 28 | end 29 | return value, state 30 | end, 31 | _describe = function(self) 32 | return "class" 33 | end 34 | } 35 | _base_0.__index = _base_0 36 | setmetatable(_base_0, _parent_0.__base) 37 | _class_0 = setmetatable({ 38 | __init = function(self, ...) 39 | return _class_0.__parent.__init(self, ...) 40 | end, 41 | __base = _base_0, 42 | __name = "ClassType", 43 | __parent = _parent_0 44 | }, { 45 | __index = function(cls, name) 46 | local val = rawget(_base_0, name) 47 | if val == nil then 48 | local parent = rawget(cls, "__parent") 49 | if parent then 50 | return parent[name] 51 | end 52 | else 53 | return val 54 | end 55 | end, 56 | __call = function(cls, ...) 57 | local _self_0 = setmetatable({}, _base_0) 58 | cls.__init(_self_0, ...) 59 | return _self_0 60 | end 61 | }) 62 | _base_0.__class = _class_0 63 | if _parent_0.__inherited then 64 | _parent_0.__inherited(_parent_0, _class_0) 65 | end 66 | ClassType = _class_0 67 | end 68 | local InstanceType 69 | do 70 | local _class_0 71 | local _parent_0 = BaseType 72 | local _base_0 = { 73 | _transform = function(self, value, state) 74 | if not (type(value) == "table") then 75 | return FailedTransform, "expecting table" 76 | end 77 | local mt = getmetatable(value) 78 | if not (mt) then 79 | return FailedTransform, "table is not instance (missing metatable)" 80 | end 81 | local cls = rawget(mt, "__class") 82 | if not (cls) then 83 | return FailedTransform, "table is not instance (metatable does not have __class)" 84 | end 85 | return value, state 86 | end, 87 | _describe = function(self) 88 | return "instance" 89 | end 90 | } 91 | _base_0.__index = _base_0 92 | setmetatable(_base_0, _parent_0.__base) 93 | _class_0 = setmetatable({ 94 | __init = function(self, ...) 95 | return _class_0.__parent.__init(self, ...) 96 | end, 97 | __base = _base_0, 98 | __name = "InstanceType", 99 | __parent = _parent_0 100 | }, { 101 | __index = function(cls, name) 102 | local val = rawget(_base_0, name) 103 | if val == nil then 104 | local parent = rawget(cls, "__parent") 105 | if parent then 106 | return parent[name] 107 | end 108 | else 109 | return val 110 | end 111 | end, 112 | __call = function(cls, ...) 113 | local _self_0 = setmetatable({}, _base_0) 114 | cls.__init(_self_0, ...) 115 | return _self_0 116 | end 117 | }) 118 | _base_0.__class = _class_0 119 | if _parent_0.__inherited then 120 | _parent_0.__inherited(_parent_0, _class_0) 121 | end 122 | InstanceType = _class_0 123 | end 124 | local SubclassOf 125 | do 126 | local _class_0 127 | local _parent_0 = BaseType 128 | local _base_0 = { 129 | _transform = function(self, value, state) 130 | local out, err = ClassType._transform(nil, value, state) 131 | if out == FailedTransform then 132 | return FailedTransform, err 133 | end 134 | local current_class 135 | if self.allow_same then 136 | current_class = value 137 | else 138 | current_class = value.__parent 139 | end 140 | if type(self.class_identifier) == "string" then 141 | while current_class do 142 | if current_class.__name == self.class_identifier then 143 | return value, state 144 | end 145 | current_class = current_class.__parent 146 | end 147 | else 148 | while current_class do 149 | if current_class == self.class_identifier then 150 | return value, state 151 | end 152 | current_class = current_class.__parent 153 | end 154 | end 155 | return FailedTransform, "table is not " .. tostring(self:_describe()) 156 | end, 157 | _describe = function(self) 158 | local name 159 | if type(self.class_identifier) == "string" then 160 | name = self.class_identifier 161 | else 162 | name = self.class_identifier.__name or "Class" 163 | end 164 | return "subclass of " .. tostring(name) 165 | end 166 | } 167 | _base_0.__index = _base_0 168 | setmetatable(_base_0, _parent_0.__base) 169 | _class_0 = setmetatable({ 170 | __init = function(self, class_identifier, opts) 171 | self.class_identifier = class_identifier 172 | if opts and opts.allow_same then 173 | self.allow_same = true 174 | else 175 | self.allow_same = false 176 | end 177 | return assert(self.class_identifier, "expecting class identifier (string or class object)") 178 | end, 179 | __base = _base_0, 180 | __name = "SubclassOf", 181 | __parent = _parent_0 182 | }, { 183 | __index = function(cls, name) 184 | local val = rawget(_base_0, name) 185 | if val == nil then 186 | local parent = rawget(cls, "__parent") 187 | if parent then 188 | return parent[name] 189 | end 190 | else 191 | return val 192 | end 193 | end, 194 | __call = function(cls, ...) 195 | local _self_0 = setmetatable({}, _base_0) 196 | cls.__init(_self_0, ...) 197 | return _self_0 198 | end 199 | }) 200 | _base_0.__class = _class_0 201 | if _parent_0.__inherited then 202 | _parent_0.__inherited(_parent_0, _class_0) 203 | end 204 | SubclassOf = _class_0 205 | end 206 | local InstanceOf 207 | do 208 | local _class_0 209 | local _parent_0 = BaseType 210 | local _base_0 = { 211 | _transform = function(self, value, state) 212 | local out, err = InstanceType._transform(nil, value, state) 213 | if out == FailedTransform then 214 | return FailedTransform, err 215 | end 216 | local cls = value.__class 217 | if type(self.class_identifier) == "string" then 218 | local current_cls = cls 219 | while current_cls do 220 | if current_cls.__name == self.class_identifier then 221 | return value, state 222 | end 223 | current_cls = current_cls.__parent 224 | end 225 | else 226 | local current_cls = cls 227 | while current_cls do 228 | if current_cls == self.class_identifier then 229 | return value, state 230 | end 231 | current_cls = current_cls.__parent 232 | end 233 | end 234 | return FailedTransform, "table is not " .. tostring(self:_describe()) 235 | end, 236 | _describe = function(self) 237 | local name 238 | if type(self.class_identifier) == "string" then 239 | name = self.class_identifier 240 | else 241 | name = self.class_identifier.__name or "Class" 242 | end 243 | return "instance of " .. tostring(name) 244 | end 245 | } 246 | _base_0.__index = _base_0 247 | setmetatable(_base_0, _parent_0.__base) 248 | _class_0 = setmetatable({ 249 | __init = function(self, class_identifier) 250 | self.class_identifier = class_identifier 251 | return assert(self.class_identifier, "expecting class identifier (string or class object)") 252 | end, 253 | __base = _base_0, 254 | __name = "InstanceOf", 255 | __parent = _parent_0 256 | }, { 257 | __index = function(cls, name) 258 | local val = rawget(_base_0, name) 259 | if val == nil then 260 | local parent = rawget(cls, "__parent") 261 | if parent then 262 | return parent[name] 263 | end 264 | else 265 | return val 266 | end 267 | end, 268 | __call = function(cls, ...) 269 | local _self_0 = setmetatable({}, _base_0) 270 | cls.__init(_self_0, ...) 271 | return _self_0 272 | end 273 | }) 274 | _base_0.__class = _class_0 275 | if _parent_0.__inherited then 276 | _parent_0.__inherited(_parent_0, _class_0) 277 | end 278 | InstanceOf = _class_0 279 | end 280 | return setmetatable({ 281 | class_type = ClassType(), 282 | instance_type = InstanceType(), 283 | instance_of = InstanceOf, 284 | subclass_of = SubclassOf 285 | }, { 286 | __index = function(self, fn_name) 287 | return error("Type checker does not exist: `" .. tostring(fn_name) .. "`") 288 | end 289 | }) 290 | -------------------------------------------------------------------------------- /tableshape/moonscript.moon: -------------------------------------------------------------------------------- 1 | import BaseType, FailedTransform from require "tableshape" 2 | 3 | class ClassType extends BaseType 4 | _transform: (value, state) => 5 | unless type(value) == "table" 6 | return FailedTransform, "expecting table" 7 | 8 | base = value.__base 9 | unless base 10 | return FailedTransform, "table is not class (missing __base)" 11 | 12 | unless type(base) == "table" 13 | return FailedTransform, "table is not class (__base not table)" 14 | 15 | mt = getmetatable value 16 | unless mt 17 | return FailedTransform, "table is not class (missing metatable)" 18 | 19 | unless mt.__call 20 | return FailedTransform, "table is not class (no constructor)" 21 | 22 | value, state 23 | 24 | _describe: => 25 | "class" 26 | 27 | class InstanceType extends BaseType 28 | _transform: (value, state) => 29 | unless type(value) == "table" 30 | return FailedTransform, "expecting table" 31 | 32 | mt = getmetatable value 33 | 34 | unless mt 35 | return FailedTransform, "table is not instance (missing metatable)" 36 | 37 | cls = rawget mt, "__class" 38 | unless cls 39 | return FailedTransform, "table is not instance (metatable does not have __class)" 40 | 41 | value, state 42 | 43 | _describe: => 44 | "instance" 45 | 46 | 47 | class SubclassOf extends BaseType 48 | new: (@class_identifier, opts) => 49 | @allow_same = if opts and opts.allow_same 50 | true 51 | else 52 | false 53 | 54 | assert @class_identifier, "expecting class identifier (string or class object)" 55 | 56 | _transform: (value, state) => 57 | out, err = ClassType._transform nil, value, state 58 | if out == FailedTransform 59 | return FailedTransform, err 60 | 61 | current_class = if @allow_same 62 | value 63 | else 64 | value.__parent 65 | 66 | if type(@class_identifier) == "string" 67 | while current_class 68 | if current_class.__name == @class_identifier 69 | return value, state 70 | 71 | current_class = current_class.__parent 72 | else 73 | while current_class 74 | if current_class == @class_identifier 75 | return value, state 76 | 77 | current_class = current_class.__parent 78 | 79 | FailedTransform, "table is not #{@_describe!}" 80 | 81 | _describe: => 82 | name = if type(@class_identifier) == "string" 83 | @class_identifier 84 | else 85 | @class_identifier.__name or "Class" 86 | 87 | "subclass of #{name}" 88 | 89 | class InstanceOf extends BaseType 90 | new: (@class_identifier) => 91 | assert @class_identifier, "expecting class identifier (string or class object)" 92 | 93 | _transform: (value, state) => 94 | out, err = InstanceType._transform nil, value, state 95 | if out == FailedTransform 96 | return FailedTransform, err 97 | 98 | cls = value.__class 99 | 100 | if type(@class_identifier) == "string" 101 | current_cls = cls 102 | while current_cls 103 | if current_cls.__name == @class_identifier 104 | return value, state 105 | 106 | current_cls = current_cls.__parent 107 | else 108 | current_cls = cls 109 | while current_cls 110 | if current_cls == @class_identifier 111 | return value, state 112 | 113 | current_cls = current_cls.__parent 114 | 115 | FailedTransform, "table is not #{@_describe!}" 116 | 117 | _describe: => 118 | name = if type(@class_identifier) == "string" 119 | @class_identifier 120 | else 121 | @class_identifier.__name or "Class" 122 | 123 | "instance of #{name}" 124 | 125 | setmetatable { 126 | class_type: ClassType! 127 | instance_type: InstanceType! 128 | 129 | instance_of: InstanceOf 130 | subclass_of: SubclassOf 131 | }, __index: (fn_name) => 132 | error "Type checker does not exist: `#{fn_name}`" 133 | --------------------------------------------------------------------------------