├── .babelrc ├── .eslintrc.json ├── .flowconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── flow_tests.js ├── package.json ├── src ├── index.js └── reporters │ ├── Reporter.js │ └── default.js └── test ├── $exact.js ├── array.js ├── classOf.js ├── helpers.js ├── instanceOf.js ├── intersection.js ├── literal.js ├── mapping.js ├── maybe.js ├── object.js ├── recursion.js ├── refinement.js ├── tuple.js ├── union.js └── unsafeValidate.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins" : [ 4 | "syntax-flow", 5 | "transform-flow-strip-types", 6 | "transform-class-properties", 7 | "transform-object-rest-spread" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["eslint:recommended", "plugin:flowtype/recommended"], 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 6, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "jsx": true 13 | } 14 | }, 15 | "plugins": [ 16 | "flowtype" 17 | ], 18 | "rules": { 19 | "semi": [2, "never"], 20 | "flowtype/semi": [2, "always"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | suppress_comment=\\(.\\|\n\\)*\\$ExpectError 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | dev 3 | lib 4 | node_modules 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | before_script: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | > **Tags:** 4 | > - [New Feature] 5 | > - [Bug Fix] 6 | > - [Breaking Change] 7 | > - [Documentation] 8 | > - [Internal] 9 | > - [Polish] 10 | > - [Experimental] 11 | 12 | **Note**: Gaps between patch versions are faulty/broken releases. 13 | **Note**: A feature tagged as Experimental is in a high state of flux, you're at risk of it changing without notice. 14 | 15 | ## 0.1.0 16 | 17 | Initial release 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Giulio Canti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This package is deprecated 2 | 3 | Checkout [io-ts](https://github.com/gcanti/io-ts) 4 | 5 | For Flow projects, checkout [io-flow-types](https://github.com/orlandoc01/io-flow-types) [in progress] 6 | 7 | # The idea 8 | 9 | A value of type `Type` (called "runtime type") is a representation of the type `T`: 10 | 11 | ```js 12 | class Type { 13 | name: string; 14 | validate: (value: mixed, context: Context) => Validation; 15 | } 16 | ``` 17 | 18 | where `Context` and `Validation` are defined as 19 | 20 | ```js 21 | type ContextEntry = { key: string, name: string }; 22 | type Context = Array; 23 | type ValidationError = { value: mixed, context: Context, description: string }; 24 | type Validation = Either, T>; 25 | ``` 26 | 27 | Example: a runtime type representing the type `string` is 28 | 29 | ```js 30 | import * as t from 'flow-io' 31 | 32 | export const string: Type = new Type( 33 | 'string', 34 | (v, c) => typeof v === 'string' ? t.success(v) : t.failure(v, c) 35 | ) 36 | ``` 37 | 38 | A runtime type can be used to validate an object in memory (for example an API payload) 39 | 40 | ```js 41 | import * as t from 'flow-io' 42 | 43 | const Person = t.object({ 44 | name: t.string, 45 | age: t.number 46 | }) 47 | 48 | // ok 49 | t.fromValidation(JSON.parse('{"name":"Giulio","age":43}'), Person) // => {name: "Giulio", age: 43} 50 | 51 | // throws Invalid value undefined supplied to : { name: string, age: number }/age: number 52 | t.fromValidation(JSON.parse('{"name":"Giulio"}'), Person) 53 | 54 | // doesn't throw, returns an Either 55 | const validation = t.validate(JSON.parse('{"name":"Giulio"}'), Person) 56 | t.map(person => console.log(person), validation) 57 | ``` 58 | 59 | # Runtime type introspection 60 | 61 | Runtime types can be inspected 62 | 63 | ```js 64 | const Name: Type = Person.props.name 65 | const Age: Type = Person.props.age 66 | ``` 67 | 68 | # Error reporters 69 | 70 | A reporter implements the following interface 71 | 72 | ```js 73 | export interface Reporter { 74 | report: (validation: Validation<*>) => A; 75 | } 76 | ``` 77 | 78 | This package exports two default reporters 79 | 80 | - `PathReporter: Reporter>` 81 | - `ThrowReporter: Reporter` 82 | 83 | Example 84 | 85 | ```js 86 | import * as t from 'flow-io' 87 | import { PathReporter, ThrowReporter } from 'flow-io/lib/reporters/default' 88 | 89 | const validation = t.validate('a', t.number) 90 | console.log(PathReporter.report(validation)) // => ["Invalid value "a" supplied to : number"] 91 | ThrowReporter.report(validation) // => throws Invalid value "a" supplied to : number 92 | ``` 93 | 94 | # Implemented types / combinators 95 | 96 | | Type | Flow syntax | Runtime type / combinator | 97 | |------|-------|-------------| 98 | | string | `string` | `string` | 99 | | number | `number` | `number` | 100 | | boolean | `boolean` | `boolean` | 101 | | generic object | `Object` | `Object` | 102 | | generic function | `Function` | `Function` | 103 | | instance of `C` | `C` | `instanceOf(C)` | 104 | | class of `C` | `Class` | `classOf(C)` | 105 | | array | `Array` | `array(A)` | 106 | | intersection | `A & B` | `intersection([A, B])` | 107 | | literal | `'s'` | `literal('s')` | 108 | | maybe | `?A` | `maybe(A)` | 109 | | map | `{ [key: A]: B }` | `mapping(A, B)` | 110 | | refinement | ✘ | `refinement(A, predicate)` | 111 | | object | `{ name: string }` | `object({ name: string })` | 112 | | tuple | `[A, B]` | `tuple([A, B])` | 113 | | union | `A | B` | `union([A, B])` | 114 | | $Exact | `{| name: string |}` | `$exact({ name: string })` | 115 | | function | `(a: A) => B` | ✘ | 116 | -------------------------------------------------------------------------------- /flow_tests.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { 4 | ContextEntry, 5 | Context, 6 | ValidationError, 7 | Validation, 8 | Validate, 9 | TypeOf, 10 | Predicate, 11 | Props 12 | } from './src/index' 13 | 14 | import { 15 | Type, 16 | LiteralType, 17 | InstanceOfType, 18 | ClassType, 19 | ArrayType, 20 | UnionType, 21 | TupleType, 22 | IntersectionType, 23 | MaybeType, 24 | MappingType, 25 | RefinementType, 26 | $ExactType, 27 | ObjectType, 28 | } from './src/index' 29 | 30 | import * as t from './src/index' 31 | 32 | // 33 | // irreducibles 34 | // 35 | 36 | const T1 = t.number 37 | t.map(v1 => { 38 | (v1: number) 39 | ;(v1: TypeOf) 40 | // $ExpectError 41 | ;(v1: string) 42 | }, t.validate(1, T1)) 43 | // $ExpectError 44 | ;('a': TypeOf) 45 | 46 | // runtime type introspection 47 | const RTI1 = t.number 48 | ;(RTI1.name: string) 49 | 50 | // 51 | // instanceOf 52 | // 53 | 54 | class A {} 55 | const T2 = t.instanceOf(A) 56 | t.map(v2 => { 57 | (v2: A) 58 | ;(v2: TypeOf) 59 | // $ExpectError 60 | ;(v2: string) 61 | }, t.validate(new A(), T2)) 62 | // $ExpectError 63 | ;(1: TypeOf) 64 | 65 | // runtime type introspection 66 | const RTI2 = t.instanceOf(A) 67 | ;(RTI2.name: string) 68 | ;(RTI2.ctor: Class) 69 | 70 | // 71 | // literals 72 | // 73 | 74 | const T3 = t.literal('a') 75 | t.map(v3 => { 76 | (v3: 'a') 77 | ;(v3: TypeOf) 78 | // $ExpectError 79 | ;(v3: 'b') 80 | }, t.validate('a', T3)) 81 | // $ExpectError 82 | ;(1: TypeOf) 83 | ;('b': TypeOf) 84 | 85 | // runtime type introspection 86 | const RTI3 = t.literal('a') 87 | ;(RTI3.name: string) 88 | ;(RTI3.value: string) 89 | 90 | // 91 | // arrays 92 | // 93 | 94 | const T4 = t.array(t.number) 95 | t.map(v4 => { 96 | (v4: Array) 97 | ;(v4: TypeOf) 98 | // $ExpectError 99 | ;(v4: Array) 100 | }, t.validate([1, 2, 3], T4)) 101 | // $ExpectError 102 | ;(1: TypeOf) 103 | // $ExpectError 104 | ;(['a']: TypeOf) 105 | 106 | // runtime type introspection 107 | const RTI4 = t.array(t.object({ a: t.number })) 108 | ;(RTI4.name: string) 109 | ;(RTI4.type: Type<{ a: number }>) 110 | 111 | // 112 | // unions 113 | // 114 | 115 | const T5 = t.union([t.string, t.number]) 116 | t.map(v5 => { 117 | (v5: string | number) 118 | ;(v5: TypeOf) 119 | // $ExpectError 120 | ;(v5: string) 121 | }, t.validate(1, T5)) 122 | // $ExpectError 123 | ;(true: TypeOf) 124 | 125 | // runtime type introspection 126 | const RTI5 = t.union([t.string, t.object({ a: t.number })]) 127 | ;(RTI5.name: string) 128 | ;(RTI5.types[0]: Type) 129 | ;(RTI5.types[1]: Type<{ a: number }>) 130 | 131 | // 132 | // tuples 133 | // 134 | 135 | const T6 = t.tuple([t.string, t.number]) 136 | t.map(v6 => { 137 | (v6: [string, number]) 138 | ;(v6: TypeOf) 139 | // $ExpectError 140 | ;(v6: [number, number]) 141 | }, t.validate(['a', 1], T6)) 142 | // $ExpectError 143 | ;([1, 2]: TypeOf) 144 | 145 | // runtime type introspection 146 | const RTI6 = t.tuple([t.string, t.object({ a: t.number })]) 147 | ;(RTI6.name: string) 148 | ;(RTI6.types[0]: Type) 149 | ;(RTI6.types[1]: Type<{ a: number }>) 150 | 151 | // 152 | // intersections 153 | // 154 | 155 | // $ExpectError 156 | t.intersection() 157 | 158 | // $ExpectError 159 | t.intersection([]) 160 | 161 | const T7 = t.intersection([t.object({ a: t.number }), t.object({ b: t.number })]) 162 | t.map(v7 => { 163 | (v7: { a: number } & { b: number }) 164 | ;(v7: TypeOf) 165 | ;(v7: { a: number }) 166 | ;(v7: { b: number }) 167 | // $ExpectError 168 | ;(v7: { a: string }) 169 | }, t.validate({ a: 1, b: 2 }, T7)) 170 | // $ExpectError 171 | ;(1: TypeOf) 172 | 173 | // runtime type introspection 174 | const RTI7 = t.intersection([t.object({ a: t.number }), t.object({ b: t.number })]) 175 | ;(RTI7.name: string) 176 | ;(RTI7.types[0]: Type<{ a: number }>) 177 | ;(RTI7.types[1]: Type<{ b: number }>) 178 | 179 | // 180 | // maybes 181 | // 182 | 183 | const T8 = t.maybe(t.number) 184 | t.map(v8 => { 185 | (v8: ?number) 186 | ;(v8: TypeOf) 187 | // $ExpectError 188 | ;(v8: ?string) 189 | }, t.validate(null, T8)) 190 | ;(null: TypeOf) 191 | ;(undefined: TypeOf) 192 | // $ExpectError 193 | ;('a': TypeOf) 194 | 195 | // runtime type introspection 196 | const RTI8 = t.maybe(t.object({ a: t.number })) 197 | ;(RTI8.name: string) 198 | ;(RTI8.type: Type<{ a: number }>) 199 | 200 | // 201 | // mappings 202 | // 203 | 204 | const T9 = t.mapping(t.union([t.literal('a'), t.literal('b')]), t.number) 205 | t.map(v9 => { 206 | (v9: { [key: 'a' | 'b']: number }) 207 | ;(v9: TypeOf) 208 | // $ExpectError 209 | ;(v9: { [key: string]: number }) 210 | }, t.validate(null, T9)) 211 | ;({}: TypeOf) 212 | // $ExpectError 213 | ;(1: TypeOf) 214 | 215 | // runtime type introspection 216 | const RTI9 = t.mapping(t.union([t.literal('a'), t.literal('b')]), t.object({ a: t.number })) 217 | ;(RTI9.name: string) 218 | ;(RTI9.domain: Type<'a' | 'b'>) 219 | ;(RTI9.codomain: Type<{ a: number }>) 220 | 221 | // 222 | // refinements 223 | // 224 | 225 | const T10 = t.refinement(t.number, n => n >= 0) 226 | t.map(v10 => { 227 | (v10: number) 228 | ;(v10: TypeOf) 229 | // $ExpectError 230 | ;(v10: string) 231 | }, t.validate(1, T10)) 232 | // $ExpectError 233 | ;('a': TypeOf) 234 | 235 | // runtime type introspection 236 | const RTI10 = t.refinement(t.object({ a: t.number }), () => true) 237 | ;(RTI10.name: string) 238 | ;(RTI10.type: Type<{ a: number }>) 239 | 240 | // 241 | // recursive types 242 | // 243 | 244 | type T11T = { 245 | a: number, 246 | b: ?T11T 247 | }; 248 | const T11 = t.recursion('T11', self => t.object({ 249 | a: t.number, 250 | b: t.maybe(self) 251 | })) 252 | t.map(v11 => { 253 | (v11: T11T) 254 | ;(v11: TypeOf) 255 | // $ExpectError 256 | ;(v11: string) 257 | }, t.validate({ a: 1 }, T11)) 258 | // $ExpectError 259 | ;(1: TypeOf) 260 | 261 | // runtime type introspection 262 | const RTI11 = t.recursion('T11', self => t.object({ 263 | a: t.number, 264 | b: t.maybe(self) 265 | })) 266 | ;(RTI11.name: string) 267 | 268 | // 269 | // $Exact 270 | // 271 | 272 | const T13 = t.$exact({ a: t.number }) 273 | t.map(v13 => { 274 | (v13: {| a: number |}) 275 | ;(v13: TypeOf) 276 | // $ExpectError 277 | ;(v13: number) 278 | }, t.validate(1, T13)) 279 | // $ExpectError 280 | ;(1: TypeOf) 281 | 282 | // runtime type introspection 283 | const RTI13 = t.$exact({ a: t.number }) 284 | ;(RTI13.name: string) 285 | ;(RTI13.props: Props) 286 | ;(RTI13.props.a: Type) 287 | 288 | // 289 | // objects 290 | // 291 | 292 | type T15T = { 293 | a: number, 294 | b: { 295 | c: string, 296 | d: { 297 | e: number 298 | } 299 | } 300 | }; 301 | const T15 = t.object({ 302 | a: t.number, 303 | b: t.object({ 304 | c: t.string, 305 | d: t.object({ 306 | e: t.number 307 | }) 308 | }) 309 | }) 310 | t.map(v15 => { 311 | (v15: T15T) 312 | ;(v15: TypeOf) 313 | // $ExpectError 314 | ;(v15.b.d.e: string) 315 | }, t.validate({}, T15)) 316 | // $ExpectError 317 | ;(1: TypeOf) 318 | // $ExpectError 319 | ;({}: TypeOf) 320 | 321 | const RTI15 = t.object({ 322 | a: t.number, 323 | b: t.object({ 324 | c: t.string, 325 | d: t.object({ 326 | e: t.number 327 | }) 328 | }) 329 | }) 330 | ;(RTI15.name: string) 331 | ;(RTI15.props: Props) 332 | 333 | // 334 | // classOf 335 | // 336 | 337 | const T16 = t.classOf(A) 338 | t.map(v16 => { 339 | (v16: Class) 340 | ;(v16: TypeOf) 341 | // $ExpectError 342 | ;(v16: string) 343 | }, t.validate(A, T16)) 344 | // $ExpectError 345 | ;(1: TypeOf) 346 | 347 | // runtime type introspection 348 | const RTI16 = t.classOf(A) 349 | ;(RTI16.name: string) 350 | ;(RTI16.ctor: Class) 351 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-io", 3 | "version": "0.1.0", 4 | "description": "Flow compatible runtime type system for IO validation", 5 | "main": "lib/index.js", 6 | "files": [ 7 | "lib", 8 | "src" 9 | ], 10 | "scripts": { 11 | "dev": "webpack --config=dev/webpack.config.js --watch --progress", 12 | "lint": "eslint src test", 13 | "test": "mocha --compilers js:babel-register", 14 | "build": "rm -rf lib/* && babel src -d lib && flow-copy-source -v src lib" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/gcanti/flow-io.git" 19 | }, 20 | "author": "Giulio Canti ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/gcanti/flow-io/issues" 24 | }, 25 | "homepage": "https://github.com/gcanti/flow-io", 26 | "dependencies": { 27 | "flow-static-land": "^0.2.1" 28 | }, 29 | "devDependencies": { 30 | "babel-cli": "^6.11.4", 31 | "babel-core": "^6.14.0", 32 | "babel-eslint": "^6.1.2", 33 | "babel-plugin-syntax-flow": "^6.13.0", 34 | "babel-plugin-transform-class-properties": "^6.11.5", 35 | "babel-plugin-transform-flow-strip-types": "^6.8.0", 36 | "babel-plugin-transform-object-rest-spread": "^6.16.0", 37 | "babel-preset-es2015": "^6.13.2", 38 | "eslint": "^2.12.0", 39 | "eslint-plugin-flowtype": "^2.25.0", 40 | "flow-copy-source": "^1.1.0", 41 | "mocha": "^3.0.2" 42 | }, 43 | "tags": [ 44 | "flow", 45 | "flowtype", 46 | "static land", 47 | "fantasy land", 48 | "algebraic types", 49 | "functional programming", 50 | "flow-static-land" 51 | ], 52 | "keywords": [ 53 | "flow", 54 | "flowtype", 55 | "static land", 56 | "fantasy land", 57 | "algebraic types", 58 | "functional programming", 59 | "flow-static-land" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import type { Either } from 'flow-static-land/lib/Either' 4 | 5 | import * as either from 'flow-static-land/lib/Either' 6 | import { unsafeCoerce } from 'flow-static-land/lib/Unsafe' 7 | 8 | // 9 | // type extractor 10 | // 11 | 12 | type ExtractType> = T; // eslint-disable-line no-unused-vars 13 | 14 | export type TypeOf = ExtractType<*, RT>; 15 | 16 | export type Validate = (value: mixed, context: Context) => Validation; 17 | 18 | export class Type { 19 | name: string; 20 | validate: Validate; 21 | constructor(name: string, validate: Validate) { 22 | this.name = name 23 | this.validate = validate 24 | } 25 | } 26 | 27 | export type ContextEntry = { 28 | key: string, 29 | type: Type 30 | }; 31 | 32 | export type Context = Array>; 33 | 34 | export type ValidationError = { 35 | value: mixed, 36 | context: Context 37 | }; 38 | 39 | export type Validation = Either, T>; 40 | 41 | // 42 | // helpers 43 | // 44 | 45 | function getValidationError(value: mixed, context: Context): ValidationError { 46 | return { 47 | value, 48 | context 49 | } 50 | } 51 | 52 | function pushAll(xs: Array, ys: Array): void { 53 | Array.prototype.push.apply(xs, ys) 54 | } 55 | 56 | function checkAdditionalProps(props: Props, o: Object, c: Context): Array { 57 | const errors = [] 58 | for (let k in o) { 59 | if (!props.hasOwnProperty(k)) { 60 | errors.push(getValidationError(o[k], c.concat(getContextEntry(k, nil)))) 61 | } 62 | } 63 | return errors 64 | } 65 | 66 | // 67 | // API 68 | // 69 | 70 | function getFunctionName(f: Function): string { 71 | return f.displayName || f.name || `` 72 | } 73 | 74 | function getContextEntry(key: string, type: Type): ContextEntry { 75 | return { 76 | key, 77 | type 78 | } 79 | } 80 | 81 | function getDefaultContext(type: Type): Context { 82 | return [{ key: '', type }] 83 | } 84 | 85 | function getTypeName(type: Type): string { 86 | return type.name 87 | } 88 | 89 | function failures(errors: Array): Validation { 90 | return either.left(errors) 91 | } 92 | 93 | function failure(value: mixed, context: Context): Validation { 94 | return either.left([getValidationError(value, context)]) 95 | } 96 | 97 | function success(value: T): Validation { 98 | return either.right(value) 99 | } 100 | 101 | function isFailure(validation: Validation): boolean { 102 | return either.isLeft(validation) 103 | } 104 | 105 | function isSuccess(validation: Validation): boolean { 106 | return either.isRight(validation) 107 | } 108 | 109 | function fromFailure(validation: Validation): Array { 110 | return either.fromLeft(validation) 111 | } 112 | 113 | function fromSuccess(validation: Validation): T { 114 | return either.fromRight(validation) 115 | } 116 | 117 | function of(a: A): Validation { 118 | return either.of(a) 119 | } 120 | 121 | function map(f: (a: A) => B, validation: Validation): Validation { 122 | return either.map(f, validation) 123 | } 124 | 125 | function ap(f: Validation<(a: A) => B>, validation: Validation): Validation { 126 | return either.ap(f, validation) 127 | } 128 | 129 | function chain(f: (a: A) => Validation, validation: Validation): Validation { 130 | return either.chain(f, validation) 131 | } 132 | 133 | function fold(failure: (errors: Array) => R, success: (value: A) => R, validation: Validation): R { 134 | return isFailure(validation) ? failure(fromFailure(validation)) : success(fromSuccess(validation)) 135 | } 136 | 137 | function validateWithContext(value: mixed, context: Context, type: Type): Validation { 138 | return type.validate(value, context) 139 | } 140 | 141 | function validate(value: mixed, type: Type): Validation { 142 | return validateWithContext(value, getDefaultContext(type), type) 143 | } 144 | 145 | function fromValidation(value: mixed, type: Type): T { 146 | return fromSuccess(validate(value, type)) 147 | } 148 | 149 | function is(value: mixed, type: Type): boolean { 150 | return isSuccess(validate(value, type)) 151 | } 152 | 153 | // 154 | // literals 155 | // 156 | 157 | export class LiteralType extends Type { 158 | value: T; 159 | constructor(name: string, validate: Validate, value: T) { 160 | super(name, validate) 161 | this.value = value 162 | } 163 | } 164 | 165 | export type LiteralTypeValue = string | number | boolean; 166 | 167 | function literal(value: T): LiteralType { 168 | return new LiteralType( 169 | JSON.stringify(value), 170 | (v, c) => v === value ? success(value) : failure(v, c), 171 | value 172 | ) 173 | } 174 | 175 | // 176 | // class instances 177 | // 178 | 179 | export class InstanceOfType extends Type { 180 | ctor: Class; 181 | constructor(name: string, validate: Validate, ctor: Class) { 182 | super(name, validate) 183 | this.ctor = ctor 184 | } 185 | } 186 | 187 | function instanceOf(ctor: Class, name?: string): InstanceOfType { 188 | return new InstanceOfType( 189 | name || getFunctionName(ctor), 190 | (v, c) => v instanceof ctor ? success(v) : failure(v, c), 191 | ctor 192 | ) 193 | } 194 | 195 | // 196 | // classes 197 | // 198 | 199 | export class ClassType extends Type { 200 | ctor: T; 201 | } 202 | 203 | function classOf(ctor: Class, name?: string): ClassType> { 204 | const type = refinement(functionType, f => f === ctor || f.prototype instanceof ctor, name) 205 | return new ClassType( 206 | name || `Class<${getFunctionName(ctor)}>`, 207 | (v, c) => type.validate(v, c), 208 | ctor 209 | ) 210 | } 211 | 212 | // 213 | // irreducibles 214 | // 215 | 216 | function isNil(v: mixed) /* : boolean %checks */ { 217 | return v === void 0 || v === null 218 | } 219 | 220 | const nullType: Type = new Type( 221 | 'null', 222 | (v, c) => v === null ? success(v) : failure(v, c) 223 | ) 224 | 225 | const voidType: Type = new Type( 226 | 'void', 227 | (v, c) => v === void 0 ? success(v) : failure(v, c) 228 | ) 229 | 230 | const nil: Type = new Type( 231 | 'nil', 232 | (v, c) => isNil(v) ? success(v) : failure(v, c) 233 | ) 234 | 235 | const any: Type = new Type( 236 | 'any', 237 | (v, c) => success(v) // eslint-disable-line no-unused-vars 238 | ) 239 | 240 | const string: Type = new Type( 241 | 'string', 242 | (v, c) => typeof v === 'string' ? success(v) : failure(v, c) 243 | ) 244 | 245 | const number: Type = new Type( 246 | 'number', 247 | (v, c) => typeof v === 'number' && isFinite(v) && !isNaN(v) ? success(v) : failure(v, c) 248 | ) 249 | 250 | const boolean: Type = new Type( 251 | 'boolean', 252 | (v, c) => typeof v === 'boolean' ? success(v) : failure(v, c) 253 | ) 254 | 255 | const arrayType: Type> = new Type( 256 | 'Array', 257 | (v, c) => Array.isArray(v) ? success(v) : failure(v, c) 258 | ) 259 | 260 | const objectType: Type = new Type( 261 | 'Object', 262 | (v, c) => !isNil(v) && typeof v === 'object' && !Array.isArray(v) ? success(v) : failure(v, c) 263 | ) 264 | 265 | const functionType: Type = new Type( 266 | 'Function', 267 | (v, c) => typeof v === 'function' ? success(v) : failure(v, c) 268 | ) 269 | 270 | // 271 | // arrays 272 | // 273 | 274 | export class ArrayType extends Type { 275 | type: Type; 276 | constructor(name: string, validate: Validate, type: Type) { 277 | super(name, validate) 278 | this.type = type 279 | } 280 | } 281 | 282 | function array>(type: RT, name?: string): ArrayType> { 283 | return new ArrayType( 284 | name || `Array<${getTypeName(type)}>`, 285 | (v, c) => { 286 | return either.chain((as: Array) => { 287 | const t = [] 288 | const errors = [] 289 | let changed = false 290 | for (let i = 0, len = as.length; i < len; i++) { 291 | const a = as[i] 292 | const validation = type.validate(a, c.concat(getContextEntry(String(i), type))) 293 | if (isFailure(validation)) { 294 | pushAll(errors, fromFailure(validation)) 295 | } 296 | else { 297 | const va = fromSuccess(validation) 298 | changed = changed || ( va !== a ) 299 | t.push(va) 300 | } 301 | } 302 | return errors.length ? failures(errors) : success(changed ? t : unsafeCoerce(as)) 303 | }, arrayType.validate(v, c)) 304 | }, 305 | type 306 | ) 307 | } 308 | 309 | // 310 | // unions 311 | // 312 | 313 | export class UnionType extends Type { 314 | types: Array>; 315 | constructor(name: string, validate: Validate, types: Array>) { 316 | super(name, validate) 317 | this.types = types 318 | } 319 | } 320 | 321 | declare function union(types: [Type, Type, Type, Type], name?: string) : UnionType; // eslint-disable-line no-redeclare 322 | declare function union(types: [Type, Type, Type, Type], name?: string) : UnionType; // eslint-disable-line no-redeclare 323 | declare function union(types: [Type, Type, Type], name?: string) : UnionType; // eslint-disable-line no-redeclare 324 | declare function union(types: [Type, Type], name?: string) : UnionType; // eslint-disable-line no-redeclare 325 | 326 | function union(types: Array>, name?: string): UnionType { // eslint-disable-line no-redeclare 327 | return new UnionType( 328 | name || `(${types.map(getTypeName).join(' | ')})`, 329 | (v, c) => { 330 | for (let i = 0, len = types.length; i < len; i++) { 331 | const validation = types[i].validate(v, c) 332 | if (isSuccess(validation)) { 333 | return validation 334 | } 335 | } 336 | return failure(v, c) 337 | }, 338 | types 339 | ) 340 | } 341 | 342 | // 343 | // tuples 344 | // 345 | 346 | export class TupleType extends Type { 347 | types: Array>; 348 | constructor(name: string, validate: Validate, types: Array>) { 349 | super(name, validate) 350 | this.types = types 351 | } 352 | } 353 | 354 | declare function tuple(types: [Type, Type, Type, Type], name?: string) : TupleType<[A, B, C, E]>; // eslint-disable-line no-redeclare 355 | declare function tuple(types: [Type, Type, Type, Type], name?: string) : TupleType<[A, B, C, D]>; // eslint-disable-line no-redeclare 356 | declare function tuple(types: [Type, Type, Type], name?: string) : TupleType<[A, B, C]>; // eslint-disable-line no-redeclare 357 | declare function tuple(types: [Type, Type], name?: string) : TupleType<[A, B]>; // eslint-disable-line no-redeclare 358 | 359 | function tuple(types: Array>, name?: string): TupleType { // eslint-disable-line no-redeclare 360 | return new TupleType( 361 | name || `[${types.map(getTypeName).join(', ')}]`, 362 | (v, c) => { 363 | return either.chain(as => { 364 | const t = [] 365 | const errors = [] 366 | let changed = false 367 | for (let i = 0, len = types.length; i < len; i++) { 368 | const a = as[i] 369 | const type = types[i] 370 | const validation = type.validate(a, c.concat(getContextEntry(String(i), type))) 371 | if (isFailure(validation)) { 372 | pushAll(errors, fromFailure(validation)) 373 | } 374 | else { 375 | const va = fromSuccess(validation) 376 | changed = changed || ( va !== a ) 377 | t.push(va) 378 | } 379 | } 380 | return errors.length ? failures(errors) : success(changed ? t : as) 381 | }, arrayType.validate(v, c)) 382 | }, 383 | types 384 | ) 385 | } 386 | 387 | // 388 | // intersections 389 | // 390 | 391 | export class IntersectionType extends Type { 392 | types: Array>; 393 | constructor(name: string, validate: Validate, types: Array>) { 394 | super(name, validate) 395 | this.types = types 396 | } 397 | } 398 | 399 | declare function intersection(types: [Type, Type, Type, Type], name?: string) : IntersectionType; // eslint-disable-line no-redeclare 400 | declare function intersection(types: [Type, Type, Type, Type], name?: string) : IntersectionType; // eslint-disable-line no-redeclare 401 | declare function intersection(types: [Type, Type, Type], name?: string) : IntersectionType; // eslint-disable-line no-redeclare 402 | declare function intersection(types: [Type, Type], name?: string) : IntersectionType; // eslint-disable-line no-redeclare 403 | 404 | function intersection(types: Array>, name?: string): IntersectionType<*> { // eslint-disable-line no-redeclare 405 | return new IntersectionType( 406 | name || `(${types.map(getTypeName).join(' & ')})`, 407 | (v, c) => { 408 | let t = v 409 | let changed = false 410 | const errors = [] 411 | for (let i = 0, len = types.length; i < len; i++) { 412 | const type = types[i] 413 | const validation = type.validate(t, c.concat(getContextEntry(String(i), type))) 414 | if (isFailure(validation)) { 415 | pushAll(errors, fromFailure(validation)) 416 | } 417 | else { 418 | const vv = fromSuccess(validation) 419 | changed = changed || ( vv !== t ) 420 | t = vv 421 | } 422 | } 423 | return errors.length ? failures(errors) : success(changed ? t : v) 424 | }, 425 | types 426 | ) 427 | } 428 | 429 | // 430 | // maybes 431 | // 432 | 433 | export class MaybeType extends Type { 434 | type: Type; 435 | constructor(name: string, validate: Validate, type: Type) { 436 | super(name, validate) 437 | this.type = type 438 | } 439 | } 440 | 441 | function maybe(type: Type, name?: string): MaybeType { 442 | return new MaybeType( 443 | name || `?${getTypeName(type)}`, 444 | (v, c) => unsafeCoerce(isNil(v) ? success(v) : type.validate(v, c)), 445 | type 446 | ) 447 | } 448 | 449 | // 450 | // map objects 451 | // 452 | 453 | export class MappingType extends Type { 454 | domain: Type; 455 | codomain: Type; 456 | constructor(name: string, validate: Validate, domain: Type, codomain: Type) { 457 | super(name, validate) 458 | this.domain = domain 459 | this.codomain = codomain 460 | } 461 | } 462 | 463 | function mapping, C, RTC: Type>(domain: RTD, codomain: RTC, name?: string): MappingType<{ [key: D]: C }> { 464 | return new MappingType( 465 | name || `{ [key: ${getTypeName(domain)}]: ${getTypeName(codomain)} }`, 466 | (v, c) => { 467 | return either.chain(o => { 468 | const t = {} 469 | const errors = [] 470 | let changed = false 471 | for (let k in o) { 472 | const ok = o[k] 473 | const domainValidation = domain.validate(k, c.concat(getContextEntry(k, domain))) 474 | const codomainValidation = codomain.validate(ok, c.concat(getContextEntry(k, codomain))) 475 | if (isFailure(domainValidation)) { 476 | pushAll(errors, fromFailure(domainValidation)) 477 | } 478 | else { 479 | const vk = fromSuccess(domainValidation) 480 | changed = changed || ( vk !== k ) 481 | k = vk 482 | } 483 | if (isFailure(codomainValidation)) { 484 | pushAll(errors, fromFailure(codomainValidation)) 485 | } 486 | else { 487 | const vok = fromSuccess(codomainValidation) 488 | changed = changed || ( vok !== ok ) 489 | t[k] = vok 490 | } 491 | } 492 | return errors.length ? failures(errors) : success(changed ? t : o) 493 | }, objectType.validate(v, c)) 494 | }, 495 | domain, 496 | codomain 497 | ) 498 | } 499 | 500 | // 501 | // refinements 502 | // 503 | 504 | export type Predicate = (value: T) => boolean; 505 | 506 | export class RefinementType extends Type { 507 | type: Type; 508 | predicate: Predicate; 509 | constructor(name: string, validate: Validate, type: Type, predicate: Predicate) { 510 | super(name, validate) 511 | this.type = type 512 | this.predicate = predicate 513 | } 514 | } 515 | 516 | function refinement(type: Type, predicate: Predicate, name?: string): RefinementType { 517 | return new RefinementType( 518 | name || `(${getTypeName(type)} | ${getFunctionName(predicate)})`, 519 | (v, c) => either.chain( 520 | t => predicate(t) ? success(t) : failure(v, c), 521 | type.validate(v, c) 522 | ), 523 | type, 524 | predicate 525 | ) 526 | } 527 | 528 | // 529 | // recursive types 530 | // 531 | 532 | function recursion>(name: string, definition: (self: Type) => RT): RT { 533 | const Self = new Type( 534 | name, 535 | (v, c) => Result.validate(v, c) 536 | ) 537 | const Result = definition(Self) 538 | Result.name = name 539 | return Result 540 | } 541 | 542 | // 543 | // $Exact 544 | // 545 | 546 | export type PropsType = $ObjMap(v: Type) => T>; 547 | 548 | export class $ExactType extends Type { 549 | props: Props; 550 | constructor(name: string, validate: Validate, props: Props) { 551 | super(name, validate) 552 | this.props = props 553 | } 554 | } 555 | 556 | function $exact(props: P, name?: string): $ExactType<$Exact>> { 557 | name = name || `$Exact<${getDefaultObjectTypeName(props)}>` 558 | const type = object(props, name) 559 | return new $ExactType( 560 | name, 561 | (v, c) => either.chain(o => { 562 | const errors = checkAdditionalProps(props, o, c) 563 | return errors.length ? failures(errors) : success(unsafeCoerce(o)) 564 | }, type.validate(v, c)), 565 | props 566 | ) 567 | } 568 | 569 | // 570 | // objects 571 | // 572 | 573 | export type Props = { [key: string]: Type }; 574 | 575 | export class ObjectType extends Type { 576 | props: Props; 577 | constructor(name: string, validate: Validate, props: Props) { 578 | super(name, validate) 579 | this.props = props 580 | } 581 | } 582 | 583 | function getDefaultObjectTypeName(props: Props): string { 584 | return `{ ${Object.keys(props).map(k => `${k}: ${props[k].name}`).join(', ')} }` 585 | } 586 | 587 | function object(props: P, name?: string): ObjectType> { 588 | return new ObjectType( 589 | name || getDefaultObjectTypeName(props), 590 | (v, c) => { 591 | return either.chain(o => { 592 | const t = Object.assign({}, o) 593 | const errors = [] 594 | let changed = false 595 | for (let k in props) { 596 | const ok = o[k] 597 | const type = props[k] 598 | const validation = type.validate(ok, c.concat(getContextEntry(k, type))) 599 | if (isFailure(validation)) { 600 | pushAll(errors, fromFailure(validation)) 601 | } 602 | else { 603 | const vok = fromSuccess(validation) 604 | changed = changed || ( vok !== ok ) 605 | t[k] = vok 606 | } 607 | } 608 | return errors.length ? failures(errors) : success(changed ? t : o) 609 | }, objectType.validate(v, c)) 610 | }, 611 | props 612 | ) 613 | } 614 | 615 | export { 616 | unsafeCoerce, 617 | getFunctionName, 618 | getContextEntry, 619 | getDefaultContext, 620 | getTypeName, 621 | failures, 622 | failure, 623 | success, 624 | isFailure, 625 | isSuccess, 626 | fromFailure, 627 | fromSuccess, 628 | of, 629 | map, 630 | ap, 631 | chain, 632 | fold, 633 | validateWithContext, 634 | validate, 635 | fromValidation, 636 | is, 637 | any, 638 | string, 639 | number, 640 | boolean, 641 | nullType as null, 642 | voidType as void, 643 | objectType as Object, 644 | functionType as Function, 645 | literal, 646 | instanceOf, 647 | classOf, 648 | array, 649 | union, 650 | tuple, 651 | maybe, 652 | refinement, 653 | recursion, 654 | mapping, 655 | intersection, 656 | $exact, 657 | object 658 | } 659 | 660 | -------------------------------------------------------------------------------- /src/reporters/Reporter.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Validation } from '../index' 3 | 4 | export interface Reporter { 5 | report: (validation: Validation<*>) => A; 6 | } 7 | -------------------------------------------------------------------------------- /src/reporters/default.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Reporter } from './Reporter' 3 | import type { Context } from '../index' 4 | import { fold, getFunctionName, isFailure } from '../index' 5 | 6 | function stringify(value: mixed): string { 7 | return typeof value === 'function' ? getFunctionName(value) : JSON.stringify(value) 8 | } 9 | 10 | function getContextPath(context: Context): string { 11 | return context.map(({ key, type }) => `${key}: ${type.name}`).join('/') 12 | } 13 | 14 | export const PathReporter: Reporter> = { 15 | report: validation => fold( 16 | es => es.map(e => `Invalid value ${stringify(e.value)} supplied to ${getContextPath(e.context)}`), 17 | () => ['No errors!'], 18 | validation 19 | ) 20 | } 21 | 22 | export const ThrowReporter: Reporter = { 23 | report: validation => { 24 | if (isFailure(validation)) { 25 | throw PathReporter.report(validation).join('\n') 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/$exact.js: -------------------------------------------------------------------------------- 1 | declare var describe: (title: string, f: () => void) => void; 2 | declare var it: (title: string, f: () => void) => void; 3 | 4 | import * as t from '../src/index' 5 | import assert from 'assert' 6 | import { 7 | assertValidationFailure, 8 | assertValidationSuccess, 9 | number2 10 | } from './helpers' 11 | 12 | describe('$exact', () => { 13 | 14 | it('should succeed validating a valid value', () => { 15 | const T = t.$exact({ a: t.string }) 16 | assertValidationSuccess(t.validate({ a: 's' }, T)) 17 | }) 18 | 19 | it('should return the same reference if validation succeeded and nothing changed', () => { 20 | const T = t.$exact({ a: t.string }) 21 | const value = { a: 's' } 22 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 23 | }) 24 | 25 | it('should return a new reference if validation succeeded and something changed', () => { 26 | const T = t.$exact({ a: number2 }) 27 | const value = { a: 1 } 28 | assert.deepEqual(t.fromSuccess(t.validate(value, T)), { a: 2 }) 29 | }) 30 | 31 | it('should fail validating an invalid value', () => { 32 | const T = t.$exact({ a: t.string }) 33 | assertValidationFailure(t.validate(1, T), [ 34 | 'Invalid value 1 supplied to : $Exact<{ a: string }>' 35 | ]) 36 | assertValidationFailure(t.validate({}, T), [ 37 | 'Invalid value undefined supplied to : $Exact<{ a: string }>/a: string' 38 | ]) 39 | assertValidationFailure(t.validate({ a: 1 }, T), [ 40 | 'Invalid value 1 supplied to : $Exact<{ a: string }>/a: string' 41 | ]) 42 | }) 43 | 44 | it('should check for additional props', () => { 45 | const T = t.$exact({ a: t.string }) 46 | assertValidationFailure(t.validate({ a: 's', additional: 2 }, T), [ 47 | 'Invalid value 2 supplied to : $Exact<{ a: string }>/additional: nil' 48 | ]) 49 | }) 50 | 51 | }) 52 | -------------------------------------------------------------------------------- /test/array.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { 9 | assertValidationFailure, 10 | assertValidationSuccess, 11 | number2 12 | } from './helpers' 13 | 14 | describe('array', () => { 15 | 16 | it('should succeed validating a valid value', () => { 17 | const T = t.array(t.number) 18 | assertValidationSuccess(t.validate([], T)) 19 | assertValidationSuccess(t.validate([1, 2, 3], T)) 20 | }) 21 | 22 | it('should return the same reference if validation succeeded and nothing changed', () => { 23 | const T = t.array(t.number) 24 | const value = [1, 2, 3] 25 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 26 | }) 27 | 28 | it('should return the a new reference if validation succeeded and something changed', () => { 29 | const T = t.array(number2) 30 | const value = [1, 2, 3] 31 | assert.deepEqual(t.fromSuccess(t.validate(value, T)), [2, 4, 6]) 32 | }) 33 | 34 | it('should fail validating an invalid value', () => { 35 | const T = t.array(t.number) 36 | assertValidationFailure(t.validate(1, T), [ 37 | 'Invalid value 1 supplied to : Array' 38 | ]) 39 | assertValidationFailure(t.validate([1, 's', 3], T), [ 40 | 'Invalid value "s" supplied to : Array/1: number' 41 | ]) 42 | }) 43 | 44 | }) 45 | -------------------------------------------------------------------------------- /test/classOf.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { assertValidationFailure, assertValidationSuccess } from './helpers' 9 | 10 | describe('classOf', () => { 11 | 12 | it('should succeed validating a valid value', () => { 13 | class A {} 14 | class B extends A {} 15 | const T = t.classOf(A) 16 | assertValidationSuccess(t.validate(A, T)) 17 | assertValidationSuccess(t.validate(B, T)) 18 | }) 19 | 20 | it('should return the same reference if validation succeeded', () => { 21 | class A {} 22 | const T = t.classOf(A) 23 | const value = A 24 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 25 | }) 26 | 27 | it('should fail validating an invalid value', () => { 28 | class A {} 29 | class C {} 30 | const T = t.classOf(A) 31 | assertValidationFailure(t.validate(C, T), [ 32 | 'Invalid value C supplied to : Class' 33 | ]) 34 | }) 35 | 36 | }) 37 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import assert from 'assert' 4 | import type { Validation } from '../src/index' 5 | import { Type } from '../src/index' 6 | import * as t from '../src/index' 7 | import { PathReporter } from '../src/reporters/default' 8 | 9 | export function assertValidationSuccess(validation: Validation): void { 10 | assert.ok(t.isSuccess(validation)) 11 | } 12 | 13 | export function assertValidationFailure(validation: Validation, descriptions: Array): void { 14 | assert.ok(t.isFailure(validation)) 15 | assert.deepEqual(PathReporter.report(validation), descriptions) 16 | } 17 | 18 | export const number2: Type = new Type( 19 | 'number2', 20 | (v, c) => t.map(n => n * 2, t.number.validate(v, c)) 21 | ) 22 | -------------------------------------------------------------------------------- /test/instanceOf.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { assertValidationFailure, assertValidationSuccess } from './helpers' 9 | 10 | describe('instanceOf', () => { 11 | 12 | it('should succeed validating a valid value', () => { 13 | class A {} 14 | const T = t.instanceOf(A) 15 | assertValidationSuccess(t.validate(new A(), T)) 16 | }) 17 | 18 | it('should return the same reference if validation succeeded', () => { 19 | class A {} 20 | const T = t.instanceOf(A) 21 | const value = new A() 22 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 23 | }) 24 | 25 | it('should fail validating an invalid value', () => { 26 | class A {} 27 | const T = t.instanceOf(A) 28 | assertValidationFailure(t.validate(1, T), [ 29 | 'Invalid value 1 supplied to : A' 30 | ]) 31 | }) 32 | 33 | }) 34 | -------------------------------------------------------------------------------- /test/intersection.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { 9 | assertValidationFailure, 10 | assertValidationSuccess, 11 | number2 12 | } from './helpers' 13 | 14 | describe('intersection', () => { 15 | 16 | it('should succeed validating a valid value', () => { 17 | const T = t.intersection([t.object({ a: t.number }), t.object({ b: t.number })]) 18 | assertValidationSuccess(t.validate({ a: 1, b: 2 }, T)) 19 | assertValidationSuccess(t.validate({ a: 1, b: 2, c: 3 }, T)) 20 | }) 21 | 22 | it('should return the same reference if validation succeeded and nothing changed', () => { 23 | const T = t.intersection([t.object({ a: t.number }), t.object({ b: t.number })]) 24 | const value = { a: 1, b: 2 } 25 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 26 | }) 27 | 28 | it('should return the a new reference if validation succeeded and something changed', () => { 29 | const T = t.intersection([t.object({ a: number2 }), t.object({ b: t.number })]) 30 | const value = { a: 1, b: 2 } 31 | assert.deepEqual(t.fromSuccess(t.validate(value, T)), { a: 2, b: 2 }) 32 | }) 33 | 34 | it('should fail validating an invalid value', () => { 35 | const T = t.intersection([t.object({ a: t.number }), t.object({ b: t.number })]) 36 | assertValidationFailure(t.validate({ a: 1 }, T), [ 37 | 'Invalid value undefined supplied to : ({ a: number } & { b: number })/1: { b: number }/b: number' 38 | ]) 39 | }) 40 | 41 | }) 42 | -------------------------------------------------------------------------------- /test/literal.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import { assertValidationFailure, assertValidationSuccess } from './helpers' 8 | 9 | describe('literal', () => { 10 | 11 | it('should succeed validating a valid value', () => { 12 | const T = t.literal('a') 13 | assertValidationSuccess(t.validate('a', T)) 14 | }) 15 | 16 | it('should fail validating an invalid value', () => { 17 | const T = t.literal('a') 18 | assertValidationFailure(t.validate(1, T), [ 19 | 'Invalid value 1 supplied to : "a"' 20 | ]) 21 | }) 22 | 23 | }) 24 | -------------------------------------------------------------------------------- /test/mapping.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { 9 | assertValidationFailure, 10 | assertValidationSuccess, 11 | number2 12 | } from './helpers' 13 | 14 | describe('mapping', () => { 15 | 16 | it('should succeed validating a valid value', () => { 17 | const T = t.mapping(t.refinement(t.string, s => s.length >= 2), t.number) 18 | assertValidationSuccess(t.validate({}, T)) 19 | assertValidationSuccess(t.validate({ aa: 1 }, T)) 20 | }) 21 | 22 | it('should return the same reference if validation succeeded if nothing changed', () => { 23 | const T = t.mapping(t.refinement(t.string, s => s.length >= 2), t.number) 24 | const value = { aa: 1 } 25 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 26 | }) 27 | 28 | it('should return a new reference if validation succeeded and something changed', () => { 29 | const T = t.mapping(t.refinement(t.string, s => s.length >= 2), number2) 30 | const value = { aa: 1 } 31 | assert.deepEqual(t.fromSuccess(t.validate(value, T)), { aa: 2 }) 32 | }) 33 | 34 | it('should fail validating an invalid value', () => { 35 | const T = t.mapping(t.refinement(t.string, s => s.length >= 2), t.number) 36 | assertValidationFailure(t.validate({ a: 1 }, T), [ 37 | 'Invalid value "a" supplied to : { [key: (string | )]: number }/a: (string | )' 38 | ]) 39 | assertValidationFailure(t.validate({ aa: 's' }, T), [ 40 | 'Invalid value "s" supplied to : { [key: (string | )]: number }/aa: number' 41 | ]) 42 | }) 43 | 44 | }) 45 | -------------------------------------------------------------------------------- /test/maybe.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { assertValidationFailure, assertValidationSuccess } from './helpers' 9 | 10 | describe('maybe', () => { 11 | 12 | it('should succeed validating a valid value', () => { 13 | const T = t.maybe(t.number) 14 | assertValidationSuccess(t.validate(null, T)) 15 | assertValidationSuccess(t.validate(undefined, T)) 16 | assertValidationSuccess(t.validate(1, T)) 17 | }) 18 | 19 | it('should return the same reference if validation succeeded', () => { 20 | const T = t.maybe(t.Object) 21 | const value = {} 22 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 23 | }) 24 | 25 | it('should fail validating an invalid value', () => { 26 | const T = t.maybe(t.number) 27 | assertValidationFailure(t.validate('s', T), [ 28 | 'Invalid value "s" supplied to : ?number' 29 | ]) 30 | }) 31 | 32 | }) 33 | -------------------------------------------------------------------------------- /test/object.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { 9 | assertValidationFailure, 10 | assertValidationSuccess, 11 | number2 12 | } from './helpers' 13 | 14 | describe('object', () => { 15 | 16 | it('should succeed validating a valid value', () => { 17 | const T = t.object({ a: t.string }) 18 | assertValidationSuccess(t.validate({ a: 's' }, T)) 19 | }) 20 | 21 | it('should preserve additional props', () => { 22 | const T = t.object({ a: t.string }) 23 | assert.deepEqual(t.fromSuccess(t.validate({ a: 's', b: 1 }, T)), { a: 's', b: 1 }) 24 | }) 25 | 26 | it('should return the same reference if validation succeeded and nothing changed', () => { 27 | const T = t.object({ a: t.string }) 28 | const value = { a: 's' } 29 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 30 | }) 31 | 32 | it('should return the a new reference if validation succeeded and something changed', () => { 33 | const T = t.object({ a: number2, b: t.number }) 34 | const value = { a: 1, b: 2, c: 3 } 35 | assert.deepEqual(t.fromSuccess(t.validate(value, T)), { a: 2, b: 2, c: 3 }) 36 | }) 37 | 38 | it('should fail validating an invalid value', () => { 39 | const T = t.object({ a: t.string }) 40 | assertValidationFailure(t.validate(1, T), [ 41 | 'Invalid value 1 supplied to : { a: string }' 42 | ]) 43 | assertValidationFailure(t.validate({}, T), [ 44 | 'Invalid value undefined supplied to : { a: string }/a: string' 45 | ]) 46 | assertValidationFailure(t.validate({ a: 1 }, T), [ 47 | 'Invalid value 1 supplied to : { a: string }/a: string' 48 | ]) 49 | }) 50 | 51 | it('should allow for additional props', () => { 52 | const T = t.object({ a: t.string }) 53 | assertValidationSuccess(t.validate({ a: 's', additional: 2 }, T)) 54 | }) 55 | 56 | }) 57 | -------------------------------------------------------------------------------- /test/recursion.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { assertValidationFailure, assertValidationSuccess } from './helpers' 9 | 10 | describe('recursion', () => { 11 | 12 | it('should succeed validating a valid value', () => { 13 | const T = t.recursion('T', self => t.object({ 14 | a: t.number, 15 | b: t.maybe(self) 16 | })) 17 | assertValidationSuccess(t.validate({ a: 1 }, T)) 18 | assertValidationSuccess(t.validate({ a: 1, b: { a: 2 } }, T)) 19 | }) 20 | 21 | it('should return the same reference if validation succeeded', () => { 22 | const T = t.recursion('T', self => t.object({ 23 | a: t.number, 24 | b: t.maybe(self) 25 | })) 26 | const value = { a: 1, b: { a: 2 } } 27 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 28 | }) 29 | 30 | it('should fail validating an invalid value', () => { 31 | const T = t.recursion('T', self => t.object({ 32 | a: t.number, 33 | b: t.maybe(self) 34 | })) 35 | assertValidationFailure(t.validate(1, T), [ 36 | 'Invalid value 1 supplied to : T' 37 | ]) 38 | assertValidationFailure(t.validate({}, T), [ 39 | 'Invalid value undefined supplied to : T/a: number' 40 | ]) 41 | assertValidationFailure(t.validate({ a: 1, b: {} }, T), [ 42 | 'Invalid value undefined supplied to : T/b: ?T/a: number' 43 | ]) 44 | }) 45 | 46 | }) 47 | -------------------------------------------------------------------------------- /test/refinement.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { assertValidationFailure, assertValidationSuccess } from './helpers' 9 | 10 | describe('refinement', () => { 11 | 12 | it('should succeed validating a valid value', () => { 13 | const T = t.refinement(t.number, n => n >= 0) 14 | assertValidationSuccess(t.validate(0, T)) 15 | assertValidationSuccess(t.validate(1, T)) 16 | }) 17 | 18 | it('should return the same reference if validation succeeded', () => { 19 | const T = t.refinement(t.Object, () => true) 20 | const value = {} 21 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 22 | }) 23 | 24 | it('should fail validating an invalid value', () => { 25 | const T = t.refinement(t.number, n => n >= 0) 26 | assertValidationFailure(t.validate(-1, T), [ 27 | 'Invalid value -1 supplied to : (number | )' 28 | ]) 29 | }) 30 | 31 | }) 32 | -------------------------------------------------------------------------------- /test/tuple.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { 9 | assertValidationFailure, 10 | assertValidationSuccess, 11 | number2 12 | } from './helpers' 13 | 14 | describe('tuple', () => { 15 | 16 | it('should succeed validating a valid value', () => { 17 | const T = t.tuple([t.number, t.string]) 18 | assertValidationSuccess(t.validate([1, 'a'], T)) 19 | assertValidationSuccess(t.validate([1, 'a', 1], T)) 20 | }) 21 | 22 | it('should return the same reference if validation succeeded and nothing changed', () => { 23 | const T = t.tuple([t.number, t.string]) 24 | const value = [1, 'a'] 25 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 26 | }) 27 | 28 | it('should return the a new reference if validation succeeded and something changed', () => { 29 | const T = t.tuple([number2, t.string]) 30 | const value = [1, 'a'] 31 | assert.deepEqual(t.fromSuccess(t.validate(value, T)), [2, 'a']) 32 | }) 33 | 34 | it('should fail validating an invalid value', () => { 35 | const T = t.tuple([t.number, t.string]) 36 | assertValidationFailure(t.validate([], T), [ 37 | 'Invalid value undefined supplied to : [number, string]/0: number', 38 | 'Invalid value undefined supplied to : [number, string]/1: string' 39 | ]) 40 | assertValidationFailure(t.validate([1], T), [ 41 | 'Invalid value undefined supplied to : [number, string]/1: string' 42 | ]) 43 | assertValidationFailure(t.validate([1, 1], T), [ 44 | 'Invalid value 1 supplied to : [number, string]/1: string' 45 | ]) 46 | }) 47 | 48 | }) 49 | -------------------------------------------------------------------------------- /test/union.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import * as t from '../src/index' 7 | import assert from 'assert' 8 | import { assertValidationFailure, assertValidationSuccess } from './helpers' 9 | 10 | describe('union', () => { 11 | 12 | it('should succeed validating a valid value', () => { 13 | const T = t.union([t.string, t.number]) 14 | assertValidationSuccess(t.validate('s', T)) 15 | assertValidationSuccess(t.validate(1, T)) 16 | }) 17 | 18 | it('should return the same reference if validation succeeded', () => { 19 | const T = t.union([t.Object, t.number]) 20 | const value = {} 21 | assert.strictEqual(t.fromSuccess(t.validate(value, T)), value) 22 | }) 23 | 24 | it('should fail validating an invalid value', () => { 25 | const T = t.union([t.string, t.number]) 26 | assertValidationFailure(t.validate(true, T), [ 27 | 'Invalid value true supplied to : (string | number)' 28 | ]) 29 | }) 30 | 31 | }) 32 | -------------------------------------------------------------------------------- /test/unsafeValidate.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare var describe: (title: string, f: () => void) => void; 4 | declare var it: (title: string, f: () => void) => void; 5 | 6 | import assert from 'assert' 7 | import * as t from '../src/index' 8 | 9 | describe('fromValidation', () => { 10 | 11 | it('should return T if validation succeeded', () => { 12 | assert.strictEqual(t.fromValidation('a', t.string), 'a') 13 | }) 14 | 15 | it('should throw if validation failed', () => { 16 | assert.throws(() => { 17 | t.fromValidation(1, t.string) 18 | }) 19 | }) 20 | 21 | }) 22 | --------------------------------------------------------------------------------