├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── revalid.js └── test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | index.js 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | COPYRIGHT (c) 2016 James Kyle 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # revalid 2 | 3 | Composable validators 4 | 5 | --- 6 | 7 | **Table of Contents:** 8 | [**Install**](#install) · 9 | [**Example**](#example) · 10 | [**How it works**](#how-it-works) · 11 | [**API**](#api) · 12 | [**Validators**](#validators) 13 | 14 | --- 15 | 16 | ## Install 17 | 18 | ```sh 19 | $ npm install --save revalid 20 | ``` 21 | 22 | ## Example 23 | 24 | ```js 25 | const { 26 | composeValidators, 27 | combineValidators, 28 | minLength, maxLength, pattern, matchesField 29 | } = require('revalid'); 30 | 31 | const containsLetters = pattern(/[a-zA-Z]/, 'letters'); 32 | const containsNumbers = pattern(/[0-9]/, 'numbers'); 33 | 34 | const password = composeValidators(minLength(8), containsLetters, containsNumbers); 35 | const passwordConfirm = composeValidators(password, matchesField('password')); 36 | 37 | const validatePasswordForm = combineValidators({ 38 | password, 39 | passwordConfirm 40 | }); 41 | 42 | const result = validatePasswordForm({ 43 | password: 'ThisPasswordIsNotSecureEnough', 44 | passwordConfirm: 'ThisIsADifferentPassword' 45 | }); 46 | // { 47 | // isValid: false, 48 | // validationErrors: { 49 | // password: { 50 | // type: 'pattern', 51 | // label: 'numbers', 52 | // pattern: /[0-9]/, 53 | // value: 'ThisPasswordIsNotSecureEnough' 54 | // }, 55 | // passwordConfirm: { 56 | // type: 'matchesField', 57 | // fieldName: 'password', 58 | // value: 'ThisIsADifferentPassword', 59 | // other: 'ThisPasswordIsNotSecureEnough' 60 | // } 61 | // } 62 | // } 63 | ``` 64 | 65 | If you want to make a field **optional**, you need to explicitly make it so: 66 | 67 | ```js 68 | const { composeValidators, minLength, maxLength, optional } = require('revalid'); 69 | 70 | const firstName = composeValidators(minLength(2), maxLength(30)); 71 | const optionalFirstName = optional(firstName); 72 | 73 | optionalFirstName(undefined) // >> false 74 | optionalFirstName(null) // >> false 75 | optionalFirstName("") // >> false 76 | optionalFirstName("x") // >> { type: 'minLength', minLength: 2, value: 'x' } 77 | optionalFirstName("xyz") // >> false 78 | ``` 79 | 80 | > Note: There is no `required`, validators are required by default. 81 | 82 | ## How it works 83 | 84 | Lets start by taking a look at one of our validators: `matchesField`. 85 | 86 | Validators in **revalid** are just plain functions that return functions. So 87 | let's start by doing that. 88 | 89 | ```js 90 | function matchesField(fieldName) { 91 | return function(value, fields) { 92 | // ... 93 | }; 94 | } 95 | ``` 96 | 97 | Notice how the returned function accepts two arguments: `value` and `fields`. 98 | 99 | Now we can write the actual validation logic. There are two things that the 100 | inner function can return: 101 | 102 | 1. `false` for when the validator **has no error**. 103 | 2. `{ type: String, ... }` for when the validator **has an error**. 104 | 105 | Let's write our success case: 106 | 107 | ```js 108 | function matchesField(fieldName) { 109 | return function(value, fields) { 110 | const other = fields[fieldName]; 111 | if (value === other) { 112 | return false; 113 | } 114 | }; 115 | } 116 | ``` 117 | 118 | Again, see that the success case is returning `false` because there isn't an 119 | error. 120 | 121 | Now for the error case which will return a plain object with a `type` that 122 | describes the validator and any additional properties that explain the error 123 | further. 124 | 125 | ```js 126 | function matchesField(fieldName) { 127 | return function(value, fields) { 128 | const other = fields[fieldName]; 129 | if (value === other) { 130 | return false; 131 | } else { 132 | return { type: 'matchesField', fieldName, value, other }; 133 | } 134 | }; 135 | } 136 | ``` 137 | 138 | And that's it! That is our validator. Now let's use it. 139 | 140 | If we want to use the validator directly, we do so like this: 141 | 142 | ```js 143 | const { matchesField } = require('revalid'); 144 | 145 | const fields = { password: 'hunter2' }; 146 | const matchesPassword = matchesField('password'); 147 | 148 | const validationError1 = matchesPassword('hunter2'); // >> false 149 | const validationError2 = matchesPassword('hunter3'); // >> { type: 'matchesField', ... } 150 | ``` 151 | 152 | We can `composeValidators` together like so: 153 | 154 | ```js 155 | const { composeValidators, minLength, maxLength } = require('revalid'); 156 | 157 | const firstName = composeValidators(minLength(2), maxLength(30)); 158 | 159 | const validationError1 = matchesPassword('Hunter'); // >> false 160 | const validationError2 = matchesPassword('Q'); // >> { type: 'minLength', ... } 161 | ``` 162 | 163 | Finally, you can `combineValidators` together to validate multiple fields at 164 | once. 165 | 166 | ```js 167 | const { combineValidators, ... } = require('revalid'); 168 | 169 | // ... 170 | 171 | const validatePasswordForm = combineValidators({ 172 | password, 173 | passwordConfirm 174 | }); 175 | 176 | const formData = { 177 | password: 'ThisPasswordIsNotSecureEnough', 178 | passwordConfirm: 'ThisIsADifferentPassword' 179 | }; 180 | 181 | const result = validatePasswordForm(formData); 182 | ``` 183 | 184 | In the above example, `result` is the following: 185 | 186 | ```js 187 | { 188 | isValid: false, 189 | validationErrors: { 190 | password: { 191 | type: 'pattern', 192 | label: 'numbers', 193 | pattern: /[0-9]/, 194 | value: 'ThisPasswordIsNotSecureEnough' 195 | }, 196 | passwordConfirm: { 197 | type: 'matchesField', 198 | fieldName: 'password', 199 | value: 'ThisIsADifferentPassword', 200 | other: 'ThisPasswordIsNotSecureEnough' 201 | } 202 | } 203 | } 204 | ``` 205 | 206 | A successful result will look like this: 207 | 208 | ```js 209 | { 210 | isValid: true, 211 | validationErrors: {} 212 | } 213 | ``` 214 | 215 | ## API 216 | 217 | ##### `composeValidators(...validators) => validator` 218 | 219 | Compose together multiple validators into a single validator. The validators 220 | will run serially until one of them returns a validation error returning that 221 | validation error. If no errors are returned then it will return `false` (just 222 | like any other validator). 223 | 224 | Composed validators can be nested within other composed validators. 225 | 226 | ```js 227 | const { composeValidators } = require('revalid'); 228 | 229 | const twoToThirty = composeValidators(minLength(2), maxLength(30)); 230 | const twoToThirtyLetters = composeValidators(twoToThirty, pattern(/^[a-zA-Z]+$/)); 231 | ``` 232 | 233 | ##### `combineValidators({ [key]: validator }) => combinedValidator` 234 | 235 | Create a function that can validate multiple fields at once. 236 | 237 | ```js 238 | const { combineValidators } = require('revalid'); 239 | 240 | const validateForm = combineValidators({ 241 | password: password, 242 | passwordConfirm: passwordConfirm 243 | }); 244 | 245 | const result = validateForm({ 246 | password: 'examplepassword', 247 | passwordConfirm: 'examplepassword' 248 | }); 249 | ``` 250 | 251 | When there are no errors: 252 | 253 | ```js 254 | { 255 | isValid: true, 256 | validationErrors: {} 257 | } 258 | ``` 259 | 260 | When there are errors: 261 | 262 | ```js 263 | { 264 | isValid: false, 265 | validationErrors: { 266 | password: { 267 | // ... 268 | } 269 | } 270 | } 271 | ``` 272 | 273 | ##### `optional(validator) => validator` 274 | 275 | Take an existing validator and make it optional, meaning that it can be 276 | `undefined`, `null`, or `''`. 277 | 278 | ```js 279 | const { optional, ... } = require('revalid'); 280 | 281 | const validator = minLength(10); 282 | const optionalValidator = optional(validator); 283 | 284 | validator(''); // error – { type: 'minLength', ... } 285 | optionalValidator(''); // success – false 286 | optionalValidator('abc'); // error – { type: 'minLength', ... } 287 | ``` 288 | 289 | ### Validators 290 | 291 | ##### `minLength(length) => validator` 292 | 293 | Creates a validator that passes when it has a string that is at least `length` 294 | characters long. 295 | 296 | **Example Error:** 297 | 298 | ```js 299 | { type: 'minLength', minLength: 15, value: "example-string" } 300 | ``` 301 | 302 | ##### `maxLength(length) => validator` 303 | 304 | Creates a validator that passes when it has a string that is at most `length` 305 | characters long. 306 | 307 | **Example Error:** 308 | 309 | ```js 310 | { type: 'maxLength', minLength: 13, value: "example-string" } 311 | ``` 312 | 313 | ##### `min(number) => validator` 314 | 315 | Creates a validator that passes when it has a string that is greater than or 316 | equal to `number`. 317 | 318 | **Example Error:** 319 | 320 | ```js 321 | { type: 'min', min: 10, value: 9 } 322 | ``` 323 | 324 | ##### `max(number) => validator` 325 | 326 | Creates a validator that passes when it has a string that is less than or 327 | equal to `number`. 328 | 329 | **Example Error:** 330 | 331 | ```js 332 | { type: 'max', min: 10, value: 11 } 333 | ``` 334 | 335 | ##### `pattern(regex, label) => validator` 336 | 337 | Creates a validator that passes when the `regex` matches a string. `label` 338 | should be used to describe the `regex`. 339 | 340 | **Example Error:** 341 | 342 | ```js 343 | { type: 'pattern', label: 'containsNumbers', pattern: /[0-9]/, value: 'abc' } 344 | ``` 345 | 346 | ##### `equalTo(other) => validator` 347 | 348 | Creates a validator that passes when a value is strictly equal `===` to 349 | `other`. 350 | 351 | **Example Error:** 352 | 353 | ```js 354 | { type: 'equalTo', other: 'foo', value: 'bar' } 355 | ``` 356 | 357 | ##### `oneOf(array) => validator` 358 | 359 | Creates a validator that passes when a value is equal to another one of the 360 | values in `array` (uses `indexOf`). 361 | 362 | **Example Error:** 363 | 364 | ```js 365 | { type: 'oneOf', values: [1, 2, 3], value: 4 } 366 | ``` 367 | 368 | ##### `matchesField(fieldName) => validator` 369 | 370 | Creates a validator that passes when a value is equal to the `fieldName` 371 | field's value (Useful for email or password confirmation). 372 | 373 | **Example Error:** 374 | 375 | ```js 376 | { type: 'matchesField', fieldName: 'password', value: 'hunter3', other: 'hunter2' } 377 | ``` 378 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "revalid", 3 | "version": "1.0.2", 4 | "description": "Composable validators", 5 | "main": "index.js", 6 | "author": "James Kyle ", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "babel-cli": "^6.9.0", 10 | "babel-preset-es2015": "^6.9.0", 11 | "babel-register": "^6.9.0" 12 | }, 13 | "scripts": { 14 | "build": "babel revalid.js -o index.js", 15 | "prepublish": "npm run build", 16 | "test": "mocha test.js --compilers js:babel-register --ui tdd" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /revalid.js: -------------------------------------------------------------------------------- 1 | exports.composeValidators = (...validators) => (value, fields) => { 2 | for (let i = 0; i < validators.length; i++) { 3 | const validator = validators[i]; 4 | const result = validator(value, fields); 5 | if (result) { 6 | return result; 7 | } 8 | } 9 | return false; 10 | }; 11 | 12 | exports.combineValidators = validators => { 13 | const keys = Object.keys(validators); 14 | return function validate(fields) { 15 | let validationErrors = {}; 16 | let isValid = true; 17 | 18 | for (let i = 0; i < keys.length; i++) { 19 | const key = keys[i]; 20 | const validator = validators[key]; 21 | const result = validator(fields[key], fields); 22 | 23 | if (result) { 24 | isValid = false; 25 | validationErrors[key] = result; 26 | } 27 | } 28 | 29 | return { isValid, validationErrors }; 30 | }; 31 | }; 32 | 33 | exports.optional = validator => (value, fields) => { 34 | if (value === undefined || value === null || value === '') { 35 | return false; 36 | } else { 37 | return validator(value, fields); 38 | } 39 | }; 40 | 41 | exports.minLength = minLength => value => { 42 | if (value && value.length >= minLength) { 43 | return false; 44 | } else { 45 | return { type: 'minLength', minLength, value }; 46 | } 47 | }; 48 | 49 | exports.maxLength = maxLength => value => { 50 | if (value && value.length <= maxLength) { 51 | return false; 52 | } else { 53 | return { type: 'maxLength', maxLength, value }; 54 | } 55 | }; 56 | 57 | exports.min = min => value => { 58 | if (isFinite(value) && parseFloat(value) >= min) { 59 | return false; 60 | } else { 61 | return { type: 'min', min, value }; 62 | } 63 | }; 64 | 65 | exports.max = max => value => { 66 | if (isFinite(value) && parseFloat(value) <= max) { 67 | return false; 68 | } else { 69 | return { type: 'max', max, value }; 70 | } 71 | }; 72 | 73 | exports.pattern = (pattern, label) => value => { 74 | if (pattern.test(value)) { 75 | return false; 76 | } else { 77 | return { type: 'pattern', label, pattern, value }; 78 | } 79 | }; 80 | 81 | exports.equalTo = other => value => { 82 | if (value === other) { 83 | return false; 84 | } else { 85 | return { type: 'equalTo', other, value }; 86 | } 87 | }; 88 | 89 | exports.oneOf = values => value => { 90 | if (values.indexOf(value) > -1) { 91 | return false; 92 | } else { 93 | return { type: 'oneOf', values, value }; 94 | } 95 | }; 96 | 97 | exports.matchesField = fieldName => (value, fields) => { 98 | const other = fields[fieldName]; 99 | if (value === other) { 100 | return false; 101 | } else { 102 | return { type: 'matchesField', fieldName, value, other }; 103 | } 104 | }; 105 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { 3 | composeValidators, combineValidators, optional, 4 | minLength, maxLength, min, max, pattern, equalTo, oneOf, matchesField 5 | } = require('./revalid'); 6 | 7 | suite('revalid', () => { 8 | test('end-to-end', function() { 9 | const containsLetters = pattern(/[a-zA-Z]/, 'containsLetters'); 10 | const containsNumbers = pattern(/[0-9]/, 'containsNumbers'); 11 | 12 | const password = composeValidators(minLength(8), containsLetters, containsNumbers); 13 | const passwordConfirm = composeValidators(password, matchesField('password')); 14 | 15 | const validatePasswordForm = combineValidators({ 16 | password, 17 | passwordConfirm 18 | }); 19 | 20 | const validResult = validatePasswordForm({ 21 | password: 'GoodPassword123', 22 | passwordConfirm: 'GoodPassword123' 23 | }); 24 | 25 | const invalidResult = validatePasswordForm({ 26 | password: 'ThisPasswordIsNotSecureEnough', 27 | passwordConfirm: 'ThisIsADifferentPassword1' 28 | }); 29 | 30 | assert.deepEqual(validResult, { 31 | isValid: true, 32 | validationErrors: {} 33 | }); 34 | 35 | assert.deepEqual(invalidResult, { 36 | isValid: false, 37 | validationErrors: { 38 | password: { 39 | type: 'pattern', 40 | label: 'containsNumbers', 41 | pattern: /[0-9]/, 42 | value: 'ThisPasswordIsNotSecureEnough' 43 | }, 44 | passwordConfirm: { 45 | type: 'matchesField', 46 | fieldName: 'password', 47 | value: 'ThisIsADifferentPassword1', 48 | other: 'ThisPasswordIsNotSecureEnough' 49 | } 50 | } 51 | }); 52 | }); 53 | 54 | test('optional()', () => { 55 | const fields = { a: 1 }; 56 | const validator = optional(matchesField('a')); 57 | 58 | assert.deepEqual(validator(undefined, fields), false); 59 | assert.deepEqual(validator(null, fields), false); 60 | assert.deepEqual(validator('', fields), false); 61 | assert.deepEqual(validator(1, fields), false); 62 | assert.deepEqual(validator(2, fields), { type: 'matchesField', fieldName: 'a', value: 2, other: 1 }); 63 | }); 64 | 65 | suite('validators', () => { 66 | test('minLength()', () => { 67 | assert.deepEqual(minLength(5)('123456'), false); 68 | assert.deepEqual(minLength(5)('12345'), false); 69 | assert.deepEqual(minLength(5)('1234'), { type: 'minLength', minLength: 5, value: '1234' }); 70 | }); 71 | 72 | test('maxLength()', () => { 73 | assert.deepEqual(maxLength(5)('1234'), false); 74 | assert.deepEqual(maxLength(5)('12345'), false); 75 | assert.deepEqual(maxLength(5)('123456'), { type: 'maxLength', maxLength: 5, value: '123456' }); 76 | }); 77 | 78 | test('min()', () => { 79 | assert.deepEqual(min(5)(6), false); 80 | assert.deepEqual(min(5)(5), false); 81 | assert.deepEqual(min(5)(4), { type: 'min', min: 5, value: 4 }); 82 | }); 83 | 84 | test('max()', () => { 85 | assert.deepEqual(max(5)(4), false); 86 | assert.deepEqual(max(5)(5), false); 87 | assert.deepEqual(max(5)(6), { type: 'max', max: 5, value: 6 }); 88 | }); 89 | 90 | test('pattern()', () => { 91 | assert.deepEqual(pattern(/^abc$/, "ABC")("abc"), false); 92 | assert.deepEqual(pattern(/^abc$/, "ABC")("xyz"), { type: 'pattern', label: 'ABC', pattern: /^abc$/, value: "xyz" }); 93 | }); 94 | 95 | test('equalTo()', () => { 96 | const a = {}; 97 | const b = {}; 98 | assert.deepEqual(equalTo(a)(a), false); 99 | assert.deepEqual(equalTo(a)(b), { type: 'equalTo', other: {}, value: {} }); 100 | }); 101 | 102 | test('oneOf()', () => { 103 | const a = {}; 104 | const b = {}; 105 | const c = {}; 106 | assert.deepEqual(oneOf([a, b])(a), false); 107 | assert.deepEqual(oneOf([a, b])(b), false); 108 | assert.deepEqual(oneOf([a, b])(c), { type: 'oneOf', values: [a, b], value: c }); 109 | }); 110 | 111 | test('matchesField()', () => { 112 | const fields = { a: 1, b: 2 }; 113 | assert.deepEqual(matchesField('a')(1, fields), false); 114 | assert.deepEqual(matchesField('b')(1, fields), { type: 'matchesField', fieldName: 'b', value: 1, other: 2 }); 115 | }); 116 | }); 117 | }); 118 | --------------------------------------------------------------------------------