├── .babelrc.js ├── .eslintrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .nycrc ├── .travis.yml ├── README.md ├── docs ├── assets │ └── css │ │ └── main.css └── index.html ├── example.html ├── index.d.ts ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── errors.js ├── operators │ ├── Check.js │ ├── Compose.js │ ├── Const.js │ ├── Each.js │ ├── Enum.js │ ├── Keys.js │ ├── Nullable.js │ ├── Optional.js │ ├── Required.js │ ├── Some.js │ └── types.js ├── trava.js ├── trava.umd.js └── utils.js └── test ├── common.js ├── errors.js └── operators.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | const presetOptions = { 2 | useBuiltIns: 'entry', 3 | corejs: '3', 4 | }; 5 | const exclude = []; 6 | const plugins = []; 7 | 8 | if (process.env.BABEL_ENV === 'es') { 9 | presetOptions.targets = { "esmodules": true }; 10 | } else { 11 | presetOptions.targets = '> 0.25%, not dead'; 12 | exclude.push('node_modules/**'); 13 | plugins.push( 14 | ['@babel/plugin-proposal-object-rest-spread', { loose: true }], 15 | '@babel/plugin-transform-object-assign' 16 | ); 17 | } 18 | 19 | if (process.env.NODE_ENV === 'test') { 20 | presetOptions.targets = { node: 'current' }; 21 | delete presetOptions.modules; 22 | plugins.push(['istanbul', { 23 | exclude: ['test/**/*.js'], 24 | include: ['src/**/*.js'], 25 | }]); 26 | } 27 | 28 | module.exports = { 29 | presets: [ 30 | ['@babel/preset-env', presetOptions], 31 | '@babel/preset-flow' 32 | ], 33 | plugins, 34 | exclude, 35 | }; 36 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:flowtype/recommended" 4 | ], 5 | 6 | "plugins": [ 7 | "flowtype" 8 | ], 9 | 10 | "env": { 11 | "browser": true, 12 | "es6": true, 13 | "mocha": true 14 | }, 15 | 16 | "parserOptions": { 17 | "ecmaVersion": 6, 18 | "sourceType": "module", 19 | 20 | "ecmaFeatures": { 21 | "classes": true 22 | }, 23 | }, 24 | 25 | "globals": { 26 | "Trava": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | .*/test/.* 4 | .*/dist/.* 5 | 6 | [include] 7 | .*/src/.* 8 | 9 | [libs] 10 | 11 | [lints] 12 | 13 | [options] 14 | 15 | [strict] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | dist/ 4 | docs/_site/ 5 | *.log 6 | .nyc_output 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !LICENSE 3 | !README.md 4 | !index.d.ts 5 | !dist 6 | !dist/**/* 7 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "require": [ 3 | "@babel/register" 4 | ], 5 | "include": [ 6 | "src/**/*.js" 7 | ], 8 | "exclude": [ 9 | "test/**/*.js" 10 | ], 11 | "reporter": [ 12 | "text", 13 | "text-summary" 14 | ], 15 | "all": true, 16 | "cache": false, 17 | "sourceMap": false 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: "node" 3 | cache: npm 4 | sudo: required 5 | script: npm run make 6 | after_success: npm run coveralls 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # travajs 2 | vanilla javascript validation library with coercion 3 | 4 | [![Build Status](https://travis-ci.com/uNmAnNeR/travajs.svg?branch=master)](https://travis-ci.com/uNmAnNeR/travajs) 5 | [![Coverage Status](https://coveralls.io/repos/github/uNmAnNeR/travajs/badge.svg?branch=master)](https://coveralls.io/github/uNmAnNeR/travajs?branch=master) 6 | [![npm version](https://badge.fury.io/js/trava.svg)](https://badge.fury.io/jas/trava) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 8 | 9 | ## Features 10 | * Node and Browser support 11 | * easy to use 12 | * no external dependencies 13 | * customizable and extendable 14 | 15 | ## Install 16 | `npm install trava` and `import Trava from 'trava';` 17 | 18 | or use CDN: 19 | 20 | `` 21 | 22 | ## Build & Test 23 | `npm run make` 24 | 25 | ## Compatibility 26 | Supports all major browsers and IE11+ 27 | 28 | ## Docs, Examples, Demo 29 | [https://unmanner.github.io/travajs/](https://unmanner.github.io/travajs/) 30 | 31 | ## Many Thanks to 32 | [@Artem Gorev](https://github.com/ArtemGorev) 33 | 34 | ## Support Development 35 | [Paypal](https://www.paypal.me/alexeykryazhev/3) 36 | -------------------------------------------------------------------------------- /docs/assets/css/main.css: -------------------------------------------------------------------------------- 1 | header { 2 | margin: 30px 0; 3 | text-align: center; 4 | } 5 | 6 | .section-h { 7 | margin-top: 50px; 8 | } 9 | 10 | .section-h a { 11 | text-decoration: none; 12 | } 13 | 14 | .section-h:after { 15 | content: '#'; 16 | font-size: 15px; 17 | font-weight: normal; 18 | line-height: 1; 19 | color: rgba(0, 0, 0, 0.3); 20 | margin-left: 8px; 21 | } 22 | 23 | nav { 24 | margin: 0; 25 | padding: 20px 0; 26 | border-top: 1px dashed rgba(0, 0, 0, 0.15); 27 | border-bottom: 1px dashed rgba(0, 0, 0, 0.15); 28 | } 29 | 30 | nav ul { 31 | flex-wrap: wrap; 32 | } 33 | 34 | header nav { 35 | margin-top: 30px; 36 | } 37 | 38 | h1 sub { 39 | font-size: 30px; 40 | font-style: italic; 41 | } 42 | 43 | a:hover { 44 | transition: all linear 0.2s; 45 | text-decoration: underline; 46 | } 47 | 48 | body { 49 | max-width: 800px; 50 | margin: 0 auto; 51 | } 52 | 53 | a { 54 | text-decoration: none; 55 | } 56 | 57 | body { 58 | padding: 0 20px; 59 | } 60 | 61 | .form-item { 62 | clear: both; 63 | } 64 | 65 | footer { 66 | text-align: center; 67 | margin-bottom: 30px; 68 | } 69 | 70 | footer nav { 71 | border-bottom: none; 72 | } 73 | 74 | .spin-wrapper { 75 | width: 100%; 76 | } 77 | .spin-wrapper .spin-button { 78 | min-height: 0; 79 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | travajs - vanilla javascript validation library 7 | 8 | 9 | 10 | 13 | 14 | 15 |
16 |
17 |

travajs

18 | vanilla javascript validation library 19 |
20 | 29 |
30 |
31 |

Trava stands for TRAnsform and VAlidate. Inspired by struct.

32 |

The main goal of the library is to provide highly extendable and customizable way of JavaScript entities validation. Validation often goes along with parsing or transforming values. So coercion feature is included by design into Trava.

33 | 34 | 35 |

Features

36 | 42 | 43 | 44 |

Install

45 |

Install from npm:

46 |
npm install trava
47 |

And import or require:

48 |
import Trava from 'trava';
49 |

or use CDN:

50 |
<script src="https://unpkg.com/trava"></script>
51 |

For modern browsers es201X builds are available (trava.es.js and trava.es.min.js).

52 | 53 | 54 |

Getting Started

55 | Let's imaging the most obvious and simple validator. Probably it will look like: 56 |
function validate (value) {
 57 |   // any checking...
 58 |   if (!check(value)) return false;
 59 |   return true;
 60 | }
61 | But in real scenarios we'd also like to get some error details, e.g.: 62 |
function validate (value) {
 63 |   if (!check1(value)) return 'ERROR_1';
 64 |   if (!check2(value)) return 'ERROR_2';
 65 |   return true;
 66 | }
67 |

We may stop here and probably you don't need any library to do validation this way, may be just some primitives for common cases. But there is one more feature we might want to get and when Trava could help. While using JSON it's often needed to parse or convert values after validation. Code for parsing looks pretty similar to validation: its just validate being replaced with parse. In Trava these steps are united together. To support both validation and transformation we need to distinguish error from transformed value returned from validator. Probably we could use js Error, but it works only with string messages. Fortunately Trava has own ValidationError to support complex errors. 68 |

69 |

So Trava validator looks like:

70 |
function validate (value) {
 71 |   if (!check1(value)) return new Trava.ValidationError({ code: 401 });
 72 |   if (!check2(value)) return new Trava.ValidationError({ code: 405 });
 73 |   return parseOrTransform(value);  // apply some parse or transform
 74 | }
75 |

Then use validator:

76 |
const result = validate(data);
 77 | if (result instanceof Trava.ValidationError) {
 78 |   // note using `data` property to extract error data
 79 |   console.log('This is error!', result.data);
 80 | } else {
 81 |   const goodTransformedValue = result;
 82 | }
 83 | 
84 |

That's all you have to know to start using Trava. It is very simple. The second advantage why to use Trava is a collection of helpful operators out of the box. See examples to learn how to use them.

85 | 86 | 87 |

Operators

88 | 102 |

Despite operators itself are just functions, it's convinient to use Trava to build validators or validate data directly:

103 |
import Trava from 'trava';
104 | let validator = Trava(validators);
105 | let values = validator(data);
106 | 
107 | // or validate directly
108 | values = Trava(validators, data);
109 | 
110 | 111 |

Compose

112 |

Compose is used to combine several validators into one. E.g.:

113 |
const validator1 = n => n < 10 ? n : new ValidationError('BAD NUMBER');
114 | const validator2 = ...;
115 | const validator3 = ...;
116 | const composedValidator = Trava.Compose([validator1, validator2, validator3]);
117 | // then `composedValidator` could be used just like simple validator
118 | 
119 |

Validation goes consequently from left to right. When an error occurs, validation stops and an error is returned immediately.

120 |

When used inside other operators explicit call Trava.Compose could be omitted. E.g.:

121 |
const composedValidator = Trava.Required(Trava.Compose([v1, v2, v3]));
122 | // is same as
123 | const composedValidator = Trava.Required([v1, v2, v3]);
124 | 
125 | 126 |

Required

127 |

Required is a guard to check if a value is defined (!== undefined):

128 |
const validator = ...;
129 | const requiredValidator = Trava.Required(validator);
130 | 
131 | let value;
132 | console.log(requiredValidator(value)); // ValidationError('Value is required')
133 | 
134 | value = 'any';
135 | console.log(requiredValidator(value)); // `Required` is bypassed
136 | 
137 |

Custom error message can be set by providing it as a second argument:

138 |
const requiredValidator = Trava.Required(validator, 'My custom error message');
139 |

Or set the default for all validators:

140 |
Trava.Required.ErrorMessage = 'My default required error';
141 |

Error message also could be a function which should return error data and will be called with same arguments as validator when error occurs.

142 |
const validator = Trava.Check(v => v > 0, v => `${v} is not positive number!`);
143 | 144 |

Optional

145 |

Optional checks if value is not defined then returns value or default value which can be provided as a second argument:

146 |
const optionalValidator = Trava.Optional(validator, 'default value');
147 | 148 |

Nullable

149 |

Nullable is just like Optional except it also checks if value is not null.

150 | 151 |

Check

152 | Check is helper to reuse validators which return boolean: 153 |
const myExistingValidator = (v) => v < 10;
154 | const travaValidator = Trava.Check(myExistingValidator);
155 | 
156 | console.log(travaValidator(20)); // ValidationError('Incorrect value')
157 | 
158 |

Custom error message can be set by providing it as a second argument:

159 |
const checkValidator = Trava.Check(n => Boolean(n), 'My custom error message');
160 |

Or use the following to set the default one:

161 |
Trava.Check.ErrorMessage = 'My default check error';
162 | 163 |

Enum

164 | Enum checks if value exists in enum: 165 |
const enumValidator = Trava.Enum(['a', 'b']);
166 | console.log(enumValidator('c')); // ValidationError('Incorrect value')
167 | 
168 |

Just like Check it accepts an error message as a second argument.

169 | 170 |

Const

171 | Const checks if value equals: 172 |
const constValidator = Trava.Const('a');
173 | console.log(constValidator('c')); // ValidationError('Incorrect value')
174 | 
175 |

Just like Check it accepts an error message as a second argument.

176 | 177 |

Each

178 |

Each is usefull for validating uniform data structures (so far works only with arrays). Errors are aggregated in object by keys (or indices):

179 |
const elementValidator = n => n < 10 ? n : new ValidationError('BAD NUMBER');
180 | const arrayValidator = Trava.Each(elementValidator);
181 | 
182 | console.log(arrayValidator([1, 15, 7, 5, 20]));
183 | // ValidationError({2: 'BAD NUMBER', 4: 'BAD NUMBER'})
184 | 
185 |
186 | Note: Values of Each are wrapped with Required by default, use Optional otherwise. 187 |
188 | 189 |

Keys

190 |

Keys is used to validate Objects:

191 |
const objectValidator = Trava.Keys({
192 |   a: Trava.Check(a => a >= 0),
193 |   b: Trava.Required(Trava.Check(b => b.startsWith('nice'), 'HEY, think positive!')),
194 | });
195 | 
196 | console.log(objectValidator({
197 |   a: -1,
198 |   b: 'bad wrong error'
199 | }));
200 | // ValidationError({
201 | //   a: 'Incorrect value'.
202 | //   b: 'HEY, think positive!',
203 | // })
204 | 
205 |

When used inside other operators explicit call Trava.Keys could be omitted. E.g.:

206 |
const objectValidator = Trava.Required({
207 |   a: Trava.Check(a => a >= 0),
208 |   b: Trava.Check(b => b < 0),
209 | });
210 | 
211 |
212 | Note: Values of Keys are wrapped with Required by default, use Optional otherwise. 213 |
214 | 215 |

Some

216 |

Some is like Compose but tries to find first non error.

217 |
const someValidator = Trava.Some([
218 |   Trava.Check(isString, 'Not a string'),
219 |   Trava.Check(isNumber, 'Not a number'),
220 | ]);
221 | 
222 | console.log(someValidator(1));   // 1
223 | console.log(someValidator('a')); // 'a'
224 | console.log(someValidator({}));  // ValidationError('Not a number') <-- latest error
225 | 
226 | 227 |

Examples

228 |
const t = require('trava');
229 | const { Required, Optional, Each, Enum, Check, Keys, Some, ValidationError } = t;
230 | 
231 | const isString = s => typeof s === 'string';
232 | const isEmail = s => /^\S+@\S+\.\S+$/.test(s);
233 | 
234 | const validateForm = t({
235 |   username: Check(un => isString(un) && /^\w{3,30}$/.test(un), 'CUSTOM ERROR: INVALID USERNAME!'), // required by default
236 |   password: Optional(Check(pwd => isString(pwd) && pwd.length >= 6)),
237 |   access_token: Optional(Some([isString, Number.isInteger]), 'default token'),
238 |   birthyear: Optional(Check(b => Number.isInteger(b) && 1900 <= b && b <= 2018)),
239 |   email: Optional(Check(isEmail)),
240 |   contacts: Optional(Each({
241 |     name: Enum(['phone', 'email']),
242 |     value: Check(isString),
243 |   }))
244 | });
245 | 
246 | const values = validateForm({
247 |   username: 'HelloTrava',
248 |   password: 'secretoversecret',
249 |   birthyear: 1990,
250 | });
251 | if (values instanceof ValidationError) {
252 |   console.error('FORM ERRORS', values.data);
253 | } else {
254 |   console.log('FORM VALUES', values);
255 | }
256 | 
257 |
258 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 45 | 46 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export as namespace Trava; 2 | export default Trava; 3 | 4 | 5 | declare function Trava (scheme: Trava.MixedValidator): Trava.Validator; 6 | declare function Trava (scheme: Trava.MixedValidator, value: any): any; 7 | 8 | declare namespace Trava { 9 | export type ValidateResult = any | Error; 10 | export type Validator = (data: any, ...args: any[]) => ValidateResult; 11 | export interface ObjectValidator { [k: string]: MixedValidator } 12 | export type MixedValidator = Validator | Array | ObjectValidator; 13 | export type ValueAccessorValidator = Validator & { __valueAccessor: boolean }; 14 | 15 | export interface EachOptions { 16 | errorsTo: any; // actually available values are {Object, Array} 17 | requiredMessage?: any; 18 | } 19 | export function Each (vs: MixedValidator, opts?: EachOptions): Validator; 20 | export function Keys (vMap: ObjectValidator): Validator; 21 | export function Required (vs: MixedValidator, errorMsg?: any): Validator; 22 | export function Optional (vs: MixedValidator, defaultValue?: any): Validator; 23 | export function Nullable (vs: MixedValidator, defaultValue?: any): Validator; 24 | export function Check (fn: (value: any) => boolean, errorMsg?: any): Validator; 25 | export function Enum (values: Array, errorMsg?: any): Validator; 26 | export function Const (value: any, errorMsg?: any): Validator; 27 | export function Compose (vs: MixedValidator): Validator; 28 | export function Some (vs: Array): Validator; 29 | 30 | export function asValueAccessor (fn: Function): ValueAccessorValidator; 31 | export function isValueAccessor (fn: Validator | ValueAccessorValidator): boolean; 32 | export class ValidationError extends Error { 33 | message: string; 34 | data: T; 35 | 36 | static extractData (error: any): string; 37 | static stringify (error: any): string; 38 | 39 | constructor (data: T); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Alexey Kryazhev", 3 | "name": "trava", 4 | "version": "1.2.1", 5 | "license": "MIT", 6 | "description": "vanilla javascript validation library", 7 | "main": "dist/trava.js", 8 | "module": "dist/trava.esm.js", 9 | "bugs": "https://github.com/uNmAnNeR/travajs/issues", 10 | "homepage": "https://unmanner.github.io/travajs/", 11 | "repository": "https://github.com/uNmAnNeR/travajs", 12 | "types": "index.d.ts", 13 | "devDependencies": { 14 | "@babel/core": "^7.6.4", 15 | "@babel/plugin-proposal-class-properties": "^7.5.5", 16 | "@babel/plugin-proposal-object-rest-spread": "^7.6.2", 17 | "@babel/plugin-transform-object-assign": "^7.2.0", 18 | "@babel/plugin-transform-runtime": "^7.6.2", 19 | "@babel/preset-env": "^7.6.3", 20 | "@babel/preset-flow": "^7.0.0", 21 | "@babel/register": "^7.6.2", 22 | "babel-eslint": "^10.0.3", 23 | "babel-plugin-istanbul": "^5.2.0", 24 | "chai": "^4.1.2", 25 | "coveralls": "^3.0.7", 26 | "cross-env": "^6.0.3", 27 | "documentation": "^12.1.2", 28 | "eslint-plugin-flowtype": "^4.3.0", 29 | "flow-bin": "^0.110.1", 30 | "mocha": "^6.2.2", 31 | "nyc": "^14.1.1", 32 | "rollup": "^1.25.2", 33 | "rollup-plugin-babel": "^4.3.3", 34 | "rollup-plugin-commonjs": "^10.1.0", 35 | "rollup-plugin-eslint": "^7.0.0", 36 | "rollup-plugin-node-resolve": "^5.2.0", 37 | "rollup-plugin-sourcemaps": "^0.4.2", 38 | "rollup-plugin-terser": "^5.1.2", 39 | "sinon": "^7.5.0" 40 | }, 41 | "engines": { 42 | "npm": ">=4.0.0", 43 | "node": ">=6.0.0" 44 | }, 45 | "scripts": { 46 | "doc": "echo 'TODO'", 47 | "flow": "flow", 48 | "pretest": "flow check", 49 | "watch": "rollup -c -w", 50 | "build": "npm run build:umd && npm run build:es && npm run build:esm", 51 | "build:umd": "rollup -c", 52 | "build:es": "cross-env BABEL_ENV=es rollup -c", 53 | "build:esm": "cross-env BABEL_ENV=esm rollup -c", 54 | "test": "cross-env NODE_ENV=test nyc mocha --exit --recursive", 55 | "make": "npm run test && npm run build", 56 | "release": "npm run doc && npm run make && npm publish", 57 | "coveralls": "nyc report --reporter=text-lcov | coveralls" 58 | }, 59 | "keywords": [ 60 | "javascript", 61 | "validate" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import { terser } from 'rollup-plugin-terser'; 3 | import { eslint } from 'rollup-plugin-eslint'; 4 | import resolve from 'rollup-plugin-node-resolve'; 5 | import commonjs from 'rollup-plugin-commonjs'; 6 | 7 | 8 | const format = process.env.BABEL_ENV || 'umd'; 9 | 10 | const isES = format.indexOf('es') === 0; 11 | const basePath = 'dist/trava' + (format !== 'umd' ? '.' + format : ''); 12 | 13 | 14 | export default [false, true].map(min => ({ 15 | input: `src/trava${format === 'umd' ? '.umd' : ''}.js`, 16 | output: { 17 | file: `${basePath}${min ? '.min' : ''}.js`, 18 | format, 19 | name: 'Trava', 20 | sourcemap: true, 21 | }, 22 | plugins: [ 23 | eslint({configFile: '.eslintrc'}), 24 | resolve(), 25 | babel(), 26 | !isES && commonjs(), 27 | min && terser(), 28 | ], 29 | })); 30 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { isString } from './utils'; 3 | 4 | 5 | export 6 | class ValidationError extends Error { 7 | message: string; 8 | data: T; 9 | 10 | static extractData (error: any): string { 11 | if (error instanceof ValidationError) return error.data; 12 | return String(error); 13 | } 14 | 15 | static stringify (error: any): string { 16 | if (error instanceof ValidationError) return ValidationError.stringify(error.data); 17 | if (error instanceof Error) return error.message; 18 | if (isString(error)) return error; 19 | 20 | try { 21 | return JSON.stringify(error); 22 | } catch (e) {} 23 | 24 | try { 25 | return String(error); 26 | } catch (e) {} 27 | 28 | return ''; 29 | } 30 | 31 | constructor (data: T) { 32 | super(ValidationError.stringify(data)); 33 | this.name = 'ValidationError'; 34 | this.data = data; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/operators/Check.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { ValidationError } from '../errors'; 3 | import { prepareErrorMessage } from '../utils'; 4 | import { type MixedValidator, type Validator } from './types'; 5 | 6 | 7 | export default 8 | function Check (fn: (value: any) => boolean, errorMsg: any=Check.ErrorMessage): Validator { 9 | return function (value: any, ...args: *) { 10 | return fn(value, ...args) ? value : new ValidationError(prepareErrorMessage(errorMsg, value, ...args)); 11 | }; 12 | } 13 | Check.ErrorMessage = "Incorrect value"; 14 | -------------------------------------------------------------------------------- /src/operators/Compose.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type MixedValidator, type Validator } from './types'; 3 | import { ValidationError } from '../errors'; 4 | import { g } from '../utils'; 5 | 6 | 7 | export default 8 | function Compose (mv: MixedValidator = (v) => v): Validator { 9 | if (!Array.isArray(mv)) { 10 | if (mv && typeof mv === 'object') return g.Trava.Keys(mv); 11 | return mv; 12 | } 13 | const vs = mv.map(Compose); 14 | 15 | return function (value: any, ...args: *) { 16 | let res = value; 17 | 18 | for (let i=0; i value === v, ...args); 9 | } 10 | -------------------------------------------------------------------------------- /src/operators/Each.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Compose from './Compose'; 3 | import Required from './Required'; 4 | import { ValidationError } from '../errors'; 5 | import { isValueAccessor } from '../utils'; 6 | import { type MixedValidator, type Validator } from './types'; 7 | 8 | 9 | type EachOptions = { 10 | errorsTo: Class | Class>; 11 | requiredMessage?: any; 12 | } 13 | // TODO Currently works only for Arrays 14 | export default 15 | function Each (mv: MixedValidator, eachOpts: EachOptions=Each.DEFAULTS): Validator { 16 | let v: Validator = Compose(mv); 17 | // make keys required by default 18 | if (!isValueAccessor(v)) v = Required(v, eachOpts.requiredMessage); 19 | 20 | return function (coll, ...args: *) { 21 | let errors; 22 | const valid = []; 23 | 24 | for (let i=0; i, ...args: *): Validator { 8 | return Check(value => values.indexOf(value) >= 0, ...args); 9 | } 10 | -------------------------------------------------------------------------------- /src/operators/Keys.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Compose from './Compose'; 3 | import Required from './Required'; 4 | import { ValidationError } from '../errors'; 5 | import { isValueAccessor, isObject } from '../utils'; 6 | import { type MixedValidator, type Validator } from './types'; 7 | 8 | 9 | export default 10 | function Keys (vMap: { [string]: MixedValidator }): Validator { 11 | return function (coll: { [string]: any }, ...args: *) { 12 | let errors; 13 | const valid = {}; 14 | 15 | Object.keys(vMap).forEach(k => { 16 | let validator = Compose(vMap[k]); 17 | // make keys required by default 18 | if (!isValueAccessor(validator)) validator = Required(validator); 19 | 20 | const res = validator(coll && coll[k], k, coll, ...args); 21 | if (res instanceof Error) { 22 | if (!errors) errors = {}; 23 | errors[k] = ValidationError.extractData(res); 24 | } else if (isObject(coll) && k in coll || res !== undefined) { 25 | valid[k] = res; 26 | } 27 | }); 28 | 29 | return errors ? 30 | new ValidationError(errors) : 31 | valid; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/operators/Nullable.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Compose from './Compose'; 3 | import { asValueAccessor } from '../utils'; 4 | import { type MixedValidator } from './types'; 5 | 6 | 7 | export default 8 | function Nullable (vs: MixedValidator, defaultValue: any=null) { 9 | const v = Compose(vs); 10 | 11 | return asValueAccessor(function (value: any, ...args: *) { 12 | if (value == null) return defaultValue; 13 | return v(value, ...args); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/operators/Optional.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Compose from './Compose'; 3 | import { asValueAccessor } from '../utils'; 4 | import { type MixedValidator, type Validator } from './types'; 5 | 6 | 7 | export default 8 | function Optional (vs: MixedValidator, defaultValue: any): Validator { 9 | const v = Compose(vs); 10 | 11 | return asValueAccessor(function (value, ...args) { 12 | if (value === undefined) return defaultValue; 13 | return v(value, ...args); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/operators/Required.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import Compose from './Compose'; 3 | import { asValueAccessor, prepareErrorMessage } from '../utils'; 4 | import { ValidationError } from '../errors'; 5 | import { type MixedValidator, type Validator } from './types'; 6 | 7 | 8 | export default 9 | function Required (vs: MixedValidator, errorMsg: any=Required.ErrorMessage): Validator { 10 | const v = Compose(vs); 11 | 12 | return asValueAccessor(function (value, ...args) { 13 | if (value === undefined) return new ValidationError(prepareErrorMessage(errorMsg, value, ...args)); 14 | return v(value, ...args); 15 | }); 16 | } 17 | Required.ErrorMessage = "Value is required"; 18 | -------------------------------------------------------------------------------- /src/operators/Some.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { type Validator, type MixedValidator } from './types'; 3 | import Compose from './Compose'; 4 | 5 | 6 | export default 7 | function Some (mv: Array): Validator { 8 | const vs = mv.map(Compose); 9 | 10 | return function (value: any, ...args: *) { 11 | let res; 12 | 13 | for (let i=0; i ValidateResult; 7 | 8 | export 9 | type ObjectValidator = { [string]: MixedValidator }; 10 | 11 | export 12 | type MixedValidator = Validator | Array | ObjectValidator; 13 | -------------------------------------------------------------------------------- /src/trava.js: -------------------------------------------------------------------------------- 1 | import { g, asValueAccessor, isValueAccessor } from './utils'; 2 | 3 | import Each from './operators/Each'; 4 | import Keys from './operators/Keys'; 5 | import Required from './operators/Required'; 6 | import Optional from './operators/Optional'; 7 | import Nullable from './operators/Nullable'; 8 | import Check from './operators/Check'; 9 | import Enum from './operators/Enum'; 10 | import Compose from './operators/Compose'; 11 | import Some from './operators/Some'; 12 | import Const from './operators/Const'; 13 | 14 | import { ValidationError } from './errors'; 15 | 16 | 17 | const Trava = function (scheme, ...args) { 18 | const vs = Compose(scheme); 19 | if (!args.length) return vs; 20 | // possible opts to implement: 21 | // - skipErrors - return just valid fields, skip errors 22 | // - wrapExceptions - treat exceptions like validation errors 23 | return vs(...args); 24 | }; 25 | 26 | Trava.Each = Each; 27 | Trava.Keys = Keys; 28 | Trava.Required = Required; 29 | Trava.Optional = Optional; 30 | Trava.Nullable = Nullable; 31 | Trava.Check = Check; 32 | Trava.Enum = Enum; 33 | Trava.Compose = Compose; 34 | Trava.Some = Some; 35 | Trava.Const = Const; 36 | Trava.ValidationError = ValidationError; 37 | Trava.asValueAccessor = asValueAccessor; 38 | Trava.isValueAccessor = isValueAccessor; 39 | 40 | 41 | g.Trava = Trava; 42 | export default Trava; 43 | export { 44 | Each, 45 | Keys, 46 | Required, 47 | Optional, 48 | Nullable, 49 | Check, 50 | Enum, 51 | Compose, 52 | Some, 53 | Const, 54 | ValidationError, 55 | asValueAccessor, 56 | isValueAccessor, 57 | }; 58 | -------------------------------------------------------------------------------- /src/trava.umd.js: -------------------------------------------------------------------------------- 1 | import Trava from './trava'; 2 | 3 | export default Trava; 4 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import type { Validator, ValidateResult } from './operators/types'; 3 | 4 | 5 | /** Checks if value is string */ 6 | export 7 | function isString (str: mixed): boolean %checks { 8 | return typeof str === 'string' || str instanceof String; 9 | } 10 | 11 | export 12 | function isObject (obj: any): boolean %checks { 13 | return typeof obj === 'object' && obj != null || typeof obj === 'function'; 14 | } 15 | 16 | type ValueAccessorValidator = { 17 | (data: any, ...args: *): ValidateResult, 18 | __valueAccessor: boolean, 19 | }; 20 | export 21 | function asValueAccessor (fn: Function): ValueAccessorValidator { 22 | fn.__valueAccessor = true; 23 | return fn; 24 | } 25 | 26 | export 27 | function isValueAccessor (fn: Validator | ValueAccessorValidator): boolean { 28 | return '__valueAccessor' in fn; 29 | } 30 | 31 | export 32 | function prepareErrorMessage (errorMsg: any, ...args: *) { 33 | if (typeof errorMsg === 'function') return errorMsg(...args); 34 | return errorMsg; 35 | } 36 | 37 | /* eslint-disable no-undef */ 38 | export 39 | const g: any = typeof window !== 'undefined' && window || 40 | typeof global !== 'undefined' && global.global === global && global || 41 | typeof self !== 'undefined' && self.self === self && self || 42 | {}; 43 | /* eslint-enable no-undef */ 44 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | import Trava from '../src/trava'; 2 | import Compose from '../src/operators/Compose'; 3 | import Check from '../src/operators/Check'; 4 | import Optional from '../src/operators/Optional'; 5 | import assert from 'assert'; 6 | 7 | 8 | describe('Trava root', function () { 9 | it('create validator if 1 arg', function () { 10 | const scheme = { 11 | a: [ 12 | Check(a => a > 0), 13 | Check(a => a < 10), 14 | ], 15 | b: { 16 | c: Optional(c => c > 5, 10), 17 | }, 18 | }; 19 | 20 | const data = { 21 | a: 5, 22 | b: { } 23 | }; 24 | 25 | const tv = Trava(scheme); 26 | const cv = Compose(scheme); 27 | 28 | assert.deepEqual(tv(data), cv(data), 'Incorrect result'); 29 | }); 30 | 31 | it('create validator and validate if 2 args', function () { 32 | const scheme = { 33 | a: [ 34 | Check(a => a > 0), 35 | Check(a => a < 10), 36 | ], 37 | b: { 38 | c: Optional(c => c > 5, 10), 39 | }, 40 | }; 41 | 42 | const data = { 43 | a: 5, 44 | b: { } 45 | }; 46 | 47 | const tres = Trava(scheme, data); 48 | const cv = Compose(scheme); 49 | 50 | assert.deepEqual(tres, cv(data), 'Incorrect result'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/errors.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import sinon, { fake, spy } from 'sinon'; 3 | 4 | import { ValidationError } from '../src/errors'; 5 | import { prepareErrorMessage } from '../src/utils'; 6 | 7 | 8 | describe('ValidationError', function () { 9 | it('should create error', function () { 10 | const errorData = { a: 1 }; 11 | const error = new ValidationError(errorData); 12 | 13 | assert.equal(error.name, 'ValidationError', 'Error is not ValidationError'); 14 | assert.equal(error.data, errorData, 'Incorrect error data'); 15 | }); 16 | 17 | it('should set message from object', function () { 18 | const errorData = { a: 1 }; 19 | const error = new ValidationError(errorData); 20 | 21 | assert.equal(error.message, JSON.stringify(errorData), 'Incorrect error message (from Object)'); 22 | }); 23 | 24 | it('should set message from Error', function () { 25 | const errorMsg = 'ERROR'; 26 | const errorData = new Error(errorMsg); 27 | const error = new ValidationError(errorData); 28 | 29 | assert.equal(error.message, errorMsg, 'Incorrect error message (from Error)'); 30 | }); 31 | 32 | it('should set message from ValidationError', function () { 33 | const errorData = { a: 1 }; 34 | const error = new ValidationError(errorData); 35 | const error2 = new ValidationError(error); 36 | 37 | assert.equal(error.message, error2.message, 'Incorrect error message (from ValidationError) (1)'); 38 | assert.equal(error2.message, JSON.stringify(errorData), 'Incorrect error message (from ValidationError) (2)'); 39 | }); 40 | 41 | it('should extract data from String', function () { 42 | const errorStr = 'ERROR'; 43 | const error = new ValidationError(errorStr); 44 | 45 | assert.equal(error.message, errorStr, 'Can not extract error data from String'); 46 | }); 47 | 48 | it('should extract data from Stringifyable', function () { 49 | const errorStr = 'ERROR'; 50 | const errorData = { toJSON() { throw 'error'; }, toString() { return errorStr; } }; 51 | const error = new ValidationError(errorData); 52 | 53 | assert.equal(error.message, errorStr, 'Can not extract error data from Stringifyable'); 54 | }); 55 | 56 | it('should extract data from any', function () { 57 | const errorStr = 'DATA'; 58 | const errorData = { toString() { return errorStr; } }; 59 | const error = new ValidationError(errorData); 60 | 61 | assert.equal(ValidationError.extractData(error), errorStr, 'Can not extract error data from any'); 62 | }); 63 | 64 | it('should extract data from string', function () { 65 | const errorStr = 'DATA'; 66 | 67 | assert.equal(ValidationError.extractData(errorStr), errorStr, 'Can not extract error data from string'); 68 | }); 69 | 70 | it('should extract error from function', function () { 71 | const err = fake.returns('ERR'); 72 | const args = [0, '1', true]; 73 | 74 | prepareErrorMessage(err, ...args); 75 | 76 | assert(err.lastCall.calledWith(...args), 'Invalid prepare error call'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/operators.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import sinon, { fake, spy } from 'sinon'; 3 | 4 | import Trava from '../src/trava'; 5 | import Check from '../src/operators/Check'; 6 | import Compose from '../src/operators/Compose'; 7 | import Each from '../src/operators/Each'; 8 | import Enum from '../src/operators/Enum'; 9 | import Keys from '../src/operators/Keys'; 10 | import Nullable from '../src/operators/Nullable'; 11 | import Optional from '../src/operators/Optional'; 12 | import Required from '../src/operators/Required'; 13 | import Some from '../src/operators/Some'; 14 | import Const from '../src/operators/Const'; 15 | import { ValidationError } from '../src/errors'; 16 | import { isValueAccessor } from '../src/utils'; 17 | 18 | 19 | describe('Operators', function () { 20 | afterEach(() => sinon.restore()); 21 | 22 | describe('Check', function () { 23 | it('should return data', function () { 24 | const data = 'DATA'; 25 | const v = Check(value => true); 26 | const validData = v(data); 27 | 28 | assert.equal(validData, data, 'Check returns invalid data'); 29 | }); 30 | 31 | it('should return error', function () { 32 | const data = 'DATA'; 33 | const errorMsg = 'ERROR'; 34 | const v = Check(value => false, errorMsg); 35 | const validData = v(data); 36 | 37 | assert(validData instanceof ValidationError, 'Check result is not error'); 38 | assert.equal(validData.message, errorMsg, 'Invalid error message'); 39 | }); 40 | 41 | it('should format error with function', function () { 42 | const POS_MSG = 'POS'; 43 | const errorMsg = val => (val > 0 ? POS_MSG : 'NEG'); 44 | const v = Check(value => false, errorMsg); 45 | 46 | const resPos = v(1); 47 | assert(resPos instanceof ValidationError, 'Check result is not error'); 48 | assert.equal(resPos.message, POS_MSG, 'Invalid error message'); 49 | }); 50 | }); 51 | 52 | 53 | describe('Enum', function () { 54 | it('should work', function () { 55 | const v = Enum([1, 2, 3]); 56 | 57 | assert.equal(v(1), 1, 'Enum returns invalid data'); 58 | assert(v(4) instanceof ValidationError, 'Enum result is not error'); 59 | }); 60 | }); 61 | 62 | 63 | describe('Compose', function () { 64 | it('should return single validator', function () { 65 | const v = () => true; 66 | 67 | assert.equal(Compose(v), v, 'Composed validator is not the same as single validator'); 68 | }); 69 | 70 | it('should return composition of several validators', function () { 71 | const v1 = fake.returns(1); 72 | const v2 = fake.returns(2); 73 | const v = Compose([v1, v2]); 74 | 75 | assert.equal(v(0), 2, 'Invalid composition'); 76 | assert(v1.lastCall.calledWith(0), 'Invalid argument 1'); 77 | assert(v2.lastCall.calledWith(1), 'Invalid argument 2'); 78 | }); 79 | 80 | it('should return first error of composed', function () { 81 | const error = 'ERROR'; 82 | const v0 = fake.returns(0); 83 | const v1 = fake.returns(new ValidationError(error)); 84 | const v2 = fake.returns(2); 85 | const v = Compose([v0, v1, v2]); 86 | 87 | const d = v(0); 88 | 89 | assert(d instanceof ValidationError, 'Invalid result'); 90 | assert.equal(d.message, error, 'Invalid error message'); 91 | assert(v1.lastCall.calledWith(0), 'Invalid argument 1'); 92 | assert(!v2.lastCall, 'Invalid validator call'); 93 | }); 94 | 95 | it('should be called recursively', function () { 96 | const v1 = fake.returns(1); 97 | const v2 = fake.returns(2); 98 | const v = Compose([[v1, v2]]); 99 | 100 | assert.equal(v(0), 2, 'Invalid composition'); 101 | assert(v1.lastCall.calledWith(0), 'Invalid argument 1'); 102 | assert(v2.lastCall.calledWith(1), 'Invalid argument 2'); 103 | }); 104 | 105 | it('should work with objects', function () { 106 | const spyKeys = spy(Trava, 'Keys'); 107 | const v = Compose([{ a: () => 1 }]); 108 | 109 | assert(spyKeys.calledOnce, 'Trava.Keys is not called'); 110 | 111 | spyKeys.restore(); 112 | }); 113 | }); 114 | 115 | describe('Each', function () { 116 | it('should validate elements', function () { 117 | const v = Each(Check(e => e > 0)); 118 | const data = [1, 2, 3]; 119 | 120 | assert.deepEqual(v(data), data, 'Invalid each data check'); 121 | }); 122 | 123 | it('should return error', function () { 124 | const error = 'ERROR'; 125 | const v = Each(Check(e => e > 0, error)); 126 | const data = [0, 1, 2, 3]; 127 | 128 | const r = v(data); 129 | 130 | assert(r instanceof ValidationError, 'Result is not error'); 131 | assert.deepEqual(r.data, { 0: error }, 'Invalid error data'); 132 | }); 133 | 134 | it('should save errors to array', function () { 135 | const error = 'ERROR'; 136 | const v = Each(Check(e => e > 1, error), { errorsTo: Array }); 137 | const data = [0, 1, 2, 3]; 138 | 139 | const r = v(data); 140 | 141 | assert(r instanceof ValidationError, 'Result is not error'); 142 | assert(Array.isArray(r.data), 'Error data is not Array'); 143 | assert.deepEqual(r.data, [ error, error ], 'Invalid error data'); 144 | }); 145 | 146 | it('should apply Required by default', function () { 147 | const error = 'ERROR'; 148 | const requiredError = 'REQUIRED!'; 149 | const v = Each(Check(e => e > 0, error), { requiredMessage: requiredError }); 150 | const data = [undefined, 1, 2]; 151 | 152 | const r = v(data); 153 | 154 | assert(r instanceof ValidationError, 'Result is not error'); 155 | assert.deepEqual(r.data, { 0: requiredError }, 'Invalid error data'); 156 | }); 157 | 158 | it('should not apply Required if another accessor is set', function () { 159 | const error = 'ERROR'; 160 | const v = Each(Optional(Check(e => e > 0, error))); 161 | const data = [undefined, 1, 2]; 162 | 163 | const r = v(data); 164 | 165 | assert.deepEqual(r, data, 'Invalid result') 166 | }); 167 | 168 | it('should compose nested validators', function () { 169 | const error = 'ERROR'; 170 | const v = Each([ 171 | Check(a => a > 0), 172 | Check(a => a < 10), 173 | ]); 174 | 175 | assert.deepEqual(v([1, 2]), [1, 2], 'Invalid result'); 176 | assert(v([0, 1]) instanceof ValidationError, 'Result is not error'); 177 | assert(v([1, 11]) instanceof ValidationError, 'Result is not error'); 178 | }); 179 | }); 180 | 181 | describe('Keys', function () { 182 | it('should validate values', function () { 183 | const v = Keys({ 184 | a: Check(a => a > 0), 185 | c: Optional(c => c >= 5, 5), 186 | }); 187 | 188 | const data = { a: 1, b: 2 }; 189 | 190 | const r = v(data); 191 | 192 | assert.deepEqual(r, { a: 1, c: 5 }, 'Invalid result'); 193 | assert(!('b' in r), 'Contains invalid key'); 194 | }); 195 | 196 | it('should return error', function () { 197 | const error = 'ERROR'; 198 | 199 | const v = Keys({ 200 | a: Check(a => a > 0, error) 201 | }); 202 | 203 | const data = { 204 | a: 0, 205 | b: 2, 206 | }; 207 | 208 | const r = v(data); 209 | 210 | assert(r instanceof ValidationError, 'Result is not error'); 211 | assert.deepEqual(r.data, { a: error }, 'Invalid error'); 212 | }); 213 | 214 | it('should compose nested validators', function () { 215 | const v = Keys({ 216 | a: [ 217 | Check(a => a > 0), 218 | Check(a => a < 10), 219 | ] 220 | }); 221 | 222 | assert.deepEqual(v({ a: 1 }), { a: 1 }, 'Invalid result'); 223 | assert(v({ a: 0 }) instanceof ValidationError, 'Result is not error'); 224 | assert(v({ a: 10 }) instanceof ValidationError, 'Result is not error'); 225 | }); 226 | 227 | it('should handle non-objects', function () { 228 | const v = Keys({ a: Check(Number.isInteger) }); 229 | const vOpt = Keys({ a: Optional(Check(Number.isInteger)) }); 230 | 231 | assert(v('string is not object!') instanceof ValidationError, 'Result is not error'); 232 | assert.deepEqual(vOpt('string is not object!'), {}, 'Invalid result'); 233 | }); 234 | }); 235 | 236 | describe('Nullable', function () { 237 | it('should bypass if not null', function () { 238 | const v = Nullable(c => c > 0); 239 | 240 | assert.equal(v(1), 1, 'Invalid result'); 241 | assert.equal(v(null), null, 'Result is not null'); 242 | }); 243 | 244 | it('should return default if null', function () { 245 | const v = Nullable(c => c > 0, 1); 246 | 247 | assert.equal(v(null), 1, 'Result is null'); 248 | }); 249 | 250 | it('should mark as value accessor', function () { 251 | const v = Nullable(c => c > 0); 252 | 253 | assert(isValueAccessor(v), 'Not a value accessor'); 254 | }); 255 | 256 | it('should compose nested validators', function () { 257 | const v = Nullable([ 258 | Check(a => a > 0), 259 | Check(a => a < 10), 260 | ]); 261 | 262 | assert.equal(v(1), 1, 'Invalid result'); 263 | assert(v(0) instanceof ValidationError, 'Result is not error'); 264 | assert(v(10) instanceof ValidationError, 'Result is not error'); 265 | }); 266 | }); 267 | 268 | describe('Optional', function () { 269 | it('should bypass if not null', function () { 270 | const v = Optional(c => c > 0); 271 | 272 | assert.equal(v(1), 1, 'Invalid result'); 273 | assert.equal(v(undefined), undefined, 'Result is not null'); 274 | }); 275 | 276 | it('should return default if null', function () { 277 | const v = Optional(c => c > 0, 1); 278 | 279 | assert.equal(v(undefined), 1, 'Result is null'); 280 | }); 281 | 282 | it('should return bypass null', function () { 283 | const v = Optional(a => a == null ? 0 : 1); 284 | 285 | assert.equal(v(null), 0, 'Invalid result'); 286 | assert.equal(v(5), 1, 'Invalid result'); 287 | }); 288 | 289 | it('should mark as value accessor', function () { 290 | const v = Optional(c => c > 0); 291 | 292 | assert(isValueAccessor(v), 'Not a value accessor'); 293 | }); 294 | 295 | it('should compose nested validators', function () { 296 | const v = Optional([ 297 | Check(a => a > 0), 298 | Check(a => a < 10), 299 | ]); 300 | 301 | assert.equal(v(1), 1, 'Invalid result'); 302 | assert(v(0) instanceof ValidationError, 'Result is not error'); 303 | assert(v(10) instanceof ValidationError, 'Result is not error'); 304 | }); 305 | }); 306 | 307 | describe('Required', function () { 308 | it('should bypass if defined', function () { 309 | const errorMsg = 'ERROR'; 310 | const v = Required(c => c > 0, errorMsg); 311 | 312 | assert.equal(v(1), 1, 'Invalid result'); 313 | 314 | const err = v(undefined); 315 | assert(err instanceof ValidationError, 'Result is not error'); 316 | assert.equal(err.message, errorMsg, 'Invalid error message'); 317 | }); 318 | 319 | it('should mark as value accessor', function () { 320 | const v = Required(c => c > 0); 321 | 322 | assert(isValueAccessor(v), 'Not a value accessor'); 323 | }); 324 | 325 | it('should compose nested validators', function () { 326 | const v = Required([ 327 | Check(a => a > 0), 328 | Check(a => a < 10), 329 | ]); 330 | 331 | assert.equal(v(1), 1, 'Invalid result'); 332 | assert(v(0) instanceof ValidationError, 'Result is not error'); 333 | assert(v(10) instanceof ValidationError, 'Result is not error'); 334 | }); 335 | 336 | it('should format error with function', function () { 337 | const POS_MSG = 'POS'; 338 | const errorMsg = (val, arg) => (arg > 0 ? POS_MSG : 'NEG'); 339 | const v = Required(value => value, errorMsg); 340 | 341 | const resPos = v(undefined, 1); 342 | assert(resPos instanceof ValidationError, 'Check result is not error'); 343 | assert.equal(resPos.message, POS_MSG, 'Invalid error message'); 344 | }); 345 | }); 346 | 347 | describe('Some', function () { 348 | it('should return first non error', function () { 349 | const v = Some([ 350 | Check(c => c < 1), 351 | Check(c => c > 10), 352 | ]); 353 | 354 | assert.equal(v(0), 0, 'Invalid result'); 355 | assert.equal(v(11), 11, 'Invalid result'); 356 | }); 357 | 358 | it('should return last error', function () { 359 | const errorMsg1 = 'MORE THAN 1'; 360 | const errorMsg2 = 'LESS THAN 10'; 361 | const v = Some([ 362 | Check(c => c < 1, errorMsg1), 363 | Check(c => c > 10, errorMsg2), 364 | ]); 365 | 366 | const err = v(5); 367 | assert(err instanceof ValidationError, 'Result is not error'); 368 | assert.equal(err.message, errorMsg2, 'Invalid error message'); 369 | }); 370 | 371 | it('should compose nested validators', function () { 372 | const v = Some([ 373 | [Check(a => a > 0), Check(a => a < 10)], 374 | [Check(a => a > 20), Check(a => a < 30)], 375 | ]); 376 | 377 | assert.equal(v(1), 1, 'Invalid result'); 378 | assert.equal(v(25), 25, 'Invalid result'); 379 | assert(v(0) instanceof ValidationError, 'Result is not error'); 380 | assert(v(15) instanceof ValidationError, 'Result is not error'); 381 | }); 382 | }); 383 | 384 | describe('Const', function () { 385 | it('should work', function () { 386 | const v = Const(1); 387 | 388 | assert.equal(v(1), 1, 'Const returns invalid data'); 389 | assert(v(4) instanceof ValidationError, 'Const result is not error'); 390 | }); 391 | }); 392 | }); 393 | --------------------------------------------------------------------------------