├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package.json └── test └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "ecmaFeatures": { 7 | "modules": true 8 | }, 9 | "rules": { 10 | "no-use-before-define": 0, 11 | "new-cap": 0, 12 | "quotes": [2, "single"], 13 | "comma-dangle": "always" 14 | } 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | node_modules 4 | test 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | before_script: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start -------------------------------------------------------------------------------- /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 | ## v3.4.1 16 | 17 | - **Bug Fix** 18 | - TypeScript: fix recursive type definition error (@RedRoserade) 19 | 20 | ## v3.4.0 21 | 22 | - **New Feature** 23 | - Add TypeScript definitions (@RedRoserade) 24 | 25 | ## v3.3.0 26 | 27 | - **New Feature** 28 | - ValidationError: relax message type (from `String` to `Any`), fix #54 (@pigoz) 29 | 30 | ## v3.2.2 31 | 32 | - **Bug Fix** 33 | - maybe value is not converted from `undefined` to `null` after validation, fix #44 (@gcanti) 34 | 35 | ## v3.2.1 36 | 37 | - **Bug Fix** 38 | - No error returned by intersection of structs, fix #42 (thanks @chrisui) 39 | 40 | ## v3.2.0 41 | 42 | - **New Feature** 43 | - add support for default props (tcomb v3.2) 44 | 45 | ## v3.1.0 46 | 47 | - **New Feature** 48 | - add support for `interface` combinator (tcomb v3.1) 49 | 50 | ## v3.0.0 51 | 52 | **Warning**. If you don't rely in your codebase on the property `maybe(MyType)(undefined) === null` this **is not a breaking change** for you. 53 | 54 | - **Breaking Change** 55 | - upgrade to `tcomb` v3.0.0 56 | 57 | ## v2.3.0 58 | 59 | - **New Feature** 60 | - add `strict` option: no additional properties are allowed while validating structs, fix #12 61 | 62 | ## v2.2.0 63 | 64 | - **New Feature** 65 | - replaced `path` argument with `options`, fix #27 (thanks @th0r) 66 | 67 | ## v2.1.1 68 | 69 | - **Experimental** 70 | - Add support for custom error messages #23 71 | 72 | ## v2.1.0 73 | 74 | - **New Feature** 75 | - upgrade to tcomb v2.2 76 | - validation of `intersection` types 77 | - error messages are formatted accordingly 78 | 79 | ## v2.0.1 80 | 81 | - **Internal** 82 | - upgrade to tcomb v2.1 83 | - updated tests accordingly 84 | 85 | ## v2.0.0 86 | 87 | - **Breaking change** 88 | - upgrade to tcomb v2.0 #17 89 | - drop bower support #18 90 | 91 | ## v1.0.4 92 | 93 | - **Bug Fix** 94 | + Path in dict errors includes all keys upto the error #15 95 | 96 | ## v1.0.3 97 | 98 | - **New Feature** 99 | + Add an optional `path` param to `validate` in order to provide a prefix to error paths #13 100 | 101 | ## v1.0.2 102 | 103 | - **Internal** 104 | + react-native compatibility #11 105 | 106 | ## v1.0.1 107 | 108 | - **Internal** 109 | + Move tcomb to peerDependencies #10 110 | 111 | ## v1.0 112 | 113 | Initial release 114 | 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 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 | [![build status](https://img.shields.io/travis/gcanti/tcomb-validation/master.svg?style=flat-square)](https://travis-ci.org/gcanti/tcomb-validation) 2 | [![dependency status](https://img.shields.io/david/gcanti/tcomb-validation.svg?style=flat-square)](https://david-dm.org/gcanti/tcomb-validation) 3 | ![npm downloads](https://img.shields.io/npm/dm/tcomb-validation.svg) 4 | 5 | A general purpose JavaScript validation library based on type combinators 6 | 7 | # Features 8 | 9 | - concise yet expressive syntax 10 | - validates native types, refinements, objects, lists and tuples, enums, unions, dicts, intersections 11 | - validates structures with arbitrary level of nesting 12 | - detailed informations on failed validations 13 | - lightweight alternative to JSON Schema 14 | - reuse your domain model written with [tcomb](https://github.com/gcanti/tcomb) 15 | 16 | # Documentation 17 | 18 | - [Basic usage](#basic-usage) 19 | - [Primitives](#primitives) 20 | - [Refinements](#refinements) 21 | - [Objects](#objects) 22 | - [Lists and tuples](#lists-and-tuples) 23 | - [Enums](#enums) 24 | - [Unions](#unions) 25 | - [Dicts](#dicts) 26 | - [Intersections](#intersections) 27 | - [Nested structures](#nested-structures) 28 | - [Customise error messages](#customise-error-messages) 29 | - [Use cases](#use-cases) 30 | - [Form validation](#form-validation) 31 | - [JSON schema](#json-schema) 32 | - [API reference](#api-reference) 33 | 34 | # Basic usage 35 | 36 | *If you don't know how to define types with tcomb you may want to take a look at its [README](https://github.com/gcanti/tcomb/blob/master/README.md) file.* 37 | 38 | The main function is `validate`: 39 | 40 | ```js 41 | validate(value, type, [options]) -> ValidationResult 42 | ``` 43 | 44 | - `value` the value to validate 45 | - `type` a type defined with the [tcomb](https://github.com/gcanti/tcomb) library 46 | - `options` (optional) is an object with the following keys 47 | - `path: Array` path prefix for validation 48 | - `context: any` passed to `getValidationErrorMessage` (useful for i18n) 49 | - `strict: boolean` (default `false`) if `true` no additional properties are allowed while validating structs 50 | 51 | returns a `ValidationResult` object containing the result of the validation 52 | 53 | **Note**. 54 | 55 | - `options` can be an array (as `path` prefix) for backward compatibility (deprecated) 56 | 57 | Example 58 | 59 | ```js 60 | var t = require('tcomb-validation'); 61 | var validate = t.validate; 62 | 63 | validate(1, t.String).isValid(); // => false 64 | validate('a', t.String).isValid(); // => true 65 | ``` 66 | 67 | You can inspect the result to quickly identify what's wrong: 68 | 69 | ```js 70 | var result = validate(1, t.String); 71 | result.isValid(); // => false 72 | result.firstError().message; // => 'Invalid value 1 supplied to String' 73 | 74 | // see `result.errors` to inspect all errors 75 | ``` 76 | 77 | ## Primitives 78 | 79 | ```js 80 | // null and undefined 81 | validate('a', t.Nil).isValid(); // => false 82 | validate(null, t.Nil).isValid(); // => true 83 | validate(undefined, t.Nil).isValid(); // => true 84 | 85 | // strings 86 | validate(1, t.String).isValid(); // => false 87 | validate('a', t.String).isValid(); // => true 88 | 89 | // numbers 90 | validate('a', t.Number).isValid(); // => false 91 | validate(1, t.Number).isValid(); // => true 92 | 93 | // booleans 94 | validate(1, t.Boolean).isValid(); // => false 95 | validate(true, t.Boolean).isValid(); // => true 96 | 97 | // optional values 98 | validate(null, maybe(t.String)).isValid(); // => true 99 | validate('a', maybe(t.String)).isValid(); // => true 100 | validate(1, maybe(t.String)).isValid(); // => false 101 | 102 | // functions 103 | validate(1, t.Function).isValid(); // => false 104 | validate(function () {}, t.Function).isValid(); // => true 105 | 106 | // dates 107 | validate(1, t.Date).isValid(); // => false 108 | validate(new Date(), t.Date).isValid(); // => true 109 | 110 | // regexps 111 | validate(1, t.RegExp).isValid(); // => false 112 | validate(/^a/, t.RegExp).isValid(); // => true 113 | ``` 114 | 115 | ## Refinements 116 | 117 | You can express more fine-grained contraints with the `refinement` syntax: 118 | 119 | ```js 120 | // a predicate is a function with signature: (x) -> boolean 121 | var predicate = function (x) { return x >= 0; }; 122 | 123 | // a positive number 124 | var Positive = t.refinement(t.Number, predicate); 125 | 126 | validate(-1, Positive).isValid(); // => false 127 | validate(1, Positive).isValid(); // => true 128 | ``` 129 | 130 | ## Objects 131 | 132 | ### Structs 133 | 134 | ```js 135 | // an object with two numerical properties 136 | var Point = t.struct({ 137 | x: t.Number, 138 | y: t.Number 139 | }); 140 | 141 | validate(null, Point).isValid(); // => false 142 | validate({x: 0}, Point).isValid(); // => false, y is missing 143 | validate({x: 0, y: 'a'}, Point).isValid(); // => false, y is not a number 144 | validate({x: 0, y: 0}, Point).isValid(); // => true 145 | validate({x: 0, y: 0, z: 0}, Point, { strict: true }).isValid(); // => false, no additional properties are allowed 146 | ``` 147 | 148 | ### Interfaces 149 | 150 | **Differences from structs** 151 | 152 | - also checks prototype keys 153 | 154 | ```js 155 | var Serializable = t.interface({ 156 | serialize: t.Function 157 | }); 158 | 159 | validate(new Point(...), Serializable).isValid(); // => false 160 | 161 | Point.prototype.serialize = function () { ... } 162 | 163 | validate(new Point(...), Serializable).isValid(); // => true 164 | ``` 165 | 166 | ## Lists and tuples 167 | 168 | **Lists** 169 | 170 | ```js 171 | // a list of strings 172 | var Words = t.list(t.String); 173 | 174 | validate(null, Words).isValid(); // => false 175 | validate(['hello', 1], Words).isValid(); // => false, [1] is not a string 176 | validate(['hello', 'world'], Words).isValid(); // => true 177 | ``` 178 | 179 | **Tuples** 180 | 181 | ```js 182 | // a tuple (width x height) 183 | var Size = t.tuple([Positive, Positive]); 184 | 185 | validate([1], Size).isValid(); // => false, height missing 186 | validate([1, -1], Size).isValid(); // => false, bad height 187 | validate([1, 2], Size).isValid(); // => true 188 | ``` 189 | 190 | ## Enums 191 | 192 | ```js 193 | var CssTextAlign = t.enums.of('left right center justify'); 194 | 195 | validate('bottom', CssTextAlign).isValid(); // => false 196 | validate('left', CssTextAlign).isValid(); // => true 197 | ``` 198 | 199 | ## Unions 200 | 201 | ```js 202 | var CssLineHeight = t.union([t.Number, t.String]); 203 | 204 | validate(null, CssLineHeight).isValid(); // => false 205 | validate(1.4, CssLineHeight).isValid(); // => true 206 | validate('1.2em', CssLineHeight).isValid(); // => true 207 | ``` 208 | 209 | ## Dicts 210 | 211 | ```js 212 | // a dictionary of numbers 213 | var Country = t.enums.of(['IT', 'US'], 'Country'); 214 | var Warranty = t.dict(Country, t.Number, 'Warranty'); 215 | 216 | validate(null, Warranty).isValid(); // => false 217 | validate({a: 2}, Warranty).isValid(); // => false, ['a'] is not a Country 218 | validate({US: 2, IT: 'a'}, Warranty).isValid(); // => false, ['IT'] is not a number 219 | validate({US: 2, IT: 1}, Warranty).isValid(); // => true 220 | ``` 221 | 222 | ## Intersections 223 | 224 | ```js 225 | var Min = t.refinement(t.String, function (s) { return s.length > 2; }, 'Min'); 226 | var Max = t.refinement(t.String, function (s) { return s.length < 5; }, 'Max'); 227 | var MinMax = t.intersection([Min, Max], 'MinMax'); 228 | 229 | MinMax.is('abc'); // => true 230 | MinMax.is('a'); // => false 231 | MinMax.is('abcde'); // => false 232 | ``` 233 | 234 | ## Nested structures 235 | 236 | You can validate structures with an arbitrary level of nesting: 237 | 238 | ```js 239 | var Post = t.struct({ 240 | title: t.String, 241 | content: t.String, 242 | tags: Words 243 | }); 244 | 245 | var mypost = { 246 | title: 'Awesome!', 247 | content: 'You can validate structures with arbitrary level of nesting', 248 | tags: ['validation', 1] // <-- ouch! 249 | }; 250 | 251 | validate(mypost, Post).isValid(); // => false 252 | validate(mypost, Post).firstError().message; // => 'tags[1] is `1`, should be a `Str`' 253 | ``` 254 | 255 | # Customise error messages 256 | 257 | You can customise the validation error message defining a function `getValidationErrorMessage(value, path, context)` on the type constructor: 258 | 259 | ```js 260 | var ShortString = t.refinement(t.String, function (s) { 261 | return s.length < 3; 262 | }); 263 | 264 | ShortString.getValidationErrorMessage = function (value) { 265 | if (!value) { 266 | return 'Required'; 267 | } 268 | if (value.length >= 3) { 269 | return 'Too long my friend'; 270 | } 271 | }; 272 | 273 | validate('abc', ShortString).firstError().message; // => 'Too long my friend' 274 | ``` 275 | 276 | ## How to keep DRY? 277 | 278 | In order to keep the validation logic in one place, one may define a custom combinator: 279 | 280 | ```js 281 | function mysubtype(type, getValidationErrorMessage, name) { 282 | var Subtype = t.refinement(type, function (x) { 283 | return !t.String.is(getValidationErrorMessage(x)); 284 | }, name); 285 | Subtype.getValidationErrorMessage = getValidationErrorMessage; 286 | return Subtype; 287 | } 288 | 289 | var ShortString = mysubtype(t.String, function (s) { 290 | if (!s) { 291 | return 'Required'; 292 | } 293 | if (s.length >= 3) { 294 | return 'Too long my friend'; 295 | } 296 | }); 297 | 298 | ``` 299 | 300 | # Use cases 301 | 302 | ## Form validation 303 | 304 | Let's design the process for a simple sign in form: 305 | 306 | ```js 307 | var SignInInfo = t.struct({ 308 | username: t.String, 309 | password: t.String 310 | }); 311 | 312 | // retrieves values from the UI 313 | var formValues = { 314 | username: $('#username').val().trim() || null, 315 | password: $('#password').val().trim() || null 316 | }; 317 | 318 | // if formValues = {username: null, password: 'password'} 319 | var result = validate(formValues, SignInInfo); 320 | result.isValid(); // => false 321 | result.firstError().message; // => 'Invalid value null supplied to /username: String' 322 | ``` 323 | 324 | ## JSON schema 325 | 326 | If you don't want to use a JSON Schema validator or it's not applicable, you can just use this lightweight library in a snap. This is the JSON Schema example of [http://jsonschemalint.com/](http://jsonschemalint.com/) 327 | 328 | ```json 329 | { 330 | "type": "object", 331 | "properties": { 332 | "foo": { 333 | "type": "number" 334 | }, 335 | "bar": { 336 | "type": "string", 337 | "enum": [ 338 | "a", 339 | "b", 340 | "c" 341 | ] 342 | } 343 | } 344 | } 345 | ``` 346 | 347 | and the equivalent `tcomb-validation` counterpart: 348 | 349 | ```js 350 | var Schema = t.struct({ 351 | foo: t.Number, 352 | bar: t.enums.of('a b c') 353 | }); 354 | ``` 355 | 356 | let's validate the example JSON: 357 | 358 | ```js 359 | var json = { 360 | "foo": "this is a string, not a number", 361 | "bar": "this is a string that isn't allowed" 362 | }; 363 | 364 | validate(json, Schema).isValid(); // => false 365 | 366 | // the returned errors are: 367 | - Invalid value "this is a string, not a number" supplied to /foo: Number 368 | - Invalid value "this is a string that isn't allowed" supplied to /bar: "a" | "b" | "c" 369 | ``` 370 | 371 | **Note**: A feature missing in standard JSON Schema is the powerful [refinement](#refinements) syntax. 372 | 373 | # Api reference 374 | 375 | ## ValidationResult 376 | 377 | `ValidationResult` represents the result of a validation. It containes the following fields: 378 | 379 | - `errors`: a list of `ValidationError` if validation fails 380 | - `value`: an instance of `type` if validation succeded 381 | 382 | ```js 383 | // the definition of `ValidationError` 384 | var ValidationError = t.struct({ 385 | message: t.String, // a default message for developers 386 | actual: t.Any, // the actual value being validated 387 | expected: t.Function, // the type expected 388 | path: list(t.union([t.String, t.Number])) // the path of the value 389 | }, 'ValidationError'); 390 | 391 | // the definition of `ValidationResult` 392 | var ValidationResult = t.struct({ 393 | errors: list(ValidationError), 394 | value: t.Any 395 | }, 'ValidationResult'); 396 | ``` 397 | 398 | ### #isValid() 399 | 400 | Returns true if there are no errors. 401 | 402 | ```js 403 | validate('a', t.String).isValid(); // => true 404 | ``` 405 | 406 | ### #firstError() 407 | 408 | Returns an object that contains an error message or `null` if validation succeeded. 409 | 410 | ```js 411 | validate(1, t.String).firstError().message; // => 'value is `1`, should be a `Str`' 412 | ``` 413 | 414 | ## validate(value, type, [options]) -> ValidationResult 415 | 416 | - `value` the value to validate 417 | - `type` a type defined with the tcomb library 418 | - `options` (optional) is an object with the following keys 419 | - `path: Array` path prefix for validation 420 | - `context: any` passed to `getValidationErrorMessage` (useful for i18n) 421 | - `strict: boolean` (default `false`) if `true` no additional properties are allowed while validating structs 422 | 423 | # Tests 424 | 425 | Run `npm test` 426 | 427 | # License 428 | 429 | The MIT License (MIT) 430 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'tcomb'; 2 | 3 | export * from 'tcomb'; 4 | 5 | // Augment 'tcomb': Add getValidationErrorMessage. 6 | declare module 'tcomb' { 7 | export interface Type { 8 | /** 9 | * Allows customization of the error message for a type. 10 | * 11 | * (Extension from tcomb-validation) 12 | * 13 | * @param actual Current value 14 | * @param path Path to validate 15 | * @param context Additional metadata. 16 | */ 17 | getValidationErrorMessage(actual: T, path: Path, context: any): string; 18 | } 19 | } 20 | 21 | /** 22 | * Defines a path through the properties of an 23 | * object (string, property name) or array (number, index). 24 | */ 25 | type Path = Array; 26 | type Predicate = (value: T) => boolean; 27 | 28 | export interface ValidationError { 29 | /** 30 | * Error message. 31 | */ 32 | message: string; 33 | /** 34 | * Current value. 35 | */ 36 | actual: any; 37 | /** 38 | * Expected type. 39 | */ 40 | expected: t.Type; 41 | /** 42 | * Path to the property/index that failed validation. 43 | */ 44 | path: Path; 45 | } 46 | 47 | /** 48 | * Result of a validation. 49 | */ 50 | export interface ValidationResult { 51 | /** 52 | * True if there are no validation errors. False otherwise. 53 | */ 54 | isValid(): boolean; 55 | /** 56 | * Returns the first error, if any. Null otherwise. 57 | */ 58 | firstError(): ValidationError | null; 59 | /** 60 | * Contains the validation errors, if any. Empty if none. 61 | */ 62 | errors: Array; 63 | } 64 | 65 | /** 66 | * Options for the validate function. 67 | */ 68 | interface ValidateOptions { 69 | /** 70 | * Path prefix for validation. 71 | */ 72 | path?: Path; 73 | /** 74 | * Data passed to getValidationErrorMessage. 75 | */ 76 | context?: any; 77 | /** 78 | * If true, no additional properties are allowed 79 | * when validating structs. 80 | * 81 | * Defaults to false. 82 | */ 83 | strict?: boolean; 84 | } 85 | 86 | /** 87 | * Validates an object and returns the validation result. 88 | * @param value The value to validate. 89 | * @param type The type to validate against. 90 | * @param options Validation options. Optional. 91 | */ 92 | export function validate(value: any, type: t.Type, options?: ValidateOptions): ValidationResult; 93 | 94 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var t = require('tcomb'); 4 | var stringify = t.stringify; 5 | 6 | var noobj = {}; 7 | 8 | var ValidationError = t.struct({ 9 | message: t.Any, 10 | actual: t.Any, 11 | expected: t.Any, 12 | path: t.list(t.union([t.String, t.Number])) 13 | }, 'ValidationError'); 14 | 15 | function getDefaultValidationErrorMessage(actual, expected, path) { 16 | var expectedName = t.getTypeName(expected); 17 | var to = path.length ? '/' + path.join('/') + ': ' + expectedName : expectedName; 18 | return 'Invalid value ' + stringify(actual) + ' supplied to ' + to; 19 | } 20 | 21 | function getValidationErrorMessage(actual, expected, path, context) { 22 | if (t.Function.is(expected.getValidationErrorMessage)) { 23 | return expected.getValidationErrorMessage(actual, path, context); 24 | } 25 | else { 26 | return getDefaultValidationErrorMessage(actual, expected, path); 27 | } 28 | } 29 | 30 | ValidationError.of = function (actual, expected, path, context) { 31 | return new ValidationError({ 32 | message: getValidationErrorMessage(actual, expected, path, context), 33 | actual: actual, 34 | expected: expected, 35 | path: path 36 | }); 37 | }; 38 | 39 | var ValidationResult = t.struct({ 40 | errors: t.list(ValidationError), 41 | value: t.Any 42 | }, 'ValidationResult'); 43 | 44 | ValidationResult.prototype.isValid = function () { 45 | return !(this.errors.length); 46 | }; 47 | 48 | ValidationResult.prototype.firstError = function () { 49 | return this.isValid() ? null : this.errors[0]; 50 | }; 51 | 52 | ValidationResult.prototype.toString = function () { 53 | if (this.isValid()) { 54 | return '[ValidationResult, true, ' + stringify(this.value) + ']'; 55 | } 56 | else { 57 | return '[ValidationResult, false, (' + this.errors.map(function (err) { 58 | return stringify(err.message); 59 | }).join(', ') + ')]'; 60 | } 61 | }; 62 | 63 | function validate(x, type, options) { 64 | options = options || {}; 65 | var path = t.Array.is(options) ? options : options.path || []; 66 | return new ValidationResult(recurse(x, type, path, options)); 67 | } 68 | 69 | function recurse(x, type, path, options) { 70 | if (t.isType(type)) { 71 | return validators[type.meta.kind](x, type, path, options); 72 | } 73 | else { 74 | return validators.es6classes(x, type, path, options); 75 | } 76 | } 77 | 78 | var validators = validate.validators = {}; 79 | 80 | validators.es6classes = function validateES6Classes(x, type, path, options) { 81 | return { 82 | value: x, 83 | errors: x instanceof type ? [] : [ValidationError.of(x, type, path, options.context)] 84 | }; 85 | }; 86 | 87 | // irreducibles and enums 88 | validators.irreducible = 89 | validators.enums = function validateIrreducible(x, type, path, options) { 90 | return { 91 | value: x, 92 | errors: type.is(x) ? [] : [ValidationError.of(x, type, path, options.context)] 93 | }; 94 | }; 95 | 96 | validators.list = function validateList(x, type, path, options) { 97 | 98 | // x should be an array 99 | if (!t.Array.is(x)) { 100 | return {value: x, errors: [ValidationError.of(x, type, path, options.context)]}; 101 | } 102 | 103 | var ret = {value: [], errors: []}; 104 | // every item should be of type `type.meta.type` 105 | for (var i = 0, len = x.length; i < len; i++ ) { 106 | var item = recurse(x[i], type.meta.type, path.concat(i), options); 107 | ret.value[i] = item.value; 108 | ret.errors = ret.errors.concat(item.errors); 109 | } 110 | return ret; 111 | }; 112 | 113 | validators.subtype = function validateSubtype(x, type, path, options) { 114 | 115 | // x should be a valid inner type 116 | var ret = recurse(x, type.meta.type, path, options); 117 | if (ret.errors.length) { 118 | return ret; 119 | } 120 | 121 | // x should satisfy the predicate 122 | if (!type.meta.predicate(ret.value)) { 123 | ret.errors = [ValidationError.of(x, type, path, options.context)]; 124 | } 125 | 126 | return ret; 127 | 128 | }; 129 | 130 | validators.maybe = function validateMaybe(x, type, path, options) { 131 | return t.Nil.is(x) ? 132 | {value: x, errors: []} : 133 | recurse(x, type.meta.type, path, options); 134 | }; 135 | 136 | validators.struct = function validateStruct(x, type, path, options) { 137 | 138 | // x should be an object 139 | if (!t.Object.is(x)) { 140 | return {value: x, errors: [ValidationError.of(x, type, path, options.context)]}; 141 | } 142 | 143 | // [optimization] 144 | if (type.is(x)) { 145 | return {value: x, errors: []}; 146 | } 147 | 148 | var ret = {value: {}, errors: []}; 149 | var props = type.meta.props; 150 | var defaultProps = type.meta.defaultProps || noobj; 151 | // every item should be of type `props[name]` 152 | for (var name in props) { 153 | if (props.hasOwnProperty(name)) { 154 | var actual = x[name]; 155 | // apply defaults 156 | if (actual === undefined) { 157 | actual = defaultProps[name]; 158 | } 159 | var prop = recurse(actual, props[name], path.concat(name), options); 160 | ret.value[name] = prop.value; 161 | ret.errors = ret.errors.concat(prop.errors); 162 | } 163 | } 164 | var strict = options.hasOwnProperty('strict') ? options.strict : type.meta.strict; 165 | if (strict) { 166 | for (var field in x) { 167 | if (x.hasOwnProperty(field) && !props.hasOwnProperty(field)) { 168 | ret.errors.push(ValidationError.of(x[field], t.Nil, path.concat(field), options.context)); 169 | } 170 | } 171 | } 172 | if (!ret.errors.length) { 173 | ret.value = new type(ret.value); 174 | } 175 | return ret; 176 | }; 177 | 178 | validators.tuple = function validateTuple(x, type, path, options) { 179 | 180 | var types = type.meta.types; 181 | var len = types.length; 182 | 183 | // x should be an array of at most `len` items 184 | if (!t.Array.is(x) || x.length > len) { 185 | return {value: x, errors: [ValidationError.of(x, type, path, options.context)]}; 186 | } 187 | 188 | var ret = {value: [], errors: []}; 189 | // every item should be of type `types[i]` 190 | for (var i = 0; i < len; i++) { 191 | var item = recurse(x[i], types[i], path.concat(i), options); 192 | ret.value[i] = item.value; 193 | ret.errors = ret.errors.concat(item.errors); 194 | } 195 | return ret; 196 | }; 197 | 198 | validators.dict = function validateDict(x, type, path, options) { 199 | 200 | // x should be an object 201 | if (!t.Object.is(x)) { 202 | return {value: x, errors: [ValidationError.of(x, type, path, options.context)]}; 203 | } 204 | 205 | var ret = {value: {}, errors: []}; 206 | // every key should be of type `domain` 207 | // every value should be of type `codomain` 208 | for (var k in x) { 209 | if (x.hasOwnProperty(k)) { 210 | var subpath = path.concat(k); 211 | var key = recurse(k, type.meta.domain, subpath, options); 212 | var item = recurse(x[k], type.meta.codomain, subpath, options); 213 | ret.value[k] = item.value; 214 | ret.errors = ret.errors.concat(key.errors, item.errors); 215 | } 216 | } 217 | return ret; 218 | }; 219 | 220 | validators.union = function validateUnion(x, type, path, options) { 221 | var ctor = type.dispatch(x); 222 | return t.Function.is(ctor) ? 223 | recurse(x, ctor, path.concat(type.meta.types.indexOf(ctor)), options) : 224 | {value: x, errors: [ValidationError.of(x, type, path, options.context)]}; 225 | }; 226 | 227 | validators.intersection = function validateIntersection(x, type, path, options) { 228 | 229 | var types = type.meta.types; 230 | var len = types.length; 231 | 232 | var ret = {value: x, errors: []}; 233 | var nrOfStructs = 0; 234 | // x should be of type `types[i]` for all i 235 | for (var i = 0; i < len; i++) { 236 | if (types[i].meta.kind === 'struct') { 237 | nrOfStructs++; 238 | } 239 | var item = recurse(x, types[i], path, options); 240 | ret.errors = ret.errors.concat(item.errors); 241 | } 242 | if (nrOfStructs > 1) { 243 | ret.errors.push(ValidationError.of(x, type, path, options.context)); 244 | } 245 | return ret; 246 | }; 247 | 248 | validators['interface'] = function validateInterface(x, type, path, options) { // eslint-disable-line dot-notation 249 | 250 | // x should be an object 251 | if (!t.Object.is(x)) { 252 | return {value: x, errors: [ValidationError.of(x, type, path, options.context)]}; 253 | } 254 | 255 | var ret = {value: {}, errors: []}; 256 | var props = type.meta.props; 257 | // every item should be of type `props[name]` 258 | for (var name in props) { 259 | var prop = recurse(x[name], props[name], path.concat(name), options); 260 | ret.value[name] = prop.value; 261 | ret.errors = ret.errors.concat(prop.errors); 262 | } 263 | var strict = options.hasOwnProperty('strict') ? options.strict : type.meta.strict; 264 | if (strict) { 265 | for (var field in x) { 266 | if (!props.hasOwnProperty(field) && !t.Nil.is(x[field])) { 267 | ret.errors.push(ValidationError.of(x[field], t.Nil, path.concat(field), options.context)); 268 | } 269 | } 270 | } 271 | return ret; 272 | }; 273 | 274 | t.mixin(t, { 275 | ValidationError: ValidationError, 276 | ValidationResult: ValidationResult, 277 | validate: validate 278 | }); 279 | 280 | module.exports = t; 281 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tcomb-validation", 3 | "version": "3.4.1", 4 | "description": "General purpose validation library for JavaScript", 5 | "main": "index.js", 6 | "typings": "index.d.ts", 7 | "files": [ 8 | "index.js", 9 | "index.d.ts" 10 | ], 11 | "scripts": { 12 | "lint": "eslint index.js", 13 | "test": "npm run lint && mocha" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/gcanti/tcomb-validation.git" 18 | }, 19 | "author": "Giulio Canti ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/gcanti/tcomb-validation/issues" 23 | }, 24 | "homepage": "https://github.com/gcanti/tcomb-validation", 25 | "dependencies": { 26 | "tcomb": "^3.0.0" 27 | }, 28 | "devDependencies": { 29 | "eslint": "0.22.1", 30 | "mocha": "2.2.5" 31 | }, 32 | "tags": [ 33 | "tcomb", 34 | "validation", 35 | "models", 36 | "domain" 37 | ], 38 | "keywords": [ 39 | "tcomb", 40 | "validation", 41 | "models", 42 | "domain" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global describe,it */ 2 | var assert = require('assert'); 3 | var t = require('../index'); 4 | 5 | var validate = t.validate; 6 | var ValidationResult = t.ValidationResult; 7 | 8 | function failure(actual, expected, path, message, value) { 9 | var err = { 10 | message: message, 11 | actual: actual, 12 | expected: expected, 13 | path: path 14 | }; 15 | return new ValidationResult({errors: [err], value: value}); 16 | } 17 | 18 | function success(value) { 19 | return new ValidationResult({value: value, errors: []}); 20 | } 21 | 22 | // 23 | // setup 24 | // 25 | 26 | var ok = function (x) { assert.strictEqual(true, x); }; 27 | var eq = assert.deepEqual; 28 | 29 | describe('validate(value, type, [options])', function () { 30 | 31 | var Point = t.struct({ 32 | x: t.Number, 33 | y: t.Number 34 | }, 'Point'); 35 | 36 | var PointInterface = t.inter({ 37 | x: t.Number, 38 | y: t.Number 39 | }, 'PointInterface'); 40 | 41 | it('irriducible', function () { 42 | eq(validate('a', t.String), success('a')); 43 | eq(validate(1, t.Number), success(1)); 44 | eq(validate(true, t.Bool), success(true)); 45 | eq(validate(/a/, t.Re), success(/a/)); 46 | var d = new Date(); 47 | eq(validate(d, t.Dat), success(d)); 48 | ok(validate(function () {}, t.Func).isValid()); 49 | eq(validate(1, t.String), failure(1, t.String, [], 'Invalid value 1 supplied to String', 1)); 50 | }); 51 | 52 | it('enums', function () { 53 | var Country = t.enums.of('IT US', 'Country'); 54 | eq(validate('IT', Country), success('IT')); 55 | eq(validate(1, Country), failure(1, Country, [], 'Invalid value 1 supplied to Country', 1)); 56 | }); 57 | 58 | it('struct', function () { 59 | eq(validate({x: 0, y: 0}, Point), success({x: 0, y: 0})); 60 | ok(validate({x: 0, y: 0}, Point).value instanceof Point); 61 | eq(validate({x: 0, y: 'a'}, Point), failure('a', t.Number, ['y'], 'Invalid value "a" supplied to /y: Number', {x: 0, y: 'a'})); 62 | eq(validate(new Point({x: 0, y: 0}), Point), success({x: 0, y: 0})); 63 | }); 64 | 65 | it('struct default props', function () { 66 | var DefaultPoint = t.struct({ 67 | x: t.Number, 68 | y: t.Number 69 | }, { defaultProps: { x: 0 } }); 70 | eq(validate({y: 0}, DefaultPoint), success({x: 0, y: 0})); 71 | eq(validate({x: 1, y: 0}, DefaultPoint), success({x: 1, y: 0})); 72 | ok(validate({y: 0}, DefaultPoint).value instanceof DefaultPoint); 73 | eq(validate({y: 'a'}, DefaultPoint), failure('a', t.Number, ['y'], 'Invalid value "a" supplied to /y: Number', {x: 0, y: 'a'})); 74 | eq(validate({x: 1, y: 'a'}, DefaultPoint), failure('a', t.Number, ['y'], 'Invalid value "a" supplied to /y: Number', {x: 1, y: 'a'})); 75 | eq(validate(new DefaultPoint({y: 0}), Point), success({x: 0, y: 0})); 76 | }); 77 | 78 | it('interface', function () { 79 | eq(validate({x: 0, y: 0}, PointInterface), success({x: 0, y: 0})); 80 | eq(validate({x: 0, y: 0, z: 0}, PointInterface), success({x: 0, y: 0})); 81 | eq(validate({x: 0, y: 'a'}, PointInterface), failure('a', t.Number, ['y'], 'Invalid value "a" supplied to /y: Number', {x: 0, y: 'a'})); 82 | eq(validate({x: 0}, PointInterface), failure(undefined, t.Number, ['y'], 'Invalid value undefined supplied to /y: Number', {x: 0, y: undefined})); 83 | eq(validate(PointInterface({x: 0, y: 0}), PointInterface), success({x: 0, y: 0})); 84 | // prototype 85 | var Serializable = t.inter({ 86 | serialize: t.Function 87 | }, 'Serializable'); 88 | function SerializableImpl() {} 89 | SerializableImpl.prototype.serialize = function () {}; 90 | eq(validate(new SerializableImpl(), Serializable), success({ serialize: SerializableImpl.prototype.serialize })); 91 | eq(validate({}, Serializable), failure(undefined, t.Function, ['serialize'], 'Invalid value undefined supplied to /serialize: Function', { serialize: undefined })); 92 | }); 93 | 94 | it('list', function () { 95 | var Tags = t.list(t.String, 'Tags'); 96 | eq(validate(['a'], Tags), success(['a'])); 97 | eq(validate(1, Tags), failure(1, Tags, [], 'Invalid value 1 supplied to Tags', 1)); 98 | eq(validate([1], Tags), failure(1, t.String, [0], 'Invalid value 1 supplied to /0: String', [1])); 99 | var Points = t.list(Point); 100 | eq(validate([{x: 0, y: 0}], Points), success([{x: 0, y: 0}])); 101 | ok(validate([{x: 0, y: 0}], Points).value[0] instanceof Point); 102 | }); 103 | 104 | it('refinement', function () { 105 | var URL = t.refinement(t.String, function (s) { return s.indexOf('http://') === 0; }, 'URL'); 106 | eq(validate('http://gcanti.github.io', URL), success('http://gcanti.github.io')); 107 | eq(validate(1, URL), failure(1, t.String, [], 'Invalid value 1 supplied to String', 1)); 108 | eq(validate('a', URL), failure('a', URL, [], 'Invalid value "a" supplied to URL', 'a')); 109 | var PointQ1 = t.refinement(Point, function (p) { 110 | return p.x >= 0 && p.y >= 0; 111 | }); 112 | eq(validate({x: 0, y: 0}, PointQ1), success({x: 0, y: 0})); 113 | ok(validate({x: 0, y: 0}, PointQ1).value instanceof Point); 114 | }); 115 | 116 | it('maybe', function () { 117 | var Maybe = t.maybe(t.String, 'Maybe'); 118 | eq(validate(null, Maybe), success(null)); 119 | eq(validate(undefined, Maybe), success(undefined)); 120 | assert.strictEqual(validate(undefined, Maybe).value, undefined); 121 | eq(validate('a', Maybe), success('a')); 122 | eq(validate(1, Maybe), failure(1, t.String, [], 'Invalid value 1 supplied to String', 1)); 123 | eq(validate(null, t.maybe(Point)), success(null)); 124 | eq(validate({x: 0, y: 0}, Point), success({x: 0, y: 0})); 125 | ok(validate({x: 0, y: 0}, Point).value instanceof Point); 126 | }); 127 | 128 | it('tuple', function () { 129 | var Tuple = t.tuple([t.String, t.Number], 'Tuple'); 130 | eq(validate(1, Tuple), failure(1, Tuple, [], 'Invalid value 1 supplied to Tuple', 1)); 131 | eq(validate(['a', 1], Tuple), success(['a', 1])); 132 | eq(validate(['a', 1, 2], Tuple), failure(['a', 1, 2], Tuple, [], 'Invalid value [\n \"a\",\n 1,\n 2\n] supplied to Tuple', ['a', 1, 2])); 133 | eq(validate(['a'], Tuple), failure(undefined, t.Number, [1], 'Invalid value undefined supplied to /1: Number', ['a', undefined])); 134 | eq(validate(['a', 'b'], Tuple), failure('b', t.Number, [1], 'Invalid value "b" supplied to /1: Number', ['a', 'b'])); 135 | Tuple = t.tuple([t.String, Point], 'Tuple'); 136 | eq(validate(['a', {x: 0, y: 0}], Tuple), success(['a', {x: 0, y: 0}])); 137 | ok(validate(['a', {x: 0, y: 0}], Tuple).value[1] instanceof Point); 138 | eq(validate(['a', 'b'], Tuple), failure('b', Point, [1], 'Invalid value "b" supplied to /1: Point', ['a', 'b'])); 139 | }); 140 | 141 | it('dict', function () { 142 | var Key = t.refinement(t.String, function (k) { 143 | return k.length >= 2; 144 | }, 'Key'); 145 | var Value = t.refinement(t.Number, function (n) { 146 | return n >= 0; 147 | }, 'Value'); 148 | var Dict = t.dict(Key, Value, 'Dict'); 149 | eq(validate({}, Dict), success({})); 150 | eq(validate({aa: 1, bb: 2}, Dict), success({aa: 1, bb: 2})); 151 | eq(validate(1, Dict), failure(1, Dict, [], 'Invalid value 1 supplied to Dict', 1)); 152 | eq(validate({a: 1}, Dict), failure('a', Key, ['a'], 'Invalid value "a" supplied to /a: Key', {a: 1})); 153 | eq(validate({aa: -1}, Dict), failure(-1, Value, ['aa'], 'Invalid value -1 supplied to /aa: Value', {aa: -1})); 154 | Dict = t.dict(Key, Point, 'Dict'); 155 | eq(validate({aa: {x: 0, y: 0}}, Dict), success({aa: {x: 0, y: 0}})); 156 | ok(validate({aa: {x: 0, y: 0}}, Dict).value.aa instanceof Point); 157 | eq(validate({a: {x: 0, y: 0}}, Dict), failure('a', Key, ['a'], 'Invalid value "a" supplied to /a: Key', {a: {x: 0, y: 0}})); 158 | ok(validate({a: {x: 0, y: 0}}, Dict).value.a instanceof Point); 159 | eq(validate({aa: {x: 0, y: 'a'}}, Dict), failure('a', t.Number, ['aa', 'y'], 'Invalid value "a" supplied to /aa/y: Number', {aa: {x: 0, y: 'a'}})); 160 | Dict = t.dict(t.String, t.String); 161 | eq(validate({a: 'a', b: 0}, Dict), failure(0, t.String, ['b'], 'Invalid value 0 supplied to /b: String', {a: 'a', b: 0})); 162 | }); 163 | 164 | it('union', function () { 165 | var Union = t.union([t.String, t.Number], 'Union'); 166 | eq(validate(1, Union), success(1)); 167 | eq(validate('a', Union), success('a')); 168 | eq(validate(true, Union), failure(true, Union, [], 'Invalid value true supplied to Union', true)); 169 | }); 170 | 171 | it('ES6 classes', function () { 172 | function Class(a) { 173 | this.a = a; 174 | } 175 | 176 | var c = new Class('a'); 177 | 178 | eq(validate(c, Class), success(c)); 179 | eq(validate(1, Class), failure(1, Class, [], 'Invalid value 1 supplied to Class', 1)); 180 | }); 181 | 182 | it('intersection', function () { 183 | var Min = t.refinement(t.String, function (s) { return s.length > 2; }, 'Min'); 184 | var Max = t.refinement(t.String, function (s) { return s.length < 5; }, 'Max'); 185 | var MinMax = t.intersection([Min, Max], 'MinMax'); 186 | 187 | eq(validate(1, MinMax), { 188 | errors: [ 189 | { 190 | message: 'Invalid value 1 supplied to String', 191 | actual: 1, 192 | expected: t.String, 193 | path: [] 194 | }, 195 | { 196 | message: 'Invalid value 1 supplied to String', 197 | actual: 1, 198 | expected: t.String, 199 | path: [] 200 | } 201 | ], 202 | value: 1 203 | }); 204 | 205 | eq(validate('aa', MinMax), { 206 | errors: [ 207 | { 208 | message: 'Invalid value "aa" supplied to Min', 209 | actual: 'aa', 210 | expected: Min, 211 | path: [] 212 | } 213 | ], 214 | value: 'aa' 215 | }); 216 | 217 | eq(validate('aaaaa', MinMax), { 218 | errors: [ 219 | { 220 | message: 'Invalid value "aaaaa" supplied to Max', 221 | actual: 'aaaaa', 222 | expected: Max, 223 | path: [] 224 | } 225 | ], 226 | value: 'aaaaa' 227 | }); 228 | 229 | eq(validate('aaa', MinMax), { 230 | errors: [], 231 | value: 'aaa' 232 | }); 233 | 234 | eq(validate({name: 'aa'}, t.struct({name: MinMax})), { 235 | errors: [ 236 | { 237 | message: 'Invalid value "aa" supplied to /name: Min', 238 | actual: 'aa', 239 | expected: Min, 240 | path: ['name'] 241 | } 242 | ], 243 | value: {name: 'aa'} 244 | }); 245 | 246 | var StructIntersection = t.intersection([t.struct({string: Min}), t.struct({string: Max})], 'StructIntersection'); 247 | 248 | eq(validate({string: 'Test'}, StructIntersection), { 249 | errors: [ 250 | { 251 | message: 'Invalid value {\n \"string\": \"Test\"\n} supplied to StructIntersection', 252 | actual: {string: 'Test'}, 253 | expected: StructIntersection, 254 | path: [] 255 | } 256 | ], 257 | value: {string: 'Test'} 258 | }); 259 | 260 | }); 261 | 262 | describe('options argument', function () { 263 | 264 | it('should handle a path key', function () { 265 | eq(validate(1, t.String, ['prefix']), failure(1, t.String, ['prefix'], 'Invalid value 1 supplied to /prefix: String', 1)); 266 | }); 267 | 268 | it('should handle a context key', function () { 269 | var context = {a: 1}; 270 | var ShortString = t.refinement(t.String, function (s) { return s.length < 3; }); 271 | ShortString.getValidationErrorMessage = function (value, path, ctx) { 272 | assert.strictEqual(ctx, context); 273 | return 'mymessage' + ctx.a; 274 | }; 275 | eq(validate('abc', ShortString, {context: context}).firstError().message, 'mymessage1'); 276 | assert.deepEqual(validate('abc', ShortString, {context: context, path: ['a']}).firstError().path, ['a']); 277 | }); 278 | 279 | it('should handle a strict boolean', function () { 280 | eq(validate({x: 0, y: 0}, Point, {strict: true}), success({x: 0, y: 0})); 281 | eq(validate({x: 0, y: 0, z: 0}, Point, {strict: true}), failure(0, t.Nil, ['z'], 'Invalid value 0 supplied to /z: Nil', {x: 0, y: 0})); 282 | eq(validate({x: 0, y: 0, z: null}, Point, {strict: true}), failure(null, t.Nil, ['z'], 'Invalid value null supplied to /z: Nil', {x: 0, y: 0})); 283 | eq(validate({x: 0, y: 0, z: undefined}, Point, {strict: true}), failure(undefined, t.Nil, ['z'], 'Invalid value undefined supplied to /z: Nil', {x: 0, y: 0})); 284 | eq(validate({x: 0, y: 0, z: 0}, PointInterface, {strict: true}), failure(0, t.Nil, ['z'], 'Invalid value 0 supplied to /z: Nil', {x: 0, y: 0})); 285 | }); 286 | 287 | it('should handle a strict boolean with nested structures', function () { 288 | var InnerType = t.struct({point: Point}); 289 | var List = t.list(InnerType); 290 | var T = t.refinement(List, function (x) { 291 | return x.length >= 2; 292 | }); 293 | eq(validate([{point: {x: 0, y: 0}}, {point: {x: 0, y: 0}}], T, {strict: true}), success([{point: {x: 0, y: 0}}, {point: {x: 0, y: 0}}])); 294 | eq(validate([{point: {x: 0, y: 0, z: 0}}, {point: {x: 0, y: 0}}], T, {strict: true}), failure(0, t.Nil, [0, 'point', 'z'], 'Invalid value 0 supplied to /0/point/z: Nil', [{point: {x: 0, y: 0}}, {point: {x: 0, y: 0}}])); 295 | }); 296 | 297 | }); 298 | 299 | it('should handle a strict struct', function () { 300 | var StrictStruct = t.struct({ 301 | name: t.String 302 | }, { name: 'StrictStruct', strict: true }); 303 | eq(validate({name: 'Giulio'}, StrictStruct), success({name: 'Giulio'})); 304 | eq(validate({name: 'Giulio', a: 1}, StrictStruct), failure(1, t.Nil, ['a'], 'Invalid value 1 supplied to /a: Nil', {name: 'Giulio'})); 305 | }); 306 | 307 | it('should handle a strict interface', function () { 308 | var StrictInterface = t.inter({ 309 | name: t.String 310 | }, { name: 'StrictStruct', strict: true }); 311 | eq(validate({name: 'Giulio'}, StrictInterface), success({name: 'Giulio'})); 312 | eq(validate({name: 'Giulio', a: 1}, StrictInterface), failure(1, t.Nil, ['a'], 'Invalid value 1 supplied to /a: Nil', {name: 'Giulio'})); 313 | }); 314 | 315 | }); 316 | 317 | describe('getValidationErrorMessage(value, context)', function () { 318 | 319 | var MyString = t.irreducible('MyIrreducible', function (x) { 320 | return typeof x === 'string' && x.length > 1; 321 | }); 322 | MyString.getValidationErrorMessage = function (value) { 323 | if (!MyString.is(value)) { 324 | return 'Invalid string'; 325 | } 326 | }; 327 | 328 | var ShortString = t.refinement(t.String, function (s) { 329 | return s.length < 3; 330 | }); 331 | ShortString.getValidationErrorMessage = function (value) { 332 | if (!ShortString.is(value)) { 333 | return 'Too long my friend'; 334 | } 335 | }; 336 | 337 | it('should handle custom validation error messages on irreducibles', function () { 338 | eq(validate(1, MyString), failure(1, MyString, [], 'Invalid string', 1)); 339 | }); 340 | 341 | it('should handle custom validation error messages on subtypes', function () { 342 | ShortString.getValidationErrorMessage = function (value) { 343 | if (!ShortString.is(value)) { 344 | return 'Too long my friend'; 345 | } 346 | }; 347 | eq(validate('aaa', ShortString), failure('aaa', ShortString, [], 'Too long my friend', 'aaa')); 348 | }); 349 | 350 | it('should handle custom validation error messages on intersections', function () { 351 | var Intersection = t.intersection([MyString, ShortString]); 352 | eq(validate('', Intersection), failure('', MyString, [], 'Invalid string', '')); 353 | eq(validate('aaa', Intersection), failure('aaa', ShortString, [], 'Too long my friend', 'aaa')); 354 | }); 355 | 356 | it('should allow to return custom validation objects', function () { 357 | ShortString.getValidationErrorMessage = function (value) { 358 | if (!ShortString.is(value)) { 359 | return { foo: 'bar' }; 360 | } 361 | }; 362 | eq(validate('aaa', ShortString), failure('aaa', ShortString, [], { foo: 'bar' }, 'aaa')); 363 | }); 364 | 365 | }); 366 | 367 | describe('ValidationResult.toString', function () { 368 | it('should return string representation when valid', function () { 369 | const res = success(true); 370 | eq('[ValidationResult, true, true]', res.toString()); 371 | }); 372 | 373 | it('should return string representation when invalid', function () { 374 | const res = failure(1, t.String, [], 'Invalid value 1 supplied to String', 1); 375 | eq('[ValidationResult, false, ("Invalid value 1 supplied to String")]', res.toString()); 376 | }); 377 | 378 | it('should return string representation when invalid with custom message', function () { 379 | const res = failure(1, t.String, [], { very: 'error' }, 1); 380 | eq('[ValidationResult, false, ({\n "very": "error"\n})]', res.toString()); 381 | }); 382 | }); 383 | --------------------------------------------------------------------------------