├── .nvmrc ├── docs ├── .nojekyll ├── .DS_Store ├── _assets │ ├── logo.png │ ├── favicon.ico │ ├── stylesheet.css │ └── index.js ├── getting_started │ ├── installation.md │ ├── writing_tests.md │ ├── callbacks.md │ └── result.md ├── _sidebar.md ├── compatability │ └── assertions.md ├── index.html ├── test │ ├── warn_only_tests.md │ ├── how_to_fail.md │ ├── index.md │ ├── async.md │ └── specific.md ├── utilities │ └── README.md ├── enforce.md.bak ├── README.md └── enforce.md ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md ├── .eslintignore ├── config ├── babel-register.js ├── test-setup.js ├── babel.config.js └── rollup.js ├── scripts ├── prepare_next.sh ├── get_next_version.js ├── prep_docs.js ├── handle_changes.sh ├── constants.js ├── determine_change_level.js ├── push_tag_to_master.sh ├── get_diff.js ├── create_release.js └── update_changelog.js ├── src ├── core │ ├── test │ │ ├── lib │ │ │ ├── index.js │ │ │ ├── isTestFn │ │ │ │ ├── index.js │ │ │ │ └── spec.js │ │ │ └── TestObject │ │ │ │ ├── index.js │ │ │ │ └── spec.js │ │ ├── index.js │ │ └── spec.js │ ├── draft │ │ ├── constants.js │ │ ├── index.js │ │ └── spec.js │ ├── Context │ │ ├── index.js │ │ └── spec.js │ ├── passable │ │ ├── index.js │ │ └── spec.js │ ├── Specific │ │ ├── index.js │ │ └── spec.js │ └── passableResult │ │ └── index.js ├── lib │ ├── index.js │ ├── globalObject │ │ └── index.js │ └── singleton │ │ ├── constants.js │ │ ├── index.js │ │ └── spec.js ├── spec │ ├── fail.d.ts │ ├── ts.spec.js │ ├── react.elements.support.spec.js │ ├── passable.api.severity.spec.js │ ├── passable.exports.spec.js │ └── passable.api.specific.spec.js ├── constants.js ├── utilities │ └── validate │ │ ├── index.js │ │ └── spec.js └── index.js ├── .npmignore ├── .gitignore ├── playground └── index.js ├── LICENSE ├── .travis.yml ├── package.json ├── .eslintrc ├── README.md ├── CHANGELOG.md ├── dist └── passable.min.js └── index.d.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiverr/passable/HEAD/docs/.DS_Store -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Owners for everything 2 | * @raphaelboukara @fiverr/fe-team 3 | -------------------------------------------------------------------------------- /docs/_assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiverr/passable/HEAD/docs/_assets/logo.png -------------------------------------------------------------------------------- /docs/_assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fiverr/passable/HEAD/docs/_assets/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | ./dist/* 2 | ./dev/* 3 | ./node_modules/* 4 | ./flow-typed/* 5 | ./docs 6 | webpack.config.js -------------------------------------------------------------------------------- /config/babel-register.js: -------------------------------------------------------------------------------- 1 | require("@babel/register")({ 2 | configFile: './config/babel.config.js' 3 | }); -------------------------------------------------------------------------------- /scripts/prepare_next.sh: -------------------------------------------------------------------------------- 1 | echo "Preparing next tag" 2 | 3 | npm version "${NEXT_VERSION}-next-${TRAVIS_COMMIT:(0):6}" --no-git-tag 4 | -------------------------------------------------------------------------------- /src/core/test/lib/index.js: -------------------------------------------------------------------------------- 1 | export { default as isTestFn } from './isTestFn'; 2 | export { default as TestObject } from './TestObject'; 3 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | export { default as globalObject } from './globalObject'; 2 | export { default as singleton } from './singleton'; 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintignore 2 | .eslintrc 3 | .travis.yml 4 | .github 5 | yarn.lock 6 | docs 7 | scripts 8 | config 9 | playground 10 | src 11 | -------------------------------------------------------------------------------- /src/spec/fail.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is should fail in tests because of invalid typings. 3 | */ 4 | interface FailureTypes { 5 | name: typeNotExist 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/globalObject/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {Object} Reference to global object. 3 | */ 4 | const globalObject = Function('return this')(); 5 | 6 | export default globalObject; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .sass-cache 4 | dev_docpress 5 | _docpress 6 | dev 7 | version.json 8 | version.txt 9 | .version 10 | documentation/MAIN.md 11 | documentation/assets/**/*.css 12 | .DS_Store -------------------------------------------------------------------------------- /config/test-setup.js: -------------------------------------------------------------------------------- 1 | const regeneratorRuntime = require('regenerator-runtime'); 2 | const chai = require('chai'); 3 | 4 | global.expect = chai.expect; 5 | global.PASSABLE_VERSION = require('../package.json').version; 6 | 7 | require('../src'); 8 | -------------------------------------------------------------------------------- /src/core/draft/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {String} Error thrown when draft gets called without an active Passable context. 3 | */ 4 | export const ERROR_NO_CONTEXT = '[Passable]: Draft was called outside of the context of a running suite. Please make sure you call it only from your Passable suite.'; 5 | -------------------------------------------------------------------------------- /src/lib/singleton/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {String} Passable's major version. 3 | */ 4 | const PASSABLE_MAJOR = PASSABLE_VERSION.split('.')[0]; 5 | 6 | /** 7 | * @type {Symbol} Used to store a global instance of Passable. 8 | */ 9 | export const SYMBOL_PASSABLE = Symbol.for(`PASSABLE#${PASSABLE_MAJOR}`); 10 | -------------------------------------------------------------------------------- /scripts/get_next_version.js: -------------------------------------------------------------------------------- 1 | const { version } = require('../package.json'); 2 | const semver = require('semver'); 3 | const determineLevel = require('./determine_change_level'); 4 | 5 | const changeLevel = determineLevel(process.argv[2] || ''); 6 | 7 | const nextVersion = semver.inc(version, changeLevel); 8 | 9 | console.log(nextVersion); 10 | -------------------------------------------------------------------------------- /scripts/prep_docs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const n4sRules = fs.readFileSync('./node_modules/n4s/docs/rules.md', 'utf8').replace('\n#', '\n##'); 4 | const enforceDoc = fs.readFileSync('./docs/enforce.md.bak', 'utf8'); 5 | 6 | const nextDoc = enforceDoc.replace('{{LIST_OF_ENFORCE_RULES}}', n4sRules); 7 | 8 | fs.writeFileSync('./docs/enforce.md', nextDoc); 9 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {String} Version number derived from current tag. 3 | */ 4 | export const VERSION = PASSABLE_VERSION; 5 | 6 | /** 7 | * @type {String} Keyword used for marking non failing tests. 8 | */ 9 | export const WARN = 'warn'; 10 | 11 | /** 12 | * @type {String} Keyword used for marking failing tests. 13 | */ 14 | export const FAIL = 'fail'; 15 | -------------------------------------------------------------------------------- /scripts/handle_changes.sh: -------------------------------------------------------------------------------- 1 | echo "Script: handle_changes" 2 | 3 | echo "Getting diff" 4 | export COMMIT_MESSAGES=$(node ./scripts/get_diff.js) 5 | 6 | echo "Commit message is:" 7 | echo $COMMIT_MESSAGES 8 | 9 | echo "Getting next version" 10 | export NEXT_VERSION=$(node ./scripts/get_next_version.js "$COMMIT_MESSAGES") 11 | 12 | echo "Next version is:" 13 | echo $NEXT_VERSION 14 | -------------------------------------------------------------------------------- /src/core/test/lib/isTestFn/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks that a given argument qualifies as a test function 3 | * @param {*} testFn 4 | * @return {Boolean} 5 | */ 6 | const isTestFn = (testFn) => { 7 | if (!testFn) { 8 | return false; 9 | } 10 | 11 | return typeof testFn.then === 'function' || typeof testFn === 'function'; 12 | }; 13 | 14 | export default isTestFn; 15 | -------------------------------------------------------------------------------- /docs/getting_started/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | You can use npm to install Passable as a package and include it in your app 3 | 4 | ```shell 5 | npm install passable --save 6 | ``` 7 | 8 | Or import Passable as a script tag to your page: 9 | ```html 10 | 11 | ``` 12 | 13 | To view the currently running version, you can use: 14 | 15 | ```js 16 | console.log(passable.VERSION) // 7.1.0 17 | ``` 18 | -------------------------------------------------------------------------------- /src/core/draft/index.js: -------------------------------------------------------------------------------- 1 | import { singleton } from '../../lib'; 2 | import { ERROR_NO_CONTEXT } from './constants'; 3 | 4 | /** 5 | * @return {Object} Current draft. 6 | */ 7 | const draft = () => { 8 | 9 | const ctx = singleton.use().ctx; 10 | 11 | if (ctx) { 12 | return ctx.result.output; 13 | } 14 | 15 | setTimeout(() => { 16 | throw new Error(ERROR_NO_CONTEXT); 17 | }); 18 | }; 19 | 20 | export default draft; 21 | -------------------------------------------------------------------------------- /docs/_assets/stylesheet.css: -------------------------------------------------------------------------------- 1 | .markdown-section code { 2 | font-size: 85%; 3 | color: #26B4AD; 4 | } 5 | 6 | .app-name { 7 | padding: 10px; 8 | } 9 | 10 | .sidebar-nav { 11 | padding-left: 10px; 12 | } 13 | 14 | body, .anchor span, a.anchor { 15 | color: #3C4464; 16 | } 17 | 18 | .token.function,.token.keyword { 19 | color: #26B4AD 20 | } 21 | 22 | .token.boolean, .token.number { 23 | color: #59d4e8; 24 | } 25 | 26 | .markdown-section a:hover { 27 | color: #3a6b96; 28 | } -------------------------------------------------------------------------------- /config/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | 3 | if (api) { 4 | api.cache(true); 5 | } 6 | 7 | const presets = [ 8 | '@babel/preset-env' 9 | ]; 10 | 11 | const plugins = [ 12 | 'babel-plugin-add-module-exports', 13 | '@babel/plugin-proposal-class-properties', 14 | '@babel/plugin-transform-object-assign' 15 | ]; 16 | 17 | return { 18 | include: [/src/, /node_modules/], 19 | presets, 20 | plugins 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/utilities/validate/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run tests and catch errors 3 | * 4 | * @param {function} callback The test content 5 | * @return {boolean} 6 | */ 7 | function validate(test) { 8 | 9 | if (typeof test !== 'function' && !(test instanceof Promise)) { 10 | throw new TypeError(`[Validate]: expected ${typeof test} to be a function.`); 11 | } 12 | 13 | try { 14 | return test() !== false; 15 | } catch (_) { 16 | return false; 17 | } 18 | } 19 | 20 | export default validate; 21 | -------------------------------------------------------------------------------- /src/core/Context/index.js: -------------------------------------------------------------------------------- 1 | import { singleton } from '../../lib'; 2 | 3 | /** 4 | * Creates a new context object, and assigns it as a static property on Passable's singleton. 5 | * @param {Object} parent Parent context. 6 | */ 7 | const Context = function(parent) { 8 | singleton.use().ctx = this; 9 | Object.assign(this, parent); 10 | }; 11 | 12 | /** 13 | * Clears stored instance from constructor function. 14 | */ 15 | Context.clear = function() { 16 | singleton.use().ctx = null; 17 | }; 18 | 19 | export default Context; 20 | -------------------------------------------------------------------------------- /scripts/constants.js: -------------------------------------------------------------------------------- 1 | const PATCH_KEYWORD = 'patch'; 2 | const MINOR_KEYWORD = 'minor'; 3 | const MAJOR_KEYWORD = 'major'; 4 | 5 | const CHANGELOG_TITLES = { 6 | [MAJOR_KEYWORD]: 'Breaking changes', 7 | [MINOR_KEYWORD]: 'Additions', 8 | [PATCH_KEYWORD]: 'Fixes and non breaking changes' 9 | }; 10 | 11 | const EXCLUDED_WORDS = [ 12 | 'dependabot', 13 | '[config]' 14 | ]; 15 | 16 | module.exports = { 17 | PATCH_KEYWORD, 18 | MINOR_KEYWORD, 19 | MAJOR_KEYWORD, 20 | CHANGELOG_TITLES, 21 | EXCLUDED_WORDS 22 | }; 23 | -------------------------------------------------------------------------------- /src/spec/ts.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | const { execSync } = require('child_process'); 3 | 4 | describe('TypeScript Typings', () => { 5 | it('Should pass the typings check', () => { 6 | expect(() => { 7 | execSync('node_modules/.bin/tsc index.d.ts'); 8 | }).to.not.throw(); 9 | }).timeout(0); 10 | 11 | it('Should fail the typings check with fail stub', () => { 12 | expect(() => { 13 | execSync('node_modules/.bin/tsc src/spec/fail.d.ts'); 14 | }).to.throw(); 15 | }).timeout(0); 16 | }); 17 | -------------------------------------------------------------------------------- /playground/index.js: -------------------------------------------------------------------------------- 1 | const passable = require('../dist/Passable'); 2 | const enforce = passable.enforce; 3 | 4 | console.log('Playground. Lets play!'); 5 | console.log(`passable version: ${passable.VERSION}`); 6 | 7 | passable('TestForm', (test) => { 8 | test('Field1', 'Should be valid', () => { 9 | enforce(1).isNumeric(); 10 | }); 11 | 12 | test('Field2', 'should wait some and pass', new Promise((resolve, reject) => setTimeout(resolve, 1000))); 13 | test('Field3', 'should wait some and fail', new Promise((resolve, reject) => setTimeout(reject, 3000))); 14 | }).done(console.log); 15 | -------------------------------------------------------------------------------- /scripts/determine_change_level.js: -------------------------------------------------------------------------------- 1 | const { MAJOR_KEYWORD, MINOR_KEYWORD, PATCH_KEYWORD } = require('./constants'); 2 | 3 | /** 4 | * Determines semver level 5 | * @param {String} message 6 | * @return {String} change level 7 | */ 8 | const determineChangeLevel = (message) => { 9 | if (message.toLowerCase().includes(`[${MAJOR_KEYWORD}]`)) { 10 | return MAJOR_KEYWORD; 11 | } 12 | 13 | if (message.toLowerCase().includes(`[${MINOR_KEYWORD}]`)) { 14 | return MINOR_KEYWORD; 15 | } 16 | 17 | return PATCH_KEYWORD; 18 | }; 19 | 20 | module.exports = determineChangeLevel; 21 | -------------------------------------------------------------------------------- /docs/_assets/index.js: -------------------------------------------------------------------------------- 1 | Object.assign(window, passable); 2 | 3 | setTimeout(() => { 4 | console.log(`All passable functions are exposed globally, 5 | you can try passable in your console.`); 6 | 7 | console.log(`PASSABLE VERSION:`, passable.VERSION); 8 | 9 | console.table(Object.keys(passable).map((item) => [item, typeof passable[item]])); 10 | 11 | console.log(`You can try: 12 | 13 | const data = { 14 | useanme: 'example' 15 | }; 16 | 17 | passable('FormName', () => { 18 | test('username', 'Must be at least 4 chars', () => { 19 | enforce(data.username).longerThanOrEquals(4); 20 | }); 21 | }); 22 | `) 23 | }, 1000); -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | 2 | * Getting Started 3 | * [Installation](./getting_started/installation.md) 4 | * [Writing tests](./getting_started/writing_tests.md) 5 | * [Passable callbacks](./getting_started/callbacks.md) 6 | * [The `result` object](./getting_started/result.md) 7 | * [The `test` Function](./test/index.md) 8 | * [How to fail a test](./test/how_to_fail.md) 9 | * [Warn only tests](./test/warn_only_tests.md) 10 | * [Skipping tests](./test/specific.md) 11 | * [Async tests](./test/async.md) 12 | * [Test Utilities](./utilities/README.md) 13 | * [Assertions with `enforce`](./enforce.md) 14 | * [Using with assertions libraries](./compatability/assertions.md) 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import enforce from 'n4s/dist/enforce.min'; 2 | import any from 'anyone/any'; 3 | import passable from './core/passable'; 4 | import draft from './core/draft'; 5 | import test from './core/test'; 6 | import validate from './utilities/validate'; 7 | import { singleton } from './lib'; 8 | import { WARN, FAIL, VERSION } from './constants'; 9 | 10 | passable.VERSION = VERSION; 11 | passable.enforce = enforce; 12 | passable.draft = draft; 13 | passable.Enforce = enforce.Enforce; 14 | passable.test = test; 15 | passable.validate = validate; 16 | passable.any = any; 17 | passable.WARN = WARN; 18 | passable.FAIL = FAIL; 19 | 20 | singleton.register(passable); 21 | 22 | export default passable; 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | | Q | A 12 | | ---------------- | --- 13 | | Bug fix? | yes/no 14 | | New feature? | yes/no 15 | | Breaking change? | yes/no 16 | | Deprecations? | yes/no 17 | | Documentation? | yes/no 18 | | Tests added? | yes/no 19 | | Fixed issues | comma-separated list of issues fixed by the pull request, where applicable 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/utilities/validate/spec.js: -------------------------------------------------------------------------------- 1 | import validate from '.'; 2 | import { enforce } from '../../index'; 3 | import { expect } from 'chai'; 4 | 5 | describe('Utilities: validate', () => { 6 | it('Should return `false` for a failing test', () => { 7 | expect(validate(() => { 8 | enforce(33).greaterThan(100); 9 | })).to.equal(false); 10 | }); 11 | 12 | it('Should return `false` for a falsy statement', () => { 13 | expect(validate(() => false)).to.equal(false); 14 | }); 15 | 16 | it('Should return `true` for a truthy statement', () => { 17 | expect(validate(() => true)).to.equal(true); 18 | }); 19 | 20 | it('Should return `true` for an empty test', () => { 21 | expect(validate(() => undefined)).to.equal(true); 22 | }); 23 | 24 | it('Should throw TypeError when no tests are present', () => { 25 | expect(() => validate()).to.throw(TypeError); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/core/test/lib/isTestFn/spec.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import { expect } from 'chai'; 3 | import isTestFn from '.'; 4 | 5 | describe('isTestFn module', () => { 6 | 7 | describe('When argument is a function', () => { 8 | it('Should return true', () => { 9 | expect(isTestFn(Function.prototype)).to.equal(true); 10 | }); 11 | }); 12 | 13 | describe('When argument is a Promise', () => { 14 | it('Should return true', () => { 15 | expect(isTestFn(new Promise(() => null))).to.equal(true); 16 | }); 17 | }); 18 | 19 | describe('When argument is any other type', () => { 20 | expect([ 21 | { [faker.lorem.word()]: faker.lorem.word() }, 22 | [faker.lorem.word()], 23 | 1, 24 | faker.lorem.word(), 25 | true, 26 | null, 27 | NaN 28 | ].some(isTestFn)).to.equal(false); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/spec/react.elements.support.spec.js: -------------------------------------------------------------------------------- 1 | const runSpec = (passable) => { 2 | describe('React Elements Support', () => { 3 | let result; 4 | 5 | beforeEach(() => { 6 | result = passable('NewUserForm', (test) => { 7 | test('username', { a: 1 }, () => { 8 | passable.enforce(true).equals(false); 9 | }); 10 | 11 | test('username', { a: 2 }, () => { 12 | enforce(true).equals(false); 13 | }, passable.WARN); 14 | }); 15 | }); 16 | 17 | it('should support objects in errors', () => { 18 | expect(result.errors.username).to.deep.equal([{ a: 1 }]); 19 | }); 20 | 21 | it('should support objects in warnings', () => { 22 | expect(result.warnings.username).to.deep.equal([{ a: 2 }]); 23 | }); 24 | }); 25 | }; 26 | 27 | runSpec(require('../')); 28 | runSpec(require('../../dist/passable')); 29 | runSpec(require('../../dist/passable.min.js')); 30 | -------------------------------------------------------------------------------- /scripts/push_tag_to_master.sh: -------------------------------------------------------------------------------- 1 | git config --global user.email "${GIT_NAME}@users.noreply.github.com" --replace-all 2 | git config --global user.name $GIT_NAME 3 | 4 | echo "Removing old master" 5 | git branch -D master 6 | 7 | echo "Switching to new master" 8 | git checkout -b master 9 | 10 | echo "Bumping version" 11 | npm version $NEXT_VERSION --no-git-tag 12 | 13 | echo "Rebuilding with current tag" 14 | yarn build 15 | 16 | echo "Updating changelog" 17 | CHANGELOG=$(node ./scripts/update_changelog.js) 18 | 19 | EMOJIS=(🚀 🤘 ✨ 🔔 🌈 🤯) 20 | EMOJI=${EMOJIS[$RANDOM % ${#EMOJIS[@]}]} 21 | 22 | git add . 23 | 24 | if (( $(grep -c . <<<"$msg") > 1 )); then 25 | git commit -m "$EMOJI Passable cumulative update: $NEXT_VERSION" -m "$COMMIT_MESSAGES" 26 | else 27 | git commit -m "$EMOJI Passable update: $NEXT_VERSION" -m "$COMMIT_MESSAGES" 28 | fi 29 | 30 | echo "Pushing to master" 31 | git push https://${GITHUB_TOKEN}@github.com/$GITHUB_REPO.git master 32 | 33 | git tag $NEXT_VERSION 34 | git push origin $NEXT_VERSION 35 | 36 | echo "Publishing Release" 37 | node ./scripts/create_release.js "$CHANGELOG" 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fiverr 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 | -------------------------------------------------------------------------------- /src/core/Context/spec.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import { singleton } from '../../lib'; 3 | import Context from '.'; 4 | 5 | describe('Context', () => { 6 | let parent, instance; 7 | 8 | beforeEach(() => { 9 | 10 | parent = { 11 | [faker.random.word()]: faker.random.word(), 12 | [faker.random.word()]: faker.random.word(), 13 | [faker.random.word()]: faker.random.word() 14 | }; 15 | 16 | instance = new Context(parent); 17 | }); 18 | 19 | it('Should assign all parent properties onto ctx instance', () => { 20 | Object.keys(parent).forEach((key) => { 21 | expect(instance[key]).to.equal(parent[key]); 22 | }); 23 | }); 24 | 25 | it('Should store instance on singleton', () => { 26 | expect(singleton.use().ctx).to.equal(instance); 27 | }); 28 | 29 | describe('Context.clear', () => { 30 | it('Should nullify stored instance', () => { 31 | expect(singleton.use().ctx).to.equal(instance); 32 | Context.clear(); 33 | expect(singleton.use().ctx).to.equal(null); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /docs/compatability/assertions.md: -------------------------------------------------------------------------------- 1 | # Using with other assertion libraries 2 | Using other assertion libraries along with Passable can't be easier. Most popular assertion libraries are supported by default, and many others are supported as well. Basically, if it throws an error, it works. 3 | 4 | For example, let's say you want to use the popular v8n validation library, which can be configured to throw an exception for failed validations. 5 | 6 | ```js 7 | import passable, { enforce } 'passable'; 8 | import v8n from 'v8n'; 9 | 10 | // data = { 11 | // username: 'ealush', 12 | // age: 27 13 | // } 14 | 15 | passable('FormWithV8N', (test) => { 16 | test('username', 'Should be a string', () => { 17 | v8n() 18 | .string() 19 | .check(data.username); 20 | }); 21 | 22 | test('age', 'Should be a number and larger than 18', () => { 23 | v8n() 24 | .number() 25 | .greaterThan(18) 26 | .check(data.age); 27 | }); 28 | }); 29 | 30 | ``` -------------------------------------------------------------------------------- /scripts/get_diff.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | function compareUrl(repo, [commit1, commit2]) { 4 | return `https://api.github.com/repos/${repo}/compare/${commit1}...${commit2}`; 5 | } 6 | 7 | function listMessages(commits = []) { 8 | return commits.reduce((accumulator, { commit, author, sha }) => { 9 | const [message] = commit.message.split('\n'); 10 | const name = author.login || commit.author.name; 11 | return `${accumulator}${sha.slice(0, 7)} ${message} (${name})\n`; 12 | }, ''); 13 | } 14 | 15 | function getCommitDiff(repo, branches, token) { 16 | return fetch(compareUrl(repo, branches), { ...token && { headers: { Authorization: `token ${token}` } } }) 17 | .then((res) => res.json()) 18 | .catch(() => process.exit(1)); 19 | } 20 | 21 | async function init({ 22 | repo, 23 | branches, 24 | token 25 | } = {}) { 26 | const { commits } = await getCommitDiff(repo, branches, token); 27 | const messages = listMessages(commits); 28 | console.log(messages); 29 | } 30 | 31 | init({ 32 | repo: process.env.GITHUB_REPO, 33 | branches: ['master', process.env.TRAVIS_BRANCH], 34 | token: process.env.GITHUB_TOKEN 35 | }); 36 | -------------------------------------------------------------------------------- /scripts/create_release.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | const TITLE_DELIMITER = '\n\n'; 4 | 5 | const changelog = process.argv[2] || ''; 6 | 7 | function releaseUrl(repo) { 8 | return `https://api.github.com/repos/${repo}/releases`; 9 | }; 10 | 11 | function postRelease({ 12 | repo, 13 | token, 14 | tag, 15 | body 16 | }) { 17 | console.log('posting release'); 18 | fetch(releaseUrl(repo), { 19 | method: 'POST', 20 | headers: { Authorization: `token ${token}` }, 21 | body: JSON.stringify({ 22 | tag_name: tag, 23 | name: tag, 24 | body 25 | }) 26 | }); 27 | } 28 | 29 | function release({ repo, token, tag }) { 30 | console.log('In release'); 31 | 32 | if (!(changelog && token && repo)) { 33 | return; 34 | } 35 | 36 | const body = changelog.substr(changelog.indexOf(TITLE_DELIMITER) + TITLE_DELIMITER.length); 37 | 38 | postRelease({ 39 | repo, 40 | token, 41 | tag, 42 | body 43 | }); 44 | }; 45 | 46 | release({ 47 | repo: process.env.GITHUB_REPO, 48 | token: process.env.GITHUB_TOKEN, 49 | tag: process.env.NEXT_VERSION, 50 | changelog 51 | }); 52 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | passable - Declarative validations 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/lib/singleton/index.js: -------------------------------------------------------------------------------- 1 | import go from '../globalObject'; 2 | import { SYMBOL_PASSABLE } from './constants'; 3 | 4 | /** 5 | * @param {String[]} versions List of passable versions. 6 | * @throws {Error} 7 | */ 8 | const throwMultiplePassableError = (...versions) => { 9 | throw new Error(`[Passable]: Multiple versions of Passable detected: (${versions.join()}). 10 | Most features should work regularly, but for optimal feature compatibility, you should have all running instances use the same version.`); 11 | }; 12 | 13 | /** 14 | * Registers current Passable instance on global object. 15 | * @param {Function} passable Reference to passable. 16 | * @return {Function} Global passable reference. 17 | */ 18 | const register = (passable) => { 19 | 20 | const existing = go[SYMBOL_PASSABLE]; 21 | 22 | if (existing) { 23 | if (existing.VERSION !== passable.VERSION) { 24 | setTimeout(() => throwMultiplePassableError(passable.VERSION, existing.VERSION)); 25 | } 26 | } else { 27 | go[SYMBOL_PASSABLE] = passable; 28 | } 29 | 30 | return go[SYMBOL_PASSABLE]; 31 | }; 32 | 33 | const singletonExport = { 34 | use: () => go[SYMBOL_PASSABLE], 35 | register 36 | }; 37 | 38 | export default singletonExport; 39 | -------------------------------------------------------------------------------- /config/rollup.js: -------------------------------------------------------------------------------- 1 | import commonjs from 'rollup-plugin-commonjs'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import babel from 'rollup-plugin-babel'; 4 | import replace from 'rollup-plugin-replace'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | const { version } = require('../package.json'); 7 | 8 | const DEFAULT_FORMAT = 'umd'; 9 | const LIBRARY_NAME = 'passable'; 10 | 11 | const PLUGINS = [ 12 | resolve(), 13 | commonjs({ 14 | include: /node_modules\/(anyone|n4s)/ 15 | }), 16 | babel({ 17 | babelrc: false, 18 | ...require('./babel.config')() 19 | }), 20 | replace({ 21 | PASSABLE_VERSION: JSON.stringify(version) 22 | }) 23 | ]; 24 | 25 | const buildConfig = ({ format = DEFAULT_FORMAT, min = false } = {}) => ({ 26 | input: 'src/index.js', 27 | output: { 28 | file: [ 29 | `dist/${LIBRARY_NAME}`, 30 | min && 'min', 31 | format !== DEFAULT_FORMAT && format, 32 | 'js' 33 | ].filter(Boolean).join('.'), 34 | name: LIBRARY_NAME, 35 | format 36 | }, 37 | plugins: min 38 | ? [ ...PLUGINS, terser() ] 39 | : PLUGINS 40 | 41 | }); 42 | 43 | export default [ 44 | buildConfig({ min: true }), 45 | buildConfig() 46 | ]; 47 | -------------------------------------------------------------------------------- /src/core/passable/index.js: -------------------------------------------------------------------------------- 1 | import Context from '../Context'; 2 | import test, { runAsync } from '../test'; 3 | import passableResult from '../passableResult'; 4 | import Specific from '../Specific'; 5 | import { singleton } from '../../lib'; 6 | 7 | const initError = (name, value, doc) => `[Passable]: failed during suite initialization. Unexpected '${typeof value}' for '${name}' argument. 8 | See: ${doc ? doc : 'https://fiverr.github.io/passable/getting_started/writing_tests.html'}`; 9 | 10 | const passable = (name, tests, specific) => { 11 | if (typeof name !== 'string') { 12 | throw new TypeError(initError('suite name', name)); 13 | } 14 | 15 | if (typeof tests !== 'function') { 16 | throw new TypeError(initError('tests', tests)); 17 | } 18 | 19 | if (specific && !Specific.is(specific)) { 20 | throw new TypeError(initError('specific', tests, 'https://fiverr.github.io/passable/test/specific.html')); 21 | } 22 | 23 | const result = passableResult(name); 24 | 25 | const pending = []; 26 | 27 | new Context({ 28 | specific: new Specific(specific), 29 | result, 30 | pending 31 | }); 32 | 33 | tests(test, result.output); 34 | 35 | Context.clear(); 36 | 37 | [...pending].forEach(runAsync); 38 | 39 | return result.output; 40 | }; 41 | 42 | export default passable; 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: npm 3 | 4 | env: 5 | global: 6 | - GIT_NAME: A1vy 7 | - GIT_EMAIL: opensource@fiverr.com 8 | - GITHUB_REPO: fiverr/passable 9 | - GIT_SOURCE: docs 10 | 11 | before_script: 12 | - npm run build 13 | - npm run eslint 14 | 15 | after_success: 16 | - chmod +x ./scripts/handle_changes.sh 17 | - source ./scripts/handle_changes.sh 18 | - node ./scripts/prep_docs.js 19 | - chmod +x ./scripts/push_tag_to_master.sh 20 | - if [ "$TRAVIS_BRANCH" = "release" ]; then ./scripts/push_tag_to_master.sh; fi 21 | - chmod +x ./scripts/prepare_next.sh 22 | - if [ "$TRAVIS_BRANCH" != "master" ]; then ./scripts/prepare_next.sh; fi 23 | 24 | deploy: 25 | - provider: npm 26 | email: it@fiverr.com 27 | skip_cleanup: true 28 | api_key: $NPM_TOKEN 29 | on: 30 | repo: fiverr/passable 31 | branch: master 32 | - provider: npm 33 | email: it@fiverr.com 34 | skip_cleanup: true 35 | api_key: $NPM_TOKEN 36 | tag: "next" 37 | on: 38 | all_branches: true 39 | condition: $TRAVIS_BRANCH = "next" 40 | repo: fiverr/passable 41 | - provider: npm 42 | email: it@fiverr.com 43 | skip_cleanup: true 44 | api_key: $NPM_TOKEN 45 | tag: "DEVELOPMENT" 46 | on: 47 | all_branches: true 48 | condition: $TRAVIS_BRANCH != "next" && $TRAVIS_BRANCH != "master" && $TRAVIS_BRANCH != "release" 49 | repo: fiverr/passable 50 | -------------------------------------------------------------------------------- /src/core/passable/spec.js: -------------------------------------------------------------------------------- 1 | import Passable from '.'; 2 | import faker from 'faker'; 3 | import { noop } from 'lodash'; 4 | import sinon from 'sinon'; 5 | import { expect } from 'chai'; 6 | 7 | describe('Test passable suite wrapper', () => { 8 | const passable = (...args) => new Passable(...args); 9 | 10 | describe('Test arguments', () => { 11 | it('Should throw a TypeError for a non-string name', () => { 12 | 13 | expect(() => passable(1, noop)) 14 | .to.throw(TypeError); 15 | expect(() => passable({}, noop)) 16 | .to.throw(TypeError); 17 | expect(() => passable(noop, noop)) 18 | .to.throw(TypeError); 19 | }); 20 | 21 | it('Should throw TypeError if `tests` is not a function', () => { 22 | expect(() => passable('MyForm', 'noop', null)) 23 | .to.throw(TypeError); 24 | }); 25 | 26 | it('Should throw an exception when specific does not follow convention', () => { 27 | expect(() => passable('FormName', noop, noop)).to.throw(TypeError); 28 | expect(() => passable('FormName', noop, true)).to.throw(TypeError); 29 | }); 30 | }); 31 | 32 | it('Should pass down `test` function to `tests` callback', () => { 33 | const p = new Passable('name', (test) => { 34 | expect(test).to.be.a('function'); 35 | }); 36 | }); 37 | 38 | it('Calls `tests` argument', (done) => { 39 | new Passable('FormName', () => done()); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/core/test/lib/TestObject/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Describes a test call inside a passable suite. 3 | * @param {Object} ctx Parent context. 4 | * @param {String} fieldName Name of the field being tested. 5 | * @param {String} statement The message returned when failing. 6 | * @param {Promise|Function} testFn The actual test callbrack or promise. 7 | * @param {String} [severity] Indicates whether the test should fail or warn. 8 | */ 9 | function TestObject(ctx, fieldName, statement, testFn, severity) { 10 | Object.assign(this, { 11 | ctx, 12 | testFn, 13 | fieldName, 14 | statement, 15 | severity, 16 | failed: false 17 | }); 18 | }; 19 | 20 | /** 21 | * @returns Current validity status of a test. 22 | */ 23 | TestObject.prototype.valueOf = function() { 24 | return this.failed !== true; 25 | }; 26 | 27 | /** 28 | * Sets a field to failed. 29 | * @returns {TestObject} Current instance. 30 | */ 31 | TestObject.prototype.fail = function() { 32 | 33 | this.ctx.result.fail( 34 | this.fieldName, 35 | this.statement, 36 | this.severity 37 | ); 38 | 39 | this.failed = true; 40 | return this; 41 | }; 42 | 43 | /** 44 | * Adds current test to pending list. 45 | */ 46 | TestObject.prototype.setPending = function() { 47 | this.ctx.pending.push(this); 48 | }; 49 | 50 | /** 51 | * Removes test from pending list. 52 | */ 53 | TestObject.prototype.clearPending = function() { 54 | this.ctx.pending = this.ctx.pending.filter((t) => t !== this); 55 | }; 56 | 57 | export default TestObject; 58 | -------------------------------------------------------------------------------- /docs/test/warn_only_tests.md: -------------------------------------------------------------------------------- 1 | # Warn only `test` 2 | By default, a failing `test` will set `.hasErrors()` to `true`. Sometimes you need to set a warn-only validation test (password strength, for example). In this case, you would add the `WARN` flag to your test function as the last argument. 3 | WARN validations work exactly the same. The only thing different is that the result will be stored under `warnings` instead of `errors`, and `.hasWarnings()` will return true. 4 | 5 | If no flag is added, your test function will default to `FAIL`. The `WARN` and `FAIL` flags are constants exported from passable. 6 | 7 | ```js 8 | // es6 imports 9 | import passable, { WARN } from 'passable'; 10 | 11 | // es5 12 | const passable = require('passable'); 13 | const WARN = passable.WARN; 14 | ``` 15 | 16 | Use it like this: 17 | 18 | ```js 19 | import passable, { WARN, enforce } from 'passable'; 20 | 21 | const result = passable('WarnAndPass', (test) => { 22 | test('WarnMe', 'Should warn and not fail', () => { 23 | enforce(5).greaterThan(500); 24 | }, WARN); 25 | }); 26 | 27 | result.hasWarnings(); // true 28 | result.hasWarnings('WarnMe'); // true 29 | result.hasErrors(); // false 30 | ``` 31 | 32 | You may also use the values directly from the result object. 33 | ```js 34 | { 35 | name: 'WarnAndPass', 36 | skipped: [], 37 | testsPerformed: { 38 | WarnMe: { 39 | testCount: 1, 40 | failCount: 0, 41 | warnCount: 1 42 | } 43 | }, 44 | errors: {}, 45 | warnings: { 46 | WarnMe: [ 47 | 'Should warn and not fail' 48 | ] 49 | }, 50 | failCount: 0, 51 | warnCount: 1, 52 | testCount: 1 53 | } 54 | ``` -------------------------------------------------------------------------------- /docs/test/how_to_fail.md: -------------------------------------------------------------------------------- 1 | # All ways to fail a `test` 2 | With the exclusion of async tests, there are two ways of failing a test, and marking it as having a validation error: 3 | 4 | ## Throwing an Error 5 | 6 | Throwing an error from within the `test`. Thrown errors within the `test` function are caught and handled to mark the test as failing. 7 | ```js 8 | test('field', 'should fail by a thrown error', () => { throw new Error(); }); 9 | ``` 10 | This also includes `enforce` failures, which throws when the validation criteria are not met: 11 | 12 | ```js 13 | test('field', 'should fail by enforce', () => { 14 | enforce(1).greaterThan(5); 15 | }); 16 | ``` 17 | 18 | ## Explicitly returning `false` 19 | 20 | Explicitly returning `false` from the `test` itself (running some logic that returns false) will fail your test. This is good especially for migration periods in which your previous validations relied on a boolean flag, and you want to quickly just copy things over. 21 | 22 | ```js 23 | test('field', 'should explicitly fail by returning `false`', () => false); 24 | test('field', 'should explicitly fail by returning `false`', () => data.value !== 1); 25 | ``` 26 | 27 | ## Promise rejection (Async) 28 | 29 | Since [async tests](./test/async.html) rely on Promise, the way to fail the test is simply to reject it: 30 | 31 | ```js 32 | test('Field3', 'should wait some and fail', 33 | new Promise((resolve, reject) => setTimeout(reject, 3000)) 34 | 35 | ); 36 | test('Field4', 'should wait some and fail', 37 | () => ( 38 | new Promise((resolve, reject) => setTimeout(reject, 3000)) 39 | ) 40 | ); 41 | ``` -------------------------------------------------------------------------------- /scripts/update_changelog.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs'); 3 | const { format } = require('date-fns'); 4 | const determineLevel = require('./determine_change_level'); 5 | const { MAJOR_KEYWORD, MINOR_KEYWORD, PATCH_KEYWORD, CHANGELOG_TITLES, EXCLUDED_WORDS } = require('./constants'); 6 | 7 | const version = process.env.NEXT_VERSION; 8 | const gitLog = process.env.COMMIT_MESSAGES; 9 | 10 | /** 11 | * Takes commit history and groups messages by change level 12 | * @param {String} gitLog commit history 13 | * @return {Object} an object with keys matching the semver levels 14 | */ 15 | const groupMessages = (gitLog) => gitLog.split('\n').reduce((accumulator, current) => { 16 | const level = determineLevel(current); 17 | 18 | if (EXCLUDED_WORDS.some((word) => current.toLowerCase().includes(word.toLowerCase()))) { 19 | return accumulator; 20 | } 21 | 22 | if (!accumulator[level]) { 23 | accumulator[level] = `### ${CHANGELOG_TITLES[level]}\n`; 24 | } 25 | 26 | return Object.assign(accumulator, { 27 | [level]: `${accumulator[level]}- ${current}\n` 28 | }); 29 | }, {}); 30 | 31 | const updateChangelog = () => { 32 | const groupedMessages = groupMessages(gitLog); 33 | 34 | const changelogTitle = `## [${version}] - ${format(new Date(), 'yyyy-MM-dd')}\n`; 35 | 36 | const versionLog = [ 37 | changelogTitle, 38 | groupedMessages[MAJOR_KEYWORD], 39 | groupedMessages[MINOR_KEYWORD], 40 | groupedMessages[PATCH_KEYWORD] 41 | ].filter(Boolean).join('\n'); 42 | 43 | const changelog = fs.readFileSync('./CHANGELOG.md', 'utf8').split('\n'); 44 | changelog.splice(6, 0, versionLog); 45 | 46 | fs.writeFileSync('./CHANGELOG.md', changelog.join('\n')); 47 | 48 | console.log(versionLog); 49 | }; 50 | 51 | updateChangelog(); 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "passable", 3 | "version": "8.0.0", 4 | "description": "Isomorphic Data Model Validations.", 5 | "main": "./dist/passable.min.js", 6 | "typings": "index.d.ts", 7 | "author": "Evyatar ", 8 | "license": "MIT", 9 | "scripts": { 10 | "build": "rollup --config config/rollup.js -m", 11 | "pretest": "npm run build", 12 | "test": "mocha --file ./config/test-setup.js --require ./config/babel-register.js --colors \"./src/**/*spec.js\"", 13 | "eslint": "eslint -c .eslintrc \"./src/**/*.js\"; exit 0" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/fiverr/passable.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/fiverr/passable/issues" 21 | }, 22 | "homepage": "https://fiverr.github.io/passable/", 23 | "devDependencies": { 24 | "@babel/cli": "^7.5.5", 25 | "@babel/core": "^7.5.5", 26 | "@babel/plugin-proposal-class-properties": "^7.5.5", 27 | "@babel/plugin-transform-object-assign": "^7.2.0", 28 | "@babel/preset-env": "^7.5.5", 29 | "@babel/register": "^7.5.5", 30 | "anyone": "0.0.5", 31 | "babel-eslint": "^10.0.1", 32 | "babel-loader": "^8.0.6", 33 | "babel-plugin-add-module-exports": "^1.0.2", 34 | "chai": "^4.2.0", 35 | "cross-env": "^7.0.3", 36 | "date-fns": "^2.0.1", 37 | "eslint": "^7.29.0", 38 | "eslint-loader": "^4.0.2", 39 | "faker": "^5.5.3", 40 | "lodash": "^4.17.15", 41 | "mocha": "^9.0.3", 42 | "n4s": "0.4.1", 43 | "node-fetch": "^2.6.0", 44 | "regenerator-runtime": "^0.13.3", 45 | "rollup": "^2.55.1", 46 | "rollup-plugin-babel": "^4.3.2", 47 | "rollup-plugin-commonjs": "^10.0.2", 48 | "rollup-plugin-node-resolve": "^5.2.0", 49 | "rollup-plugin-replace": "^2.2.0", 50 | "rollup-plugin-terser": "^5.0.0", 51 | "semver": "^7.3.5", 52 | "sinon": "^11.1.1", 53 | "typescript": "^4.3.5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/test/index.md: -------------------------------------------------------------------------------- 1 | # The `test` function 2 | The test function is a single test in your validations. It is similar to unit tests `it` or `test` function. `test` accepts the name of the test, the error message and the actual test logic. Tests can either be sync or async. 3 | 4 | You may have multipe `test` functions validating different aspects each field, each with a different error. 5 | 6 | ```js 7 | import passable, { enforce } from 'passable'; 8 | 9 | passable('MyForm', (test) => { 10 | 11 | test('name', 'should be at least 3 chars', () => { 12 | enforce(data.name).longerThanOrEquals(3); 13 | }); 14 | 15 | test('name', 'must be unique', new Promise((resolve, reject) => { 16 | fetch(`/userExists?name=${name}`) 17 | .then(res => res.json) 18 | .then(data => { 19 | if (data.exists) { 20 | reject(); 21 | } else { 22 | resolve(); 23 | } 24 | } 25 | })); 26 | 27 | test('age', 'must be at least 18', () => { 28 | enforce(data.age).greaterThanOrEquals(18); 29 | }); 30 | }); 31 | ``` 32 | 33 | ## Synchronous Tests 34 | In most cases, you would want to test some already existing data value against predefined rules, for example - input field length or email regex. These kinds of validations can be performed synchronously, as they only rely on information already present for you when you initialize the test suite. 35 | 36 | The synchronous test is simply a callback function passed as the third argument of the `test` function. If this function throws an exception or explicitly returns `false` the test is considered to be failing. Otherwise, it passes implicitly. 37 | 38 | ```js 39 | test('name', 'should be at least 3 chars', () => { 40 | enforce(data.name).longerThan(2); 41 | }); 42 | ``` 43 | 44 | ### Table of Contents 45 | * [Async tests](test/async.md) 46 | * [How to fail a test](test/how_to_fail.md) 47 | * [Warn only test](test/warn_only_tests.md) 48 | * [Running a specific tests](test/specific.md) 49 | -------------------------------------------------------------------------------- /docs/utilities/README.md: -------------------------------------------------------------------------------- 1 | # Passable utilities and helpers 2 | 3 | Using Passable on its own usually covers most of your needs, but sometimes you could use the following utility functions to cover the less common use cases. 4 | 5 | ## `any()` for OR relationship tests 6 | 7 | > Since 7.1.0 8 | 9 | Sometimes you need to have `OR` (`||`) relationship in your validations, this is tricky to do on your own, and `any()` simplifies this process. 10 | 11 | The general rule for using `any()` in your validation is when you can say: "At least one of the following has to pass". 12 | 13 | A good example would be: When your validated field can be either empty (not required), but when field - has to pass some validation. 14 | 15 | ### Usage 16 | `any()` accepts an infinite number of arguments, all of which are functions. It returns a function, that when called - behaves just like a passable [`test`](../test/index.md) function callback, and it fails on [these conditions](../test/how_to_fail.md). 17 | 18 | The only difference is - if any of the supplied tests passes, the success condition is met and `any()` returns true. 19 | 20 | ```js 21 | import passable, { test, enforce, any } from 'passable'; 22 | 23 | const validation = (data) => passable('Checkout', () => { 24 | 25 | test('coupon', 'When filled, must be at least 5 chars', any( 26 | () => enforce(data.coupon).isEmpty(), 27 | () => enforce(data.coupon).longerThanOrEquals(5) 28 | )); 29 | }); 30 | 31 | ``` 32 | 33 | ## `validate()` for leaner tests 34 | 35 | > Since 5.10.0 36 | 37 | In cases where you have only a handful of fields to validate, you might want to use `enforce`, but not wrap it with a whole passable suite. For these cases, you can use `valiate`, which can wrap enforce, and gives you back a boolean result. 38 | 39 | ### Usage 40 | 41 | `validate` accepts one argument: 42 | 43 | | Argument | Type | Required? | Description | 44 | |------------|------------|-----------|-------------| 45 | | `test` | `function` | Yes | The test function to run | 46 | 47 | Just like the [`test`](../test/index.md) function callback, `validate` will fail your tests on [these conditions](../test/how_to_fail.md). 48 | 49 | ```js 50 | 51 | import { validate, enforce } from 'passable'; 52 | 53 | // name = 'Eve' 54 | const valid = validate(() => { 55 | enforce(name).longerThanOrEquals(5); 56 | }); 57 | 58 | // valid = false; 59 | ``` -------------------------------------------------------------------------------- /docs/test/async.md: -------------------------------------------------------------------------------- 1 | # Asynchronous Tests 2 | 3 | Sometimes you would want to validate data with information from the server, for example - username availability. In these cases, you should add an async test to your suite, reaching the server before going in and performing the validation. An async test is a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) object. When it *resolves*, the test passes, and when it *rejects* the test fails. An async test will not complete unless either happens. 4 | 5 | There are two ways to perform an async test. One, is by passing a promise as your test, and the other by returning a promise from your test function. 6 | 7 | ## Passing a Promise directly 8 | 9 | ```js 10 | test('name', 'must be unique', new Promise((resolve, reject) => { 11 | fetch(`/userExists?name=${name}`) 12 | .then(res => res.json) 13 | .then(data => { 14 | if (data.exists) { 15 | reject(); // rejects and marks the test as failing 16 | } else { 17 | resolve(); // completes. doesn't mark the test as failing 18 | } 19 | }); 20 | })); 21 | ``` 22 | 23 | ## Returning a Promise / Async/Await 24 | 25 | ```js 26 | test('name', 'Should be unique', async () => { 27 | const res = await doesUserExist(user); 28 | return res; 29 | }); 30 | 31 | test('name', 'I fail', async () => Promise.reject()); 32 | ``` 33 | 34 | ## Rejecting with rejection message 35 | 36 | What if your promise can reject with different messages? No problem! 37 | You can reject the promise with your own message by passing it to the 38 | rejection callback. 39 | 40 | Notice that when using rejection messages we do not need to pass `statement` 41 | argument to `test`. This means that the statement will always be inferred 42 | from the rejection message. 43 | 44 | In case you do pass `statement`, it will serve as a fallback message in any 45 | case that the rejection message is not provided. 46 | 47 | ```js 48 | test('name', new Promise((resolve, reject) => { 49 | fetch(`/checkUsername?name=${name}`) 50 | .then(res => res.json) 51 | .then(data => { 52 | if (data.status === 'fail') { 53 | reject(data.message); // rejects with message and marks the test as failing 54 | } else { 55 | resolve(); // completes. doesn't mark the test as failing 56 | } 57 | }); 58 | })); 59 | ``` -------------------------------------------------------------------------------- /src/core/test/lib/TestObject/spec.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker'; 2 | import _ from 'lodash'; 3 | import { expect } from 'chai'; 4 | import { WARN, FAIL } from '../../../../constants'; 5 | import passableResult from '../../../passableResult'; 6 | import TestObject from '.'; 7 | 8 | describe('TestObject module', () => { 9 | let testObject, ctx, testFn, fieldName, statement, severity; 10 | 11 | beforeEach(() => { 12 | ctx = { 13 | result: passableResult(faker.lorem.word()), 14 | pending: [] 15 | }; 16 | testFn = _.noop; 17 | fieldName = faker.lorem.word(); 18 | statement = faker.lorem.sentence(); 19 | severity = _.sample([WARN, FAIL]); 20 | 21 | testObject = new TestObject( 22 | ctx, 23 | testFn, 24 | fieldName, 25 | statement, 26 | severity 27 | ); 28 | }); 29 | 30 | describe('.fail() methoh', () => { 31 | it('Should set `failed` to true', () => { 32 | expect(testObject.failed).to.equal(false); 33 | testObject.fail(); 34 | expect(testObject.failed).to.equal(true); 35 | }); 36 | }); 37 | 38 | describe('.setPending() method', () => { 39 | it('Should push current instance to pending array', () => { 40 | expect(ctx.pending).to.have.lengthOf(0); 41 | testObject.setPending(); 42 | expect(ctx.pending).to.have.lengthOf(1); 43 | expect(ctx.pending).to.have.members([testObject]); 44 | }); 45 | }); 46 | 47 | describe('.clearPending() method', () => { 48 | beforeEach(() => { 49 | testObject.setPending(); 50 | }); 51 | 52 | it('Should push current instance to pending array', () => { 53 | expect(ctx.pending).to.have.members([testObject]); 54 | testObject.clearPending(); 55 | expect(ctx.pending).not.to.have.members([testObject]); 56 | }); 57 | }); 58 | 59 | describe('.valueOf()', () => { 60 | 61 | describe('Default case', () => { 62 | it('Should return true', () => { 63 | expect(testObject == true).to.equal(true); // eslint-disable-line 64 | }); 65 | }); 66 | 67 | describe('When invalid', () => { 68 | 69 | beforeEach(() => { 70 | testObject.fail(); 71 | }); 72 | 73 | it('Should return false', () => { 74 | expect(testObject == false).to.equal(true); // eslint-disable-line 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /docs/enforce.md.bak: -------------------------------------------------------------------------------- 1 | # Enforce 2 | For assertions, Passable is bundled with [Enforce](npmjs.com/package/n4s). Enforce is a validation assertions library. It allows you to run your data against rules and conditions and test whether it passes your validations. It is intended for validation logic that gets repeated over and over again and should not be written manually. It comes with a wide-variety of pre-built rules, but it can also be extended to support your own repeated custom logic. 3 | 4 | The way Enforce operates is similar to most common assertion libraries. You pass it a value, and one or more rules to test your value against - if the validation fails, it throws an Error, otherwise - it will move on to the next rule rule in the chain. 5 | 6 | ```js 7 | import { passable } from 'passable' 8 | 9 | enforce(4) 10 | .isNumber(); 11 | // passes 12 | 13 | enforce(4) 14 | .isNumber() 15 | .greaterThan(2); 16 | // passes 17 | 18 | enforce(4) 19 | .lessThan(2) // throws an error, will not carry on to the next rule 20 | .greaterThan(3); 21 | ``` 22 | 23 | ## Content 24 | - [List of Enforce rules](#list-of-enforce-rules) 25 | - [Custom Enforce Rules](#custom-enforce-rules) 26 | 27 | Enforce exposes all predefined and custom rules. You may use chaining to make multiple enfocements for the same value. 28 | 29 | {{LIST_OF_ENFORCE_RULES}} 30 | 31 | # Custom enforce rules 32 | To make it easier to reuse logic across your application, sometimes you would want to encapsulate bits of logic in rules that you can use later on, for example, "what's considered a valid email". 33 | 34 | Your custom rules are essentially a single javascript object containing your rules. 35 | ```js 36 | const myCustomRules = { 37 | isValidEmail: (value) => value.indexOf('@') > -1, 38 | hasKey: (value, {key}) => value.hasOwnProperty(key), 39 | passwordsMatch: (passConfirm, options) => passConfirm === options.passConfirm && options.passIsValid 40 | } 41 | ``` 42 | Just like the predefined rules, your custom rules can accepts two parameters: 43 | * `value` The actual value you are testing against. 44 | * `args` (optional) the arguments which you pass on when running your tests. 45 | 46 | 47 | You can extend enforce with your custom rules by creating a new instance of `Enforce` and adding the rules object as the argument. 48 | 49 | ```js 50 | import { Enforce } from 'passable'; 51 | 52 | const myCustomRules = { 53 | isValidEmail: (value) => value.indexOf('@') > -1, 54 | hasKey: (value, key) => value.hasOwnProperty(key), 55 | passwordsMatch: (passConfirm, options) => passConfirm === options.passConfirm && options.passIsValid 56 | } 57 | 58 | const enforce = new Enforce(myCustomRules); 59 | 60 | enforce(user.email).isValidEmail(); 61 | ``` 62 | -------------------------------------------------------------------------------- /src/core/Specific/index.js: -------------------------------------------------------------------------------- 1 | /** Class representing validation inclusion and exclusion groups */ 2 | class Specific { 3 | 4 | /** 5 | * Initialize Specific object 6 | * 7 | * @param {String | Array | Object | undefined} specific 8 | */ 9 | constructor(specific) { 10 | 11 | if (!specific) { return; } 12 | 13 | if (!Specific.is(specific)) { 14 | throw new TypeError(); 15 | } 16 | 17 | if (typeof specific === 'string' || Array.isArray(specific)) { 18 | if (specific.length === 0) { return; } 19 | this.only = this.populateGroup(this.only, specific); 20 | return; 21 | } 22 | 23 | if (specific.only) { 24 | this.only = this.populateGroup(this.only, specific.only); 25 | } 26 | 27 | if (specific.not) { 28 | this.not = this.populateGroup(this.not, specific.not); 29 | } 30 | } 31 | 32 | /** 33 | * Populate inclusion and exclusion groups 34 | * 35 | * @param {Object} group - the group to populate. 36 | * @param {String | Array} field - the field to add to the group 37 | * @return {Object} modified group 38 | */ 39 | populateGroup(group, field) { 40 | group = group || {}; 41 | 42 | if (typeof field === 'string') { 43 | group[field] = true; 44 | } else if (Array.isArray(field)) { 45 | field.forEach((item) => group[item] = true); 46 | } 47 | 48 | return group; 49 | } 50 | 51 | /** 52 | * Checkes whether a given field name is in exclusion group 53 | * or not a member of inclusion group (when present) 54 | * 55 | * @param {String} fieldName 56 | * @return {Boolean} 57 | */ 58 | excludes(fieldName) { 59 | if (this.only && !this.only[fieldName]) { 60 | return true; 61 | } 62 | 63 | if (this.not && this.not[fieldName]) { 64 | return true; 65 | } 66 | 67 | return false; 68 | } 69 | 70 | /** 71 | * Test whether a given argument matches 72 | * the `specific` filter convention 73 | * 74 | * @param {Any} item 75 | * @return {boolean} 76 | */ 77 | static is(item) { 78 | if (Array.isArray(item)) { 79 | return item.every((item) => typeof item === 'string'); 80 | } 81 | 82 | if (typeof item === 'string') { return true; } 83 | 84 | if (item !== null && typeof item === 'object' && ( 85 | item.hasOwnProperty('only') 86 | || item.hasOwnProperty('not') 87 | )) { 88 | return true; 89 | } 90 | 91 | return false; 92 | } 93 | } 94 | 95 | export default Specific; -------------------------------------------------------------------------------- /src/spec/passable.api.severity.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | const runSpec = (passable) => { 4 | const { WARN } = passable; 5 | 6 | describe('Test warn flag', () => { 7 | it('Should mark test with warning', () => { 8 | expect(warnPass.hasErrors()).to.equal(false); 9 | expect(warnPass.hasWarnings()).to.equal(true); 10 | }); 11 | 12 | it('Should mark test with both warning and error', () => { 13 | expect(warnFail.hasErrors()).to.equal(true); 14 | expect(warnFail.hasWarnings()).to.equal(true); 15 | }); 16 | 17 | it('Should only fail test', () => { 18 | expect(fail.hasErrors()).to.equal(true); 19 | expect(fail.hasWarnings()).to.equal(false); 20 | }); 21 | }); 22 | 23 | // Actual test data 24 | 25 | const warnPass = passable('WarnPass', (test) => { 26 | test('WarnPass', 'should warn', () => false, WARN); 27 | }), 28 | warnFail = passable('WarnFail', (test) => { 29 | test('Warn', 'should warn', () => false, WARN); 30 | test('Fail', 'should Fail', () => false); 31 | }), 32 | fail = passable('Fail', (test) => { 33 | test('Warn', 'should not warn', () => true, WARN); 34 | test('Fail', 'should Fail', () => false); 35 | }); 36 | 37 | const warnPassExpected = { 38 | name: 'WarnPass', 39 | skipped: [], 40 | testsPerformed: { 41 | WarnPass: { testCount: 1, failCount: 0, warnCount: 1 } 42 | }, 43 | errors: {}, 44 | warnings: { WarnPass: ['should warn'] }, 45 | failCount: 0, 46 | warnCount: 1, 47 | testCount: 1 48 | }, 49 | warnFailExpected = { 50 | name: 'WarnFail', 51 | skipped: [], 52 | testsPerformed: { 53 | Warn: { testCount: 1, failCount: 0, warnCount: 1 }, 54 | Fail: { testCount: 1, failCount: 1, warnCount: 0 } 55 | }, 56 | errors: { Fail: ['should Fail'] }, 57 | warnings: { Warn: ['should warn'] }, 58 | failCount: 1, 59 | warnCount: 1, 60 | testCount: 2 61 | }, 62 | failExpected = { 63 | name: 'Fail', 64 | skipped: [], 65 | testsPerformed: { 66 | Warn: { testCount: 1, failCount: 0, warnCount: 0 }, 67 | Fail: { testCount: 1, failCount: 1, warnCount: 0 } 68 | }, 69 | errors: { Fail: ['should Fail'] }, 70 | warnings: {}, 71 | failCount: 1, 72 | warnCount: 0, 73 | testCount: 2 74 | }; 75 | }; 76 | 77 | runSpec(require('../')); 78 | runSpec(require('../../dist/passable')); 79 | runSpec(require('../../dist/passable.min.js')); 80 | -------------------------------------------------------------------------------- /docs/test/specific.md: -------------------------------------------------------------------------------- 1 | # Running or skipping `specific` tests 2 | Sometimes you want to test only a specific field out of the whole dataset. For example, when validating upon user interaction (such as input change), you probably do not need to validate all other fields as well. Similarly, you might want to **not** run the tests of specific fields, for example - fields that have not been touched by the user. 3 | 4 | To specify which fields should or should not run, use the `specific` param. It is the third argument in the `passable()` function, and it is optional. 5 | 6 | `specific` accepts any of the following: 7 | 8 | | Type | Description 9 | |-----------------|------------ 10 | | `undefined` | No `test` will be skipped (same as empty array or empty string `[] | ''`) 11 | | `string` | The names of the `test` function that will run. All the rest will be skipped. 12 | | `Array` | Array with the names of the `test` functions that should run. All the rest will be skipped. 13 | | `Object` | Allows both setting `test` functions that should run, or not run 14 | 15 | **Remember** Using the array or string directly as your `specific` param is exactly the same as using `only`, and is provided as a shorthand to reduce clutter. 16 | 17 | ```js 18 | passable('formName', (test) => {...}, ['field_1', 'field_2']) 19 | // ------ 20 | passable('formName', (test) => {...}, 'field_1') 21 | ``` 22 | 23 | ## `specific` object structure: 24 | > Since 6.0.0 25 | 26 | As noted before, The `specific` object gives you more control on which fields should be tested and which should not. It may contain any of the following keys: 27 | 28 | | Name | Type | Description 29 | |--------|------------------|----- 30 | | `only` | `Array`/`String` | Only these `test` functions will run. The rest will be skipped 31 | | `not` | `Array`/`String` | These `test` functions will be skipped. The rest will run normally 32 | 33 | ```js 34 | passable('formName', (test) => {...}, {only: 'field_1'}) 35 | // ------ 36 | passable('formName', (test) => {...}, {only: ['field_1', 'field_2']}) 37 | // ------ 38 | passable('formName', (test) => {...}, {not: 'field_1'}) 39 | // ------ 40 | passable('formName', (test) => {...}, {not: ['field_1', 'field_2']}) 41 | ``` 42 | 43 | ## Production use 44 | 45 | The easiest way to use the `specific` argument in production, is to wrap your validation with a function that passes down the fields to include, only if needed. 46 | 47 | In the following example, only First test is going to run. Second will be skipped. 48 | ```js 49 | // app.js 50 | import validate from './validate.js'; 51 | const result = validate(['First'], data); 52 | 53 | // validation.js 54 | function validate (specific) { 55 | return passable('MyForm', (test) => { 56 | test('First', 'should pass', () => {...}); 57 | test('Second', 'should be skipped', () => {...}); 58 | }, specific); 59 | }; 60 | ``` -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@fiverr.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module", 4 | "ecmaFeatures": { 5 | "globalReturn": true, 6 | "modules": true 7 | } 8 | }, 9 | "parser": "babel-eslint", 10 | "env": { 11 | "browser": true, 12 | "es6": true, 13 | "node": true, 14 | "mocha": true 15 | }, 16 | "globals": { 17 | "document": false, 18 | "escape": false, 19 | "navigator": false, 20 | "unescape": false, 21 | "window": false, 22 | "describe": true, 23 | "before": true, 24 | "it": true, 25 | "sinon": true 26 | }, 27 | "rules": { 28 | "eqeqeq": 2, 29 | "no-multi-spaces": [2, { 30 | "exceptions": { 31 | "VariableDeclarator": true, 32 | "Property": false 33 | } 34 | }], 35 | "comma-dangle": 2, 36 | "comma-spacing": 2, 37 | "comma-style": 2, 38 | "computed-property-spacing": 2, 39 | "dot-notation": 2, 40 | "func-call-spacing": 2, 41 | "indent": ["error", 4, { 42 | "SwitchCase": 1 43 | }], 44 | "jsx-quotes": 2, 45 | "keyword-spacing": 2, 46 | "lines-around-comment": ["error", { 47 | "beforeBlockComment": true, 48 | "allowBlockStart": true, 49 | "beforeLineComment": true 50 | }], 51 | "new-cap": 2, 52 | "no-lonely-if": 2, 53 | "no-case-declarations": 0, 54 | "no-multiple-empty-lines": 2, 55 | "no-trailing-spaces": 1, 56 | "no-unneeded-ternary": 2, 57 | "no-whitespace-before-property": 2, 58 | "quotes": ["error", "single", { 59 | "avoidEscape": true 60 | }], 61 | "semi-spacing": 2, 62 | "semi": ["error", "always"], 63 | "space-before-function-paren": ["error", "never"], 64 | "space-in-parens": 2, 65 | "spaced-comment": ["error", "always"], 66 | "vars-on-top": 2, 67 | "radix": 2, 68 | "no-nested-ternary": 2, 69 | "one-var-declaration-per-line": 2, 70 | "no-unsafe-negation": 2, 71 | "array-callback-return": 2, 72 | "block-scoped-var": 2, 73 | "curly": 2, 74 | "default-case": 2, 75 | "no-caller": 2, 76 | "no-eval": 2, 77 | "no-floating-decimal": 2, 78 | "no-global-assign": 2, 79 | "no-implied-eval": 2, 80 | "no-loop-func": 2, 81 | "no-empty-function": 2, 82 | "no-extra-bind": 2, 83 | "no-sequences": 2, 84 | "no-useless-call": 2, 85 | "no-useless-escape": 2, 86 | "no-useless-return": 2, 87 | "no-with": 2, 88 | "wrap-iife": [2, "inside"], 89 | "consistent-this": 2, 90 | "arrow-body-style": 2, 91 | "arrow-parens": 2, 92 | "arrow-spacing": 2, 93 | "no-duplicate-imports": 2, 94 | "no-template-curly-in-string": 2, 95 | "no-useless-computed-key": 2, 96 | "no-useless-constructor": 2, 97 | "no-useless-rename": 2, 98 | "no-var": 2, 99 | "object-shorthand": ["error", "properties"], 100 | "prefer-arrow-callback": 2, 101 | "prefer-const": ["error", { 102 | "destructuring": "all", 103 | "ignoreReadBeforeAssign": false 104 | }], 105 | "prefer-spread": 2, 106 | "prefer-template": 2, 107 | "rest-spread-spacing": 2, 108 | "template-curly-spacing": 2 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # ![Passable](https://cdn.rawgit.com/fiverr/passable/master/docs/_assets/logo.png?raw=true "Passable") 2 | 3 | Declarative data validations. 4 | 5 | [![npm version](https://badge.fury.io/js/passable.svg)](https://badge.fury.io/js/passable) [![Build Status](https://travis-ci.org/fiverr/passable.svg?branch=master)](https://travis-ci.org/fiverr/passable) 6 | 7 | 8 | - [Documentation homepage](https://fiverr.github.io/passable/) 9 | - [Try it live](https://stackblitz.com/edit/passable-example?file=validate.js) 10 | - [Getting started](getting_started/writing_tests). 11 | 12 | ## What is Passable? 13 | Passable is a library for JS applications for writing validations in a way that's structured and declarative. 14 | 15 | Inspired by the syntax of modern unit testing framework, passable validations are written as a spec or a contract, that reflects your form structure. 16 | Your validations run in production code, and you can use them in any framework (or without any framework at all). 17 | 18 | The idea behind passable is that you can easily adopt its very familiar syntax, and transfer your knowledge from the world of testing to your form validations. 19 | 20 | Much like most testing frameworks, Passable comes with its own assertion function, [enforce](./enforce.md), all error based assertion libraries are supported. 21 | 22 | ## Key features 23 | 1. [Non failing tests](test/warn_only_tests). 24 | 2. [Conditionally running tests](test/specific). 25 | 3. [Async validations](test/async). 26 | 4. [Test callbacks](getting_started/callbacks). 27 | 28 | --- 29 | 30 | ## Syntactic differences from testing frameworks 31 | 32 | Since Passable is running in production environment, and accommodates different needs, some changes to the basic unit test syntax have been made, to cover the main ones quickly: 33 | 34 | - Your test function is not available globally, it is an argument passed to your suite's callback. 35 | - Each test has two string values before its callback, one for the field name, and one for the error returned to the user. 36 | - Your suite accepts another argument after the callback - name (or array of names) of a field. This is so you can run tests selectively only for changed fields. 37 | 38 | ```js 39 | // validation.js 40 | import passable, { enforce } from 'passable'; 41 | 42 | const validation = (data) => passable('NewUserForm', (test) => { 43 | 44 | test('username', 'Must be at least 3 chars', () => { 45 | enforce(data.username).longerThanOrEquals(3); 46 | }); 47 | 48 | test('email', 'Is not a valid email address', () => { 49 | enforce(data.email) 50 | .isNotEmpty() 51 | .matches(/[^@]+@[^\.]+\..+/g); 52 | }); 53 | }); 54 | 55 | export default validation; 56 | ``` 57 | 58 | ```js 59 | // myFeature.js 60 | import validation from './validation.js'; 61 | 62 | const res = validation({ 63 | username: 'example', 64 | email: 'email@example.com' 65 | }); 66 | 67 | res.hasErrors() // returns whether the form has errors 68 | res.hasErrors('username') // returns whether the 'username' field has errors 69 | res.getErrors() // returns an object with an array of errors per field 70 | res.getErrors('username') // returns an array of errors for the `username` field 71 | ``` 72 | 73 | ## "BUT HEY! I ALREADY USE X VALIDATION LIBRARY! CAN IT WORK WITH PASSABLE?" 74 | As a general rule, Passable works similarly to unit tests in term that if your test throws an exception, it is considered to be failing. Otherwise, it is considered to be passing. 75 | 76 | There are a [few more ways](test/how_to_fail) to handle failures in order to ease migration, and in most cases, you can move your validation logic directly to into Passable with only a few adjustments. 77 | 78 | For example, if you use a [different assertion libraries](compatability/assertions) such as `chai` (expect) or `v8n`, you can simply use it instead of enforce, and it should work straight out of the box. 79 | -------------------------------------------------------------------------------- /src/spec/passable.exports.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import passable, { Enforce, enforce, validate } from '../index'; 3 | const passableExports = require('../index'); 4 | import { version } from '../../package.json'; 5 | 6 | describe("Test Passable's exports", () => { 7 | 8 | it('Should expose all outward facing passable API functions', () => { 9 | const exportsArray = Object.keys(passableExports); 10 | const names = ['validate', 'enforce']; 11 | names.forEach((name) => { 12 | expect(passableExports[name]).to.be.a('function'); 13 | }); 14 | }); 15 | 16 | describe("Test passable's default export", () => { 17 | it('Default export should output correct version number (es6 imports)', () => { 18 | expect(passable.VERSION).to.equal(version); 19 | }); 20 | 21 | it('Default export should output correct version number (commonjs)', () => { 22 | expect(passableExports.VERSION).to.equal(version); 23 | }); 24 | 25 | it('Default export name should be `passable` (commonjs)', () => { 26 | expect(passableExports.name).to.equal('passable'); 27 | }); 28 | 29 | it('Default export name should be `passable` (es6 imports)', () => { 30 | expect(passable.name).to.equal('passable'); 31 | }); 32 | }); 33 | 34 | describe('Test Enforce import', () => { 35 | it('enforce instance should be on passable (commonjs)', () => { 36 | expect(passableExports.enforce).to.be.a('function'); 37 | }); 38 | 39 | it('enforce constructor should be assigned to passable (commonjs)', () => { 40 | expect(passableExports.Enforce).to.be.a('function'); 41 | }); 42 | 43 | it('enforce instance should be on passable (es6 imports)', () => { 44 | expect(passable.enforce).to.be.a('function'); 45 | }); 46 | 47 | it('enforce constructor should be on passable (es6 imports)', () => { 48 | expect(passable.Enforce).to.be.a('function'); 49 | }); 50 | 51 | it('enforce instance should be destructurable directly from passable', () => { 52 | expect(enforce).to.be.a('function'); 53 | }); 54 | 55 | it('enforce constructor should be destructurable directly from passable', () => { 56 | expect(Enforce).to.be.a('function'); 57 | }); 58 | }); 59 | 60 | describe('Test validate import', () => { 61 | it('validate should be assigned to passable (commonjs)', () => { 62 | expect(passableExports.validate).to.be.a('function'); 63 | }); 64 | 65 | it('validate should be assigned to passable (es6 imports)', () => { 66 | expect(passable.validate).to.be.a('function'); 67 | }); 68 | 69 | it('validate should be destructurable directly from passable', () => { 70 | expect(validate).to.be.a('function'); 71 | }); 72 | 73 | it('validate function name should be validate (es6 imports)', () => { 74 | expect(passable.validate.name).to.equal('validate'); 75 | }); 76 | 77 | it('validate function name should be validate (commonjs)', () => { 78 | expect(passableExports.validate.name).to.equal('validate'); 79 | }); 80 | }); 81 | 82 | describe('Test severity imports', () => { 83 | it('WARN should be assigned to passable (commonjs)', () => { 84 | expect(passableExports.WARN).to.equal('warn'); 85 | }); 86 | 87 | it('WARN should be assigned to passable (es6 imports)', () => { 88 | expect(passable.WARN).to.equal('warn'); 89 | }); 90 | 91 | it('FAIL should be assigned to passable (commonjs)', () => { 92 | expect(passableExports.FAIL).to.equal('fail'); 93 | }); 94 | 95 | it('FAIL should be assigned to passable (es6 imports)', () => { 96 | expect(passable.FAIL).to.equal('fail'); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Passable](https://cdn.rawgit.com/fiverr/passable/master/docs/_assets/logo.png?raw=true "Passable") 2 | 3 | Declarative data validations. 4 | 5 | [![npm version](https://badge.fury.io/js/passable.svg)](https://badge.fury.io/js/passable) [![Build Status](https://travis-ci.org/fiverr/passable.svg?branch=master)](https://travis-ci.org/fiverr/passable) 6 | 7 | 8 | - [Documentation homepage](https://fiverr.github.io/passable/) 9 | - [Try it live](https://stackblitz.com/edit/passable-example?file=validate.js) 10 | - [Getting started](https://fiverr.github.io/passable/#/./getting_started/writing_tests). 11 | 12 | ## What is Passable? 13 | Passable is a library for JS applications for writing validations in a way that's structured and declarative. 14 | 15 | Inspired by the syntax of modern unit testing framework, passable validations are written as a spec or a contract, that reflects your form structure. 16 | Your validations run in production code, and you can use them in any framework (or without any framework at all). 17 | 18 | The idea behind passable is that you can easily adopt its very familiar syntax, and transfer your knowledge from the world of testing to your form validations. 19 | 20 | Much like most testing frameworks, Passable comes with its own assertion function, [enforce](https://fiverr.github.io/passable/#/./enforce.md), all error based assertion libraries are supported. 21 | 22 | ## Key features 23 | 1. [Non failing tests](https://fiverr.github.io/passable/#/test/warn_only_tests). 24 | 2. [Conditionally running tests](https://fiverr.github.io/passable/#/test/specific). 25 | 3. [Async validations](https://fiverr.github.io/passable/#/test/async). 26 | 4. [Test callbacks](https://fiverr.github.io/passable/#/getting_started/callbacks). 27 | 28 | --- 29 | 30 | ## Syntactic differences from testing frameworks 31 | 32 | Since Passable is running in production environment, and accommodates different needs, some changes to the basic unit test syntax have been made, to cover the main ones quickly: 33 | 34 | - Your test function is not available globally, it is an argument passed to your suite's callback. 35 | - Each test has two string values before its callback, one for the field name, and one for the error returned to the user. 36 | - Your suite accepts another argument after the callback - name (or array of names) of a field. This is so you can run tests selectively only for changed fields. 37 | 38 | ```js 39 | // validation.js 40 | import passable, { enforce } from 'passable'; 41 | 42 | const validation = (data) => passable('NewUserForm', (test) => { 43 | 44 | test('username', 'Must be at least 3 chars', () => { 45 | enforce(data.username).longerThanOrEquals(3); 46 | }); 47 | 48 | test('email', 'Is not a valid email address', () => { 49 | enforce(data.email) 50 | .isNotEmpty() 51 | .matches(/[^@]+@[^\.]+\..+/g); 52 | }); 53 | }); 54 | 55 | export default validation; 56 | ``` 57 | 58 | ```js 59 | // myFeature.js 60 | import validation from './validation.js'; 61 | 62 | const res = validation({ 63 | username: 'example', 64 | email: 'email@example.com' 65 | }); 66 | 67 | res.hasErrors() // returns whether the form has errors 68 | res.hasErrors('username') // returns whether the 'username' field has errors 69 | res.getErrors() // returns an object with an array of errors per field 70 | res.getErrors('username') // returns an array of errors for the `username` field 71 | ``` 72 | 73 | ## "BUT HEY! I ALREADY USE X VALIDATION LIBRARY! CAN IT WORK WITH PASSABLE?" 74 | As a general rule, Passable works similarly to unit tests in term that if your test throws an exception, it is considered to be failing. Otherwise, it is considered to be passing. 75 | 76 | There are a [few more ways](https://fiverr.github.io/passable/#/./test/how_to_fail) to handle failures in order to ease migration, and in most cases, you can move your validation logic directly to into Passable with only a few adjustments. 77 | 78 | For example, if you use a [different assertion libraries](https://fiverr.github.io/passable/#/./compatability/assertions) such as `chai` (expect) or `v8n`, you can simply use it instead of enforce, and it should work straight out of the box. 79 | -------------------------------------------------------------------------------- /src/lib/singleton/spec.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import faker from 'faker'; 3 | import { test } from 'mocha'; 4 | import passable from '../../'; 5 | import Context from '../../core/Context'; 6 | import singleton from '.'; 7 | import go from '../globalObject'; 8 | import { SYMBOL_PASSABLE } from './constants'; 9 | 10 | describe('singleton', () => { 11 | 12 | after(() => { 13 | singleton.register(passable); 14 | }); 15 | 16 | describe('Attaching to global scope', () => { 17 | beforeEach(() => { 18 | delete go[SYMBOL_PASSABLE]; 19 | }); 20 | 21 | afterEach(() => { 22 | delete go[SYMBOL_PASSABLE]; 23 | }); 24 | 25 | test('That global instance is not populated', () => { 26 | expect(go[SYMBOL_PASSABLE]).to.equal(undefined); 27 | }); 28 | 29 | it('Should register passable on a global object', () => { 30 | singleton.register(passable, Context); 31 | 32 | expect(go[SYMBOL_PASSABLE]).to.equal(passable); 33 | }); 34 | 35 | describe('When already registered', () => { 36 | 37 | beforeEach(() => { 38 | singleton.register(passable); 39 | }); 40 | 41 | describe('When same version', () => { 42 | it('Should return silently', (done) => { 43 | const timeout = setTimeout(done, 300); 44 | 45 | process.on('uncaughtException', (err) => { 46 | clearTimeout(timeout); 47 | }); 48 | 49 | }); 50 | }); 51 | 52 | describe('When different version', () => { 53 | 54 | let _uncaughtListeners; 55 | 56 | beforeEach(() => { 57 | _uncaughtListeners = process.listeners('uncaughtException'); 58 | process.removeAllListeners('uncaughtException'); 59 | }); 60 | 61 | afterEach(() => { 62 | _uncaughtListeners.forEach((listener) => process.on('uncaughtException', listener)); 63 | }); 64 | 65 | it('Should throw an error', (done) => { 66 | const fn = () => null; 67 | fn.VERSION = Math.random(); 68 | 69 | singleton.register(fn); 70 | 71 | process.on('uncaughtException', (err) => { 72 | expect(err.message).to.have.string('Multiple versions of Passable detected'); 73 | process.removeAllListeners('uncaughtException'); 74 | done(); 75 | }); 76 | 77 | }); 78 | }); 79 | }); 80 | 81 | }); 82 | 83 | describe('Make sure everything works together', () => { 84 | 85 | before(() => { 86 | singleton.register(passable); 87 | }); 88 | 89 | const instances = [require('../../'), require('../../../dist/passable'), require('../../../dist/passable.min.js')]; 90 | const pairs = instances.reduce((pairs, current) => ( 91 | [...pairs, ...instances.map(({ test }) => [ current, test ])] 92 | ), []); 93 | 94 | pairs.forEach(([ passable, test ]) => { 95 | it('Should produce correct validation result', () => { 96 | const failCount = _.random(1, 10); 97 | const successCount = _.random(1, 10); 98 | const warnCount = _.random(1, 10); 99 | const output = passable(faker.random.word(), () => { 100 | 101 | Array.from({ length: warnCount }, () => test(faker.random.word(), faker.lorem.sentence(), () => false, passable.WARN)); 102 | Array.from({ length: failCount }, () => test(faker.random.word(), faker.lorem.sentence(), () => false)); 103 | Array.from({ length: successCount }, () => test(faker.random.word(), faker.lorem.sentence(), () => true)); 104 | }); 105 | 106 | expect(output.failCount).to.equal(failCount); 107 | expect(output.warnCount).to.equal(warnCount); 108 | expect(output.testCount).to.equal(warnCount + failCount + successCount); 109 | }); 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/core/test/index.js: -------------------------------------------------------------------------------- 1 | import { FAIL } from '../../constants'; 2 | import { singleton } from '../../lib'; 3 | import { isTestFn, TestObject } from './lib'; 4 | 5 | /** 6 | * Run async test. 7 | * @param {TestObject} testObject A TestObject instance. 8 | */ 9 | export const runAsync = (testObject) => { 10 | const { fieldName, testFn, statement, ctx } = testObject; 11 | 12 | ctx.result.markAsync(fieldName); 13 | 14 | const done = () => { 15 | testObject.clearPending(); 16 | 17 | if (!hasRemainingPendingTests(ctx, fieldName)) { 18 | ctx.result.markAsDone(fieldName); 19 | } 20 | 21 | if (!hasRemainingPendingTests(ctx)) { 22 | ctx.result.markAsDone(); 23 | } 24 | }; 25 | 26 | const fail = (rejectionMessage) => { 27 | testObject.statement = typeof rejectionMessage === 'string' 28 | ? rejectionMessage 29 | : statement; 30 | 31 | if (ctx.pending.includes(testObject)) { 32 | testObject.fail(); 33 | } 34 | 35 | done(); 36 | }; 37 | 38 | try { 39 | testFn.then(done, fail); 40 | } catch (e) { 41 | fail(); 42 | } 43 | }; 44 | 45 | /** 46 | * Checks if there still are remaining pending tests for given criteria 47 | * @param {Object} ctx Parent context 48 | * @param {String} [fieldName] Name of the field to test against 49 | * @return {Boolean} 50 | */ 51 | const hasRemainingPendingTests = (ctx, fieldName) => { 52 | if (!ctx.pending.length) { 53 | return false; 54 | } 55 | 56 | if (fieldName) { 57 | return ctx.pending.some((testObject) => testObject.fieldName === fieldName); 58 | } 59 | 60 | return !!ctx.pending.length; 61 | }; 62 | 63 | /** 64 | * Performs "shallow" run over test functions, assuming sync tests only. 65 | * @param {TestObject} testObject TestObject instance. 66 | * @return {*} Result from test function 67 | */ 68 | const preRun = (testObject) => { 69 | let result; 70 | try { 71 | result = testObject.testFn(); 72 | } catch (e) { 73 | result = false; 74 | } 75 | 76 | if (result === false) { 77 | testObject.fail(); 78 | } 79 | 80 | return result; 81 | }; 82 | 83 | /** 84 | * Registers test, if async - adds to pending array 85 | * @param {TestObject} testObject A TestObject Instance. 86 | */ 87 | const register = (testObject) => { 88 | const { testFn, ctx, fieldName } = testObject; 89 | let pending = false; 90 | let result; 91 | 92 | if (ctx.specific.excludes(fieldName)) { 93 | ctx.result.addToSkipped(fieldName); 94 | return; 95 | } 96 | 97 | ctx.result.initFieldCounters(fieldName); 98 | ctx.result.bumpTestCounter(fieldName); 99 | 100 | if (testFn && typeof testFn.then === 'function') { 101 | pending = true; 102 | } else { 103 | result = preRun(testObject); 104 | } 105 | 106 | if (result && typeof result.then === 'function') { 107 | pending = true; 108 | 109 | testObject.testFn = result; 110 | } 111 | 112 | if (pending) { 113 | testObject.setPending(); 114 | } 115 | }; 116 | 117 | /** 118 | * Test function used by consumer to provide their own validations. 119 | * @param {String} fieldName Name of the field to test. 120 | * @param {String} [statement] The message returned in case of a failure. 121 | * @param {function | Promise} testFn The actual test callback or promise. 122 | * @param {String} [severity] Indicates whether the test should fail or warn. 123 | * @return {TestObject} A TestObject instance. 124 | */ 125 | const test = (fieldName, ...args) => { 126 | let statement, 127 | testFn, 128 | severity; 129 | 130 | if (isTestFn(args[0])) { 131 | [testFn, severity] = args; 132 | } else if (['string', 'object'].some((type) => typeof args[0] === type)) { 133 | [statement, testFn, severity] = args; 134 | } 135 | 136 | if (!isTestFn(testFn)) { 137 | return; 138 | } 139 | 140 | const testObject = new TestObject( 141 | singleton.use().ctx, 142 | fieldName, 143 | statement, 144 | testFn, 145 | severity || FAIL 146 | ); 147 | 148 | register(testObject); 149 | 150 | return testObject; 151 | }; 152 | 153 | export default test; 154 | -------------------------------------------------------------------------------- /docs/getting_started/writing_tests.md: -------------------------------------------------------------------------------- 1 | # Writing tests 2 | Much like when writing unit-tests, writing validations with Passable is all about knowing in advance which values you expect to get. The structure is very similar to the familiar unit test `describe/it/expect` combo, only that with Passable the functions you will mostly run are `Passable/test/enforce`. 3 | 4 | * `passable` - the wrapper for your form validation, much like the describe function in unit tests. 5 | * `test` - a single tests, most commonly a single field, much like the it function in unit tests. [More about test](../test/index.md) 6 | * `enforce` - the function which gets and enforces the data model compliance, similar to the expect function in unit tests. [More about enforce](../enforce.md); 7 | 8 | ## Passable Parameters 9 | The passable suite accepts three arguments: 10 | 11 | | Name | Optional? | Type | Description 12 | |------------|:---------:|:--------:|------------------------------------------------ 13 | | `name` | No | String | A name for the group of tests. E.G - form name 14 | | `tests` | No | Function | A function containing the actual validation logic. 15 | | `only/not` | Yes | Array / Object | Whitelist or blacklist of tests to run/skip in the suite see: [Running a specific tests](../test/specific.md) 16 | 17 | 18 | The most basic test would look somewhat like this: 19 | 20 | ```js 21 | // data = { 22 | // username: 'ealush', 23 | // age: 27 24 | // } 25 | passable('NewUserForm', (test) => { 26 | test('username', 'Must be a string between 2 and 10 chars', () => { 27 | enforce(data.username).isString().longerThan(1).shorterThan(11); 28 | }); 29 | 30 | test('username', 'already exists', fetch(`/check_availability?username=${data.username}`)); 31 | 32 | test('age', 'Must be greater than 18', () => { 33 | enforce(data.age).greaterThan(18); 34 | }); 35 | }); 36 | ``` 37 | 38 | In the example above, we tested a form named `NewUserForm`, and ran two tests on it. One of the `username` field, and one on the `age` field. When testing the username field, we made sure that **all** conditions are true, and when testing age, we made sure that **at least one** condition is true. 39 | 40 | If our validation fails, among other information, we would get the names of the fields, and an array of errors for each, so we can show them back to the user. 41 | 42 | ## Accessing the intermediate test result from within your suite 43 | // since 7.5.0 44 | 45 | In some cases, you might want to access the intermediate result, for example, if you'd like to stop a server call from happening for a field that you already know is invalid. To do that, you can access the draft of the result object from within the test suite. 46 | 47 | To use `draft`, import it from passable, and call it as a function. Its return value is the intermediate result of your test suite. 48 | 49 | **Note** 50 | * It is only possible to access intermediate test results for sync tests, and it is recommended to put all the async tests at the bottom of your suite so they have access to the result of all the sync tests. 51 | * You may not call `draft` from outside a running suite. Doing that will result in a thrown error. 52 | 53 | In the following example, we're preventing the async validation from running over the username field in case it already has errors. 54 | 55 | ```js 56 | import passable, { test, enforce, draft } from 'passable'; 57 | 58 | passable('NewUserForm', () => { 59 | test('username', 'Must be a string between 2 and 10 chars', () => { 60 | enforce(data.username).isString().longerThan(1).shorterThan(11); 61 | }); 62 | 63 | if (!draft().hasErrors('username')) { 64 | // if the username did not pass the previous test, the following test won't run 65 | test('username', 'already exists', fetch(`/check_availability?username=${data.username}`)); 66 | } 67 | }); 68 | ``` 69 | 70 | You can also use `draft` as the second argument of the your passable suite tests callback. This api is likely to be deprecated in an upcoming major version. Using draft this way does not require you to call it as a function. 71 | 72 | ### Example: 73 | 74 | ```js 75 | import passable, { enforce } from 'passable'; 76 | 77 | passable('NewUserForm', (test, draft) => { 78 | test('username', 'Must be a string between 2 and 10 chars', () => { 79 | enforce(data.username).isString().longerThan(1).shorterThan(11); 80 | }); 81 | 82 | if (!draft.hasErrors('username')) { 83 | // if the username did not pass the previous test, the following test won't run 84 | test('username', 'already exists', fetch(`/check_availability?username=${data.username}`)); 85 | } 86 | }); 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/getting_started/callbacks.md: -------------------------------------------------------------------------------- 1 | # Passable callbacks 2 | 3 | Passable commes with multiple callback functions that allow you to interact with async tests. 4 | 5 | # `.after()` 6 | 7 | > Since 7.0.0 8 | 9 | The after callback is a function that can be chained to a passable suite and allows invoking a callback whenever a certain field has finished running, regardless of whether it passed or failed. It accepts two arguments: `fieldName` and `callback`. You may chain multiple callbacks to the same field. 10 | 11 | When running, the `.after()` function passes down to its callback argument the current result object, **note** it might not be final yet, as there may be other async fields still being processed. 12 | 13 | ```js 14 | import passable, { enforce } from 'passable'; 15 | const email_regex = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}/gi; 16 | 17 | passable('SendEmailForm', (test) => { 18 | 19 | test('UserEmail', 'must be a valid email', () => { 20 | enforce(data.email).matches(email_email_regex); 21 | }); 22 | 23 | test('UserName', 'must not be blacklisted', new Promise((resolve, reject) => { 24 | fetch(`/isBlacklisted?name=${data.name}`).then(...).then((data) => { 25 | if (data.blackListed) { 26 | reject(); 27 | } else { 28 | resolve(); 29 | } 30 | }); 31 | })); 32 | 33 | test('Content', 'must be between 5 and 500 chars', () => { 34 | enforce(data.content).longerThan(4).shorterThan(501); 35 | }); 36 | }).after((res) => { 37 | if (res.hasErrors('username')) { 38 | showUserNameErrors(res.errors) 39 | } 40 | }); 41 | ``` 42 | 43 | # `.done()` 44 | 45 | > Since 6.1.0 46 | 47 | The `.done()` callback is a function that can be chained to a passable suite. It accepts a function to be run whenever the suite completes running all [tests](../test/index.md) (both sync, and async - if present), regardless of whether they passed or [failed](../test/how_to_fail.md). 48 | 49 | `.done()` calls can be infinitely chained to one another, and as the passable suite completes - they will all run synchronously - meaning that if there is an async action being performed in one of the callbacks, the next `.done()` call will *not* wait for it to complete before starting. 50 | 51 | When running, the `done()` function passes down to its callback function the final passable result object, so you do not have to store it in an external variable. 52 | 53 | ```js 54 | import passable, { enforce } from 'passable'; 55 | const email_regex = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}/gi; 56 | 57 | passable('SendEmailForm', (test) => { 58 | 59 | test('UserEmail', 'must be a valid email', () => { 60 | enforce(data.email).matches(email_email_regex); 61 | }); 62 | 63 | test('UserName', 'must not be blacklisted', new Promise((resolve, reject) => { 64 | fetch(`/isBlacklisted?name=${data.name}`).then(...).then((data) => { 65 | if (data.blackListed) { 66 | reject(); 67 | } else { 68 | resolve(); 69 | } 70 | }); 71 | })); 72 | 73 | test('Content', 'must be between 5 and 500 chars', () => { 74 | enforce(data.content).longerThan(4).shorterThan(501); 75 | }); 76 | }).done((res) => { 77 | if (res.hasErrors()) { 78 | showValidationErrors(res.errors) 79 | } 80 | }).done(reportToServer).done(promptUserQuestionnaire); 81 | ``` 82 | 83 | # `.cancel()` 84 | 85 | > Since 7.0.0 86 | 87 | When running your validation suite multiple times in a short amount of time - for example, when validating user inputs upon change, your async validations may finish after you already started running the suite again. This will cause the `.done()` and `.after()` callbacks of the previous run to be run in proximity to the `.done()` and `.after()` callbacks of the current run. 88 | 89 | Depending on what you do in your callbacks, this can lead to wasteful action, or to validation state rapidly changing in front of the user's eyes. 90 | 91 | To combat this, there's the `.cancel()` callback, which cancels any pending `.done()` and `.after()` callbacks. 92 | 93 | You can use it in many ways, but the simplistic way to look at it is this: You need to keep track of your cancel callback in a scope that's still going to be accessible in the next run. 94 | 95 | Example: 96 | 97 | ```js 98 | let cancel = null; 99 | 100 | // this is a simple event handler 101 | const handleChange = (e) => { 102 | 103 | if (cancel) { 104 | cancel(); // now, if cancel already exists, it will cancel any pending callbacks 105 | } 106 | 107 | // you should ideally import your suite from somewhere else, this is here just for the demonstration 108 | const result = passable('MyForm', (test) => { 109 | // some async validations go here 110 | }); 111 | 112 | result.done((res) => { 113 | // do something 114 | }); 115 | 116 | cancel = result.cancel; // save the cancel callback aside 117 | } 118 | ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [7.5.0] - 2019-11-12 8 | 9 | ### Additions 10 | - 37ed4d7 [Minor] Add draft as an export from passable (#204) (ealush) 11 | - aff8c86 [Minor] Create a Passable singleton | future hook support (#213) (ealush) 12 | 13 | ## [7.4.0] - 2019-11-04 14 | 15 | ### Additions 16 | - c0a9aec [MINOR] Add return value for test. (#197) (Evyatar Alush) 17 | 18 | ### Fixes and non breaking changes 19 | - 7ec4daf Use anyone/any instead of any (#198) (Evyatar Alush) 20 | 21 | ## [7.3.0] - 2019-09-09 22 | 23 | ### Additions 24 | - d0e88e8 [MINOR] Allow Failing With Rejection Message On Async Tests (Micha Sherman) 25 | 26 | ## [7.2.4] - 2019-08-17 27 | 28 | ### Fixed 29 | - [PATCH] Lookup Proxy on global namespace 30 | 31 | ## [7.2.2] - 2019-08-12 32 | 33 | ### Changed 34 | - [Docs] Improved Enforce documentation 35 | - [Patch] Use n4s npm package 36 | 37 | ## [7.2.0] - 2019-08-03 38 | 39 | ### Added 40 | - [Minor] Add isEven rule. 41 | - [Minor] Add isOdd rule. 42 | 43 | ### Changed 44 | - [Patch] Make result callback functions non-enumerable 45 | 46 | 47 | ## [7.1.1] - 2019-06-24 48 | 49 | ### Added 50 | - [Patch] Add type definitions for cancel callback. 51 | 52 | ## [7.1.0] - 2019-06-23 53 | 54 | ### Added 55 | - [Minor] Added an option to directly import `test` from passable; 56 | - [Minor] `any` utility for assertions with `OR` `||` relationships. 57 | 58 | ## [7.0.0] - 2019-06-11 59 | 60 | ### Changed 61 | - [Major] Lowercased library name when imported on global object. 62 | - [Major] Renamed `validationErrors` and `validationWarnings` output properties to `errors` and `warnings`. 63 | - [Patch] Guarantee that `.done()` callbacks only run once. 64 | 65 | ### Added 66 | - [Minor] `.after()` callback that can run after a specific field finished execution. 67 | - [Minor] New size rules (`lessThan`, `greaterThan`, `lessThanOrEquals`, `greaterThanOrEquals`, `numberEquals`, `numberNotEquals`). 68 | - [Minor] New size rules (`longerThan`, `shorterThan`, `longerThanOrEquals`, `shorterThanOrEquals`, `lengthEquals`, `lengthNotEquals`). 69 | - [Minor] New content rules (`equals`, `notEquals`). 70 | - [Minor] Support returning promise from test callback for async tests. 71 | - [Minor] Add cancel callback. 72 | 73 | ### Removed 74 | - [Major] Removed output properties: `hasValidationErrors`, `hasValidationWarnings`. 75 | - [Major] Removed compound rules (`anyOf`, `allOf`, `noneOf`). 76 | - [Major] Removed general size rules (`smallerThan`, `largerThan`, `smallerThanOrEquals`, `largerThanOrEquals`, `sizeEquals`, `sizeNotEquals`). 77 | 78 | ## [6.3.4] - 2019-05-01 79 | 80 | ### Changed 81 | - [Patch] Reduce bundle size 82 | 83 | ## [6.3.1] - 2019-02-22 84 | 85 | ### Added 86 | - [Patch] Fix documentation typo (#107) 87 | - [Patch] Add type hints to passable (#106 + #107) 88 | 89 | ## [6.3.0] - 2019-01-06 90 | 91 | ### Added 92 | - [Minor] access intermediate validation result via draft (#103) 93 | - [Patch] Publish next tag separately from feature branch (#104) 94 | - [Minor] add hasErrors and hasWarnings functions (#105) 95 | 96 | ## [6.2.0] - 2018-12-27 97 | 98 | ### Added 99 | - [Minor] mark field as async 100 | 101 | ## [6.1.3] - 2018-12-27 102 | 103 | ### Fixed 104 | - [PATCH] run done callback in sync mode 105 | 106 | ## [6.1.0] - 2018-10-20 107 | 108 | ### Added 109 | - [Minor] Async test support + done callback chaining (#100) 110 | 111 | ### Changed 112 | - [Patch] Update github configuration 113 | 114 | ## [6.0.0] - 2018-04-16 115 | 116 | ### Added 117 | - [Major] Extract Enforce as standalone API 118 | - [Patch] Create `Specific` Object 119 | - [Patch] Auto deploy next branch 120 | - [Minor] Add isNumeric check 121 | - [Minor] Initialize proxy with all keys. Add support for proxy-polyfill 122 | - [Minor] Negative rules support 123 | - [Patch] Use proxy polyfill when proxy is not supported 124 | - [Minor] Add isTruthy and isFalsy enforce rules 125 | 126 | ### Changed 127 | - [Major] `specific` field filter: Introduce `only` and `not` 128 | - [Major] Refactor rules to follow convention (Size compares against number only) 129 | - [Major] Pass - move to be the last argument 130 | - [Patch] Rename `pass` to `test` 131 | - [Major] Move specific to last argument, make optional 132 | 133 | ### Removed 134 | - [Major] Make validate leaner by removing `message` arg 135 | 136 | ### Fixed 137 | - [Patch] Type-checking errors ignores 138 | 139 | ## [5.10.3] - 2018-01-29 140 | 141 | ### Added 142 | - [Minor] Validate function 143 | - [PATCH] Commonjs require support for enforce 144 | - [Minor] Add default expect=true to rules 145 | - [Minor] export enforce as a standalone module 146 | - [Minor] Allow chaining rules directly under enforce 147 | -------------------------------------------------------------------------------- /docs/getting_started/result.md: -------------------------------------------------------------------------------- 1 | # The result object 2 | 3 | ## Properties 4 | | Name | Type | Description | 5 | |----------------------------------|------------|-----------------------------------------------------| 6 | | `name` | `String` | The name of the form being validated 7 | | `failCount` | `Number` | Overall errors count for this form 8 | | `warnCount` | `Number` | Overall warnings count for this form 9 | | `testCount` | `Number` | Overall test count in this form 10 | | `testsPerformed` | `Object{}` | Detailed stats per field (structure detailed below) 11 | | `errors` | `Object[]` | Actual errors per each field 12 | | `errors[field-name]` | `Object[]` | All error strings for this field 13 | | `warnings` | `Object[]` | Actual errors per each field 14 | | `warnings[field-name]` | `Object[]` | All warning strings for this field 15 | | `skipped` | `Array` | All skipped fields (empty, unless the `specific` option is used) 16 | | `getErrors` | `Function` | Getter function which allows accessing the errors array of one or all fields 17 | | `getWarnings` | `Function` | Getter function which allows accessing the warnings array of one or all fields 18 | | `hasErrors` | `Function` | Returns whether a certain field (or the whole suite, if no field passed) has errors 19 | | `hasWarnings` | `Function` | Returns whether a certain field (or the whole suite, if no field passed) has warnings 20 | 21 | ### `testsPerformed` field structure 22 | | Name | Type | Description | 23 | |-------------|----------|---------------------------------------| 24 | | `failCount` | `Number` | Overall errors count for this field | 25 | | `warnCount` | `Number` | Overall warnings count for this field | 26 | | `testCount` | `Number` | Overall test count in this field | 27 | 28 | ## `hasErrors` and `hasWarnings` functions 29 | > since 6.3.0 30 | 31 | If you only need to know if a certain field has validation errors or warnings but don't really care which they are, you can use `hasErrors` or `hasWarnings` functions. 32 | 33 | ```js 34 | resultObject.hasErrors('username'); 35 | // true 36 | 37 | resultObject.hasWarnings('password'); 38 | // false 39 | ``` 40 | 41 | In case you want to know whether the whole suite has errors or warnings, you can use the same functions, just without specifying a field 42 | 43 | ```js 44 | resultObject.hasErrors(); 45 | // true 46 | 47 | resultObject.hasWarnings(); 48 | // true 49 | ``` 50 | 51 | ## `getErrors` and `getWarnings` functions 52 | > since 5.10.0 53 | 54 | You can easily traverse the object tree to acess the field errors and warnings, but when accessing many fields, it can get pretty messy: 55 | 56 | ```js 57 | resultObject.errors.myField && resultObject.errors.myField[0]; 58 | ``` 59 | This is clearly not ideal. There is a shortcut to getting to a specific field: 60 | 61 | ```js 62 | resultObject.getErrors('username'); 63 | // ['Error string 1', `Error string 2`] 64 | 65 | resultObject.getWarnings('password'); 66 | // ['Warning string 1', `Warning string 2`] 67 | ``` 68 | 69 | If there are no errors for the field, the function returns an empty array: 70 | ```js 71 | resultObject.getErrors('username'); 72 | // [] 73 | 74 | resultObject.getWarnings('username'); 75 | // [] 76 | ``` 77 | 78 | ## Why isn't there an `isValid` prop? 79 | There is **no** `isValid` prop, this is by design. Passable cannot know your business logic, nor can it ever assume that `0` errors means valid result. `0` errors can be due to skipped fields. Same goes for isInvalid. Even though, usually, errors mean invalidity, it is not always the case. This is why Passable gives you all the information about the tests, but it is your job to decide whether it means that the validation failed or not. 80 | 81 | ## Passing Example 82 | ```js 83 | { 84 | "name": "NewUserForm", 85 | "failCount": 0, 86 | "warnCount": 0, 87 | "testCount": 2, 88 | "testsPerformed": { 89 | "username": { 90 | "testCount": 1, 91 | "failCount": 0, 92 | "warnCount": 0 93 | }, 94 | "age": { 95 | "testCount": 1, 96 | "failCount": 0, 97 | "warnCount": 0 98 | } 99 | }, 100 | "errors": {}, 101 | "warnings": {}, 102 | "skipped": [] 103 | } 104 | ``` 105 | 106 | ## Failing Example 107 | ```js 108 | { 109 | "name": "NewUserForm", 110 | "failCount": 2, 111 | "warnCount": 0, 112 | "testCount": 2, 113 | "testsPerformed": { 114 | "username": { 115 | "testCount": 1, 116 | "failCount": 1, 117 | "warnCount": 0 118 | }, 119 | "age": { 120 | "testCount": 1, 121 | "failCount": 1, 122 | "warnCount": 0 123 | } 124 | }, 125 | "errors": { 126 | "username": [ 127 | "Must be a string between 2 and 10 chars" 128 | ], 129 | "age": [ 130 | "Can either be empty, or larger than 18" 131 | ] 132 | }, 133 | "warnings": {}, 134 | "skipped": [] 135 | } 136 | ``` 137 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Passable 2 | Passable is an Isomorphic library which may run either on the server, or in the browser (or both), and it should be treated as such. Internal modules should not rely on any runtime specific environment, or assume any exists. 3 | 4 | It provides a declarative API for writing user input validations. When adding new consumer-facing APIs, they should keep the same declarative 'style and spirit'. 5 | 6 | ## Branching guidelines 7 | Our working branch is Master. New versions are merged to, and are published from it. Apart from master, we use a development branch called `next`. `next` is an accumulative branch, collecting all changes waiting to be published. **next pull requests are not merged directly to master**, but to `next` instead. 8 | This also means that when working on a new feature, we branch off from `next` and not from master. 9 | 10 | For urgent changes and bugfixes that cannot be wait until the next version, we open a `hotfix` branch (for example: `hotfix-fix-all-bugs`). It should branch off directly from master, and be merged back directly to master. 11 | 12 | Our `next` branch needs to be constantly updated from master so current fixes and configuration changes are present there as well. 13 | 14 | | Branch Name | Purpose | 15 | |-------------|:----------------------------------------------------------------------------------------| 16 | | master | Working branch. Ready to publish versions go here | 17 | | next | Development branch, accumulates future changes | 18 | | hotfix | Descriptive term for all hotfix branches that cannot wait until the next version update | 19 | 20 | ## Version management 21 | The version number specify `major`.`minor`.`patch` 22 | 23 | |Type | Content | Example | 24 | |------|:--------|:--------| 25 | |patch | Internal fix | Bug fix, Performance improvements, tests, small tweaks| 26 | |minor | Interface change with full backward compatibility | Adding new features, Full backwards compatibility| 27 | |major | Interface change without full backward compatibility | Changing a function name or interface, Removing a function| 28 | 29 | ## Commit message guidelines 30 | Commit messages should be labeled with the version bump the changes require. For example: 31 | 32 | * > [Patch] Fixing a broken runner 33 | * > [Patch] Documentation update 34 | * > [Patch] Configuration update 35 | * > [Minor] Adding a new runner 36 | * > [Major] Replacing all runners 37 | 38 | This is to make it easier to determine which version bump should be done upon adding a new release. 39 | 40 | ## Documentation guidelines 41 | Passable's documentation is present in [https://fiverr.github.io/passable](https://fiverr.github.io/passable). Its source is present as markdown(`.md`) files in the `documentation` folder of the project. Upon releasing a new version, a static html website is generated using [docpress](https://github.com/docpress/docpress). When adding a new page to the documentation, it needs to be added the the `content` first, which is present under: `/documentation/README.md`. Otherwise docpress will not know to look for it. 42 | 43 | When modifying existing API or adding new functionality, it is important to update the documentation as well. 44 | 45 | ## Testing 46 | There are two kinds of tests for Passable's code. All are either named or suffixed with `spec.js`. 47 | ### Unit tests 48 | Unit tests run over the source directory. These tests check each of Passable's internal interfaces. 49 | When testing a module, its spec file should be in the same directory, named `spec.js`: 50 | ``` 51 | . 52 | ├── module_name 53 | │ ├── index.js 54 | │ ├── spec.js 55 | ``` 56 | When testing a broader part of the api, a filename should be more descriptive: 57 | ``` 58 | . 59 | ├── spec 60 | │ ├── passable.api.custom.spec.js 61 | ``` 62 | the All new code has to be tested. 63 | 64 | ### Usecase tests 65 | Which test full Passable usecases, and run both on the source and distribution versions of Passable. When adding new functionality to Passable, make sure to add a corresponding usecase test. 66 | If your changes are not reflected in the usecase tests, it is probably due to you having the old distribution file in your branch. Please run: 67 | ```js 68 | npm run build 69 | // or 70 | yarn build 71 | ``` 72 | 73 | Usecase tests are located under: 74 | ``` 75 | . 76 | ├── spec 77 | │ ├── usecase 78 | │ ├── passable.usecase.spec.js 79 | │ ├── usecase_a.js 80 | │ ├── usecase_b.js 81 | ``` 82 | 83 | To run all tests: 84 | ```js 85 | npm test 86 | // or 87 | yarn test 88 | ``` 89 | 90 | ## Linting 91 | Linting is done via eslint and Fiverr's eslint configuration. 92 | 93 | ```js 94 | npm run eslint 95 | // or 96 | yarn eslint 97 | ``` 98 | 99 | ### Playground 100 | The "playground" directory is a place you can write test validations, and include __Passable__ as a module. 101 | It's node_modules contains a symlink to the parent directory so you're working with your actual work files. 102 | 103 | The content of this directory is gitignored (short of example file and __Passable__ virtual module) so feel free to add your sandbox files there 104 | -------------------------------------------------------------------------------- /src/spec/passable.api.specific.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | const runSpec = (passable) => { 4 | describe('Test running specific tests', () => { 5 | describe('Test array input', () => { 6 | 7 | it('Should only run first test', () => { 8 | const result = specificTests(['First']); 9 | 10 | expect(result.skipped).to.deep.equal(['Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']); 11 | expect(result.testCount).to.equal(1); 12 | }); 13 | 14 | it('Should run Second, Third and Seventh Test', () => { 15 | const result = specificTests(['Second', 'Third', 'Seventh']); 16 | 17 | expect(result.skipped).to.deep.equal(['First', 'Fourth', 'Fifth', 'Sixth']); 18 | expect(result.testCount).to.equal(3); 19 | }); 20 | 21 | it('Should skip all tests', () => { 22 | const result = specificTests(['IDoNotExist']); 23 | 24 | expect(result.skipped).to.deep.equal(['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']); 25 | expect(result.testCount).to.equal(0); 26 | }); 27 | 28 | it('Should run all tests', () => { 29 | const result = specificTests([]); 30 | 31 | expect(result.skipped).to.deep.equal([]); 32 | expect(result.testCount).to.equal(8); 33 | }); 34 | }); 35 | 36 | describe('Test string input', () => { 37 | 38 | it('Should only run first test', () => { 39 | const result = specificTests('First'); 40 | 41 | expect(result.skipped).to.deep.equal(['Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']); 42 | expect(result.testCount).to.equal(1); 43 | }); 44 | 45 | it('Should skip all tests', () => { 46 | const result = specificTests('IDoNotExist'); 47 | 48 | expect(result.skipped).to.deep.equal(['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']); 49 | expect(result.testCount).to.equal(0); 50 | }); 51 | 52 | it('Should run all tests', () => { 53 | const result = specificTests(''); 54 | 55 | expect(result.skipped).to.deep.equal([]); 56 | expect(result.testCount).to.equal(8); 57 | }); 58 | }); 59 | 60 | describe('Test object input', () => { 61 | 62 | it('Should only run first test', () => { 63 | const result = specificTests({ 64 | only: 'First' 65 | }); 66 | 67 | expect(result.skipped).to.deep.equal(['Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']); 68 | expect(result.testCount).to.equal(1); 69 | }); 70 | 71 | it('Should run Second, Third and Seventh Test', () => { 72 | const result = specificTests({ 73 | only: ['Second', 'Third', 'Seventh'] 74 | }); 75 | 76 | expect(result.skipped).to.deep.equal(['First', 'Fourth', 'Fifth', 'Sixth']); 77 | expect(result.testCount).to.equal(3); 78 | }); 79 | 80 | it('Should skip all tests', () => { 81 | const result = specificTests({ 82 | only: 'IDoNotExist' 83 | }); 84 | 85 | expect(result.skipped).to.deep.equal(['First', 'Second', 'Third', 'Fourth', 'Fifth', 'Sixth', 'Seventh']); 86 | expect(result.testCount).to.equal(0); 87 | }); 88 | 89 | it('Should skip Second and Fourth', () => { 90 | const result = specificTests({ 91 | not: ['Second', 'Fourth'] 92 | }); 93 | 94 | expect(result.skipped).to.deep.equal(['Second', 'Fourth']); 95 | expect(result.testCount).to.equal(6); 96 | }); 97 | 98 | it('Should skip First', () => { 99 | const result = specificTests({ 100 | not: 'First' 101 | }); 102 | 103 | expect(result.skipped).to.deep.equal(['First']); 104 | expect(result.testCount).to.equal(7); 105 | }); 106 | }); 107 | 108 | it('Should run all tests', () => { 109 | const result = specificTests(null); 110 | 111 | expect(result.skipped).to.deep.equal([]); 112 | expect(result.testCount).to.equal(8); 113 | }); 114 | }); 115 | 116 | function specificTests(specific) { 117 | return passable('specificTests', (test) => { 118 | test('First', 'should pass', () => true); 119 | test('Second', 'should pass', () => true); 120 | test('Third', 'should fail', () => false); 121 | test('Fourth', 'should fail', () => false); 122 | test('Fifth', 'should fail', () => false); 123 | test('Sixth', 'should pass', () => true); 124 | test('Sixth', 'should pass', () => true); // twice! 125 | test('Seventh', 'should pass', () => true); 126 | }, specific); 127 | }; 128 | 129 | }; 130 | 131 | runSpec(require('../')); 132 | runSpec(require('../../dist/passable')); 133 | runSpec(require('../../dist/passable.min.js')); 134 | -------------------------------------------------------------------------------- /src/core/Specific/spec.js: -------------------------------------------------------------------------------- 1 | import Specific from './index'; 2 | import { expect } from 'chai'; 3 | 4 | describe('Test Specific class constructor', () => { 5 | describe('Test default value fallback', () => { 6 | 7 | const defaultObject = {}; 8 | 9 | it('Should return default object when no args passed', () => { 10 | expect(new Specific()).to.deep.equal({}); 11 | }); 12 | 13 | it('Should return default object when specific is explicitly null', () => { 14 | expect(new Specific(null)).to.deep.equal(defaultObject); 15 | }); 16 | 17 | it('Should return default object when specific is an empty string', () => { 18 | expect(new Specific('')).to.deep.equal(defaultObject); 19 | }); 20 | 21 | it('Should return default object when specific is an empty array', () => { 22 | expect(new Specific([])).to.deep.equal(defaultObject); 23 | }); 24 | 25 | it('Should return default object when specific is of wrong type', () => { 26 | expect(() => new Specific(new Set())).to.throw(TypeError); 27 | expect(() => new Specific(new Map())).to.throw(TypeError); 28 | expect(() => new Specific(55)).to.throw(TypeError); 29 | expect(() => new Specific(true)).to.throw(TypeError); 30 | }); 31 | }); 32 | 33 | describe('Test legacy api', () => { 34 | it('Should store array values in `only`', () => { 35 | expect(new Specific(['field_1', 'field_2'])).to.deep.equal({ 36 | only: { 37 | field_1: true, 38 | field_2: true 39 | } 40 | }); 41 | }); 42 | 43 | it('Should store string value in `only`', () => { 44 | expect(new Specific('field_1')).to.deep.equal({ 45 | only: { 46 | field_1: true 47 | } 48 | }); 49 | }); 50 | }); 51 | 52 | describe('Test Object input', () => { 53 | it('Should add `only` array values to `only` object', () => { 54 | expect(new Specific({ 55 | only: ['f1', 'f2'] 56 | })).to.deep.equal({ 57 | only: { 58 | f1: true, 59 | f2: true 60 | } 61 | }); 62 | }); 63 | 64 | it('Should add `not` array values to `not` object', () => { 65 | expect(new Specific({ 66 | not: ['f1', 'f2'] 67 | })).to.deep.equal({ 68 | not: { 69 | f1: true, 70 | f2: true 71 | } 72 | }); 73 | }); 74 | 75 | it('Should add array values to correct object', () => { 76 | expect(new Specific({ 77 | only: ['f1', 'f2'], 78 | not: ['f3', 'f4'] 79 | })).to.deep.equal({ 80 | only: { 81 | f1: true, 82 | f2: true 83 | }, 84 | not: { 85 | f3: true, 86 | f4: true 87 | } 88 | }); 89 | }); 90 | 91 | it('Should add `only` string values to `only` object', () => { 92 | expect(new Specific({ 93 | only: 'f1' 94 | })).to.deep.equal({ 95 | only: { 96 | f1: true 97 | } 98 | }); 99 | }); 100 | 101 | it('Should add `not` string values to `not` object', () => { 102 | expect(new Specific({ 103 | not: 'f1' 104 | })).to.deep.equal({ 105 | not: { 106 | f1:true 107 | } 108 | }); 109 | }); 110 | 111 | it('Should add string values to correct object', () => { 112 | expect(new Specific({ 113 | only: 'f1', 114 | not: 'f3' 115 | })).to.deep.equal({ 116 | only: { 117 | f1: true 118 | }, 119 | not: { 120 | f3: true 121 | } 122 | }); 123 | }); 124 | }); 125 | }); 126 | 127 | describe('Test `is` (specific) function', () => { 128 | describe('Test truthy returns', () => { 129 | it('Should return true for an empty array', () => { 130 | expect(Specific.is([])).to.equal(true); 131 | }); 132 | 133 | it('Should return true for an empty string', () => { 134 | expect(Specific.is('')).to.equal(true); 135 | }); 136 | 137 | it('Should return true for an Object with `only`', () => { 138 | expect(Specific.is({only: ''})).to.equal(true); 139 | }); 140 | 141 | it('Should return true for an Object with `not`', () => { 142 | expect(Specific.is({not: ''})).to.equal(true); 143 | }); 144 | 145 | it('Should return true for an array of strings', () => { 146 | expect(Specific.is(['a', 'b', 'c'])).to.equal(true); 147 | }); 148 | 149 | it('Should return true for a string', () => { 150 | expect(Specific.is('a')).to.equal(true); 151 | }); 152 | }); 153 | 154 | describe('Test falsy returns', () => { 155 | it('Should return false for an array of mixed types', () => { 156 | expect(Specific.is([1, 'f2'])).to.equal(false); 157 | }); 158 | 159 | it('Should return false for a number', () => { 160 | expect(Specific.is(55)).to.equal(false); 161 | }); 162 | 163 | it('Should return false for a boolean', () => { 164 | expect(Specific.is(true)).to.equal(false); 165 | }); 166 | 167 | it('Should return false for an Object without `not` or `only`', () => { 168 | expect(Specific.is({})).to.equal(false); 169 | }); 170 | 171 | it('Should return false for null', () => { 172 | expect(Specific.is(null)).to.equal(false); 173 | }); 174 | 175 | it('Should return false for undefined', () => { 176 | expect(Specific.is(null)).to.equal(false); 177 | }); 178 | }); 179 | }); -------------------------------------------------------------------------------- /src/core/draft/spec.js: -------------------------------------------------------------------------------- 1 | import Mocha from 'mocha'; 2 | import faker from 'faker'; 3 | import { ERROR_NO_CONTEXT } from './constants'; 4 | 5 | const runSpec = (passable) => { 6 | let suite; 7 | const { WARN } = passable; 8 | 9 | const createSuite = (tests) => { 10 | suite = passable(faker.random.word(), tests); 11 | }; 12 | 13 | describe('Draft', () => { 14 | 15 | it('Should be `tests` second argument', (done) => { 16 | createSuite((test, draft) => { 17 | setTimeout(() => { 18 | expect(draft).to.deep.equal(suite); 19 | done(); 20 | }, 10); 21 | }); 22 | }); 23 | 24 | it('Should be exposed as a function from passable', () => { 25 | createSuite((test, draft) => { 26 | expect(passable.draft()).to.equal(draft); 27 | }); 28 | }); 29 | 30 | it('Should contain intermediate test result', () => { 31 | // This test is so long because it tests `draft` throughout 32 | // a suite's life cycle, both as an argument, and as an import 33 | createSuite((test, draft) => { 34 | expect(draft.testCount).to.equal(0); 35 | expect(passable.draft().testCount).to.equal(0); 36 | expect(draft.failCount).to.equal(0); 37 | expect(passable.draft().failCount).to.equal(0); 38 | expect(draft.warnCount).to.equal(0); 39 | expect(passable.draft().warnCount).to.equal(0); 40 | expect(draft.hasErrors()).to.equal(false); 41 | expect(passable.draft().hasErrors()).to.equal(false); 42 | expect(draft.hasWarnings()).to.equal(false); 43 | expect(passable.draft().hasWarnings()).to.equal(false); 44 | expect(draft.skipped).to.deep.equal([]); 45 | expect(passable.draft().skipped).to.deep.equal([]); 46 | 47 | expect(draft.hasErrors('field1')).to.equal(false); 48 | expect(passable.draft().hasErrors('field1')).to.equal(false); 49 | test('field1', 'message', () => expect(1).to.equal(2)); 50 | expect(draft.testCount).to.equal(1); 51 | expect(passable.draft().testCount).to.equal(1); 52 | expect(draft.failCount).to.equal(1); 53 | expect(passable.draft().failCount).to.equal(1); 54 | expect(draft.warnCount).to.equal(0); 55 | expect(passable.draft().warnCount).to.equal(0); 56 | expect(draft.hasErrors()).to.equal(true); 57 | expect(passable.draft().hasErrors()).to.equal(true); 58 | expect(draft.hasErrors('field1')).to.equal(true); 59 | expect(passable.draft().hasErrors('field1')).to.equal(true); 60 | expect(draft.hasWarnings()).to.equal(false); 61 | expect(passable.draft().hasWarnings()).to.equal(false); 62 | 63 | test('field2', 'message', () => expect(2).to.equal(2)); 64 | expect(draft.testCount).to.equal(2); 65 | expect(passable.draft().testCount).to.equal(2); 66 | expect(draft.failCount).to.equal(1); 67 | expect(passable.draft().failCount).to.equal(1); 68 | expect(draft.warnCount).to.equal(0); 69 | expect(passable.draft().warnCount).to.equal(0); 70 | expect(draft.hasErrors()).to.equal(true); 71 | expect(passable.draft().hasErrors()).to.equal(true); 72 | expect(draft.hasWarnings()).to.equal(false); 73 | expect(passable.draft().hasWarnings()).to.equal(false); 74 | 75 | expect(draft.hasWarnings('field3')).to.equal(false); 76 | expect(passable.draft().hasWarnings('field3')).to.equal(false); 77 | test('field3', 'message', () => expect(2).to.equal(1), WARN); 78 | expect(draft.testCount).to.equal(3); 79 | expect(passable.draft().testCount).to.equal(3); 80 | expect(draft.failCount).to.equal(1); 81 | expect(passable.draft().failCount).to.equal(1); 82 | expect(draft.warnCount).to.equal(1); 83 | expect(passable.draft().warnCount).to.equal(1); 84 | expect(draft.hasErrors()).to.equal(true); 85 | expect(passable.draft().hasErrors()).to.equal(true); 86 | expect(draft.hasWarnings()).to.equal(true); 87 | expect(passable.draft().hasWarnings()).to.equal(true); 88 | expect(draft.hasWarnings('field3')).to.equal(true); 89 | expect(passable.draft().hasWarnings('field3')).to.equal(true); 90 | 91 | test('field4', 'message', Promise.resolve(), WARN); 92 | expect(draft.testCount).to.equal(4); 93 | expect(passable.draft().testCount).to.equal(4); 94 | expect(draft.failCount).to.equal(1); 95 | expect(passable.draft().failCount).to.equal(1); 96 | expect(draft.warnCount).to.equal(1); 97 | expect(passable.draft().warnCount).to.equal(1); 98 | expect(draft.hasErrors()).to.equal(true); 99 | expect(passable.draft().hasErrors()).to.equal(true); 100 | expect(draft.hasWarnings()).to.equal(true); 101 | expect(passable.draft().hasWarnings()).to.equal(true); 102 | expect(draft.hasWarnings('field4')).to.equal(false); 103 | expect(passable.draft().hasWarnings('field4')).to.equal(false); 104 | }); 105 | }); 106 | }); 107 | 108 | describe('When called outside of a running suite', () => { 109 | let _uncaughtListeners; 110 | 111 | beforeEach(() => { 112 | _uncaughtListeners = process.listeners('uncaughtException'); 113 | process.removeAllListeners('uncaughtException'); 114 | }); 115 | 116 | afterEach(() => { 117 | _uncaughtListeners.forEach((listener) => process.on('uncaughtException', listener)); 118 | }); 119 | 120 | it('Should throw an error', (done) => { 121 | process.on('uncaughtException', (err) => { 122 | expect(err.message).to.have.string(ERROR_NO_CONTEXT); 123 | process.removeAllListeners('uncaughtException'); 124 | done(); 125 | }); 126 | passable.draft(); 127 | }); 128 | }); 129 | }; 130 | 131 | runSpec(require('../../')); 132 | runSpec(require('../../../dist/passable')); 133 | runSpec(require('../../../dist/passable.min.js')); 134 | -------------------------------------------------------------------------------- /src/core/passableResult/index.js: -------------------------------------------------------------------------------- 1 | import { WARN, FAIL } from '../../constants'; 2 | const severities = [ WARN, FAIL ]; 3 | 4 | const passableResult = (name) => { 5 | 6 | const completionCallbacks = []; 7 | let asyncObject = null; 8 | let hasValidationErrors = false; 9 | let hasValidationWarnings = false; 10 | let cancelled = false; 11 | 12 | /** 13 | * Initializes specific field's counters 14 | * @param {string} fieldName - The name of the field. 15 | */ 16 | const initFieldCounters = (fieldName) => { 17 | if (output.testsPerformed[fieldName]) { return output; } 18 | 19 | output.testsPerformed[fieldName] = { 20 | testCount: 0, 21 | failCount: 0, 22 | warnCount: 0 23 | }; 24 | }; 25 | 26 | /** 27 | * Bumps test counters to indicate tests that's being performed 28 | * @param {string} fieldName - The name of the field. 29 | */ 30 | const bumpTestCounter = (fieldName) => { 31 | if (!output.testsPerformed[fieldName]) { return output; } 32 | 33 | output.testsPerformed[fieldName].testCount++; 34 | output.testCount++; 35 | }; 36 | 37 | /** 38 | * Bumps field's warning counts and adds warning string 39 | * @param {string} fieldName - The name of the field. 40 | * @param {string} statement - The error string to add to the object. 41 | */ 42 | const bumpTestWarning = (fieldName, statement) => { 43 | hasValidationWarnings = true; 44 | output.warnings[fieldName] = output.warnings[fieldName] || []; 45 | output.warnings[fieldName].push(statement); 46 | output.warnCount++; 47 | output.testsPerformed[fieldName].warnCount++; 48 | }; 49 | 50 | /** 51 | * Bumps field's error counts and adds error string 52 | * @param {string} fieldName - The name of the field. 53 | * @param {string} statement - The error string to add to the object. 54 | */ 55 | const bumpTestError = (fieldName, statement) => { 56 | hasValidationErrors = true; 57 | output.errors[fieldName] = output.errors[fieldName] || []; 58 | output.errors[fieldName].push(statement); 59 | output.failCount++; 60 | output.testsPerformed[fieldName].failCount++; 61 | }; 62 | 63 | /** 64 | * Fails a field and updates output accordingly 65 | * @param {string} fieldName - The name of the field. 66 | * @param {string} statement - The error string to add to the object. 67 | * @param {string} severity - Whether it is a `fail` or `warn` test. 68 | */ 69 | const fail = (fieldName, statement, severity) => { 70 | if (!output.testsPerformed[fieldName]) { return output; } 71 | const selectedSeverity = severity && severities.includes(severity) ? severity : FAIL; 72 | selectedSeverity === WARN 73 | ? bumpTestWarning(fieldName, statement) 74 | : bumpTestError(fieldName, statement); 75 | }; 76 | 77 | /** 78 | * Uniquely add a field to the `skipped` list 79 | * @param {string} fieldName - The name of the field. 80 | */ 81 | const addToSkipped = (fieldName) => { 82 | !output.skipped.includes(fieldName) && output.skipped.push(fieldName); 83 | }; 84 | 85 | /** 86 | * Runs completion callbacks aggregated by `done` 87 | * regardless of success or failure 88 | */ 89 | const runCompletionCallbacks = () => { 90 | completionCallbacks.forEach((cb) => !cancelled && cb(output)); 91 | }; 92 | 93 | /** 94 | * Marks a field as async 95 | * @param {string} fieldName - The name of the field. 96 | */ 97 | const markAsync = (fieldName) => { 98 | asyncObject = asyncObject || {}; 99 | asyncObject[fieldName] = asyncObject[fieldName] || {}; 100 | asyncObject[fieldName] = { 101 | done: false, 102 | callbacks: asyncObject[fieldName].callbacks || [] 103 | }; 104 | }; 105 | 106 | /** 107 | * Marks an async field as done 108 | * @param {string} fieldName - The name of the field. 109 | */ 110 | const markAsDone = (fieldName) => { 111 | if (!fieldName) { 112 | return runCompletionCallbacks(); 113 | } 114 | 115 | if (asyncObject !== null && asyncObject[fieldName]) { 116 | asyncObject[fieldName].done = true; 117 | 118 | // run field callbacks set in `after` 119 | if (asyncObject[fieldName].callbacks) { 120 | asyncObject[fieldName].callbacks.forEach((callback) => !cancelled && callback(output)); 121 | } 122 | } 123 | }; 124 | 125 | /** 126 | * Registers callback functions to be run when test suite is done running 127 | * If current suite is not async, runs the callback immediately 128 | * @param {function} callback the function to be called on done 129 | * @return {object} output object 130 | */ 131 | const done = (callback) => { 132 | if (typeof callback !== 'function') {return output;} 133 | if (!asyncObject) { 134 | callback(output); 135 | } 136 | 137 | completionCallbacks.push(callback); 138 | 139 | return output; 140 | }; 141 | 142 | /** 143 | * Registers callback functions to be run when a certain field is done running 144 | * If field is not async, runs the callback immediately 145 | * @param {string} fieldName - The name of the field. 146 | * @param {function} callback the function to be called on done 147 | * @return {object} output object 148 | */ 149 | const after = (fieldName, callback) => { 150 | if (typeof callback !== 'function') { 151 | return output; 152 | } 153 | 154 | asyncObject = asyncObject || {}; 155 | if (!asyncObject[fieldName] && output.testsPerformed[fieldName]) { 156 | callback(output); 157 | } else if (asyncObject[fieldName]) { 158 | asyncObject[fieldName].callbacks = [...(asyncObject[fieldName].callbacks || []), callback]; 159 | } 160 | 161 | return output; 162 | }; 163 | 164 | /** 165 | * cancels done/after callbacks. They won't invoke when async operations complete 166 | */ 167 | const cancel = () => { 168 | cancelled = true; 169 | 170 | return output; 171 | }; 172 | 173 | /** 174 | * Gets all the errors of a field, or of the whole object 175 | * @param {string} fieldName - The name of the field. 176 | * @return {array | object} The field's errors, or all errors 177 | */ 178 | const getErrors = (fieldName) => { 179 | if (!fieldName) { 180 | return output.errors; 181 | } 182 | 183 | if (output.errors[fieldName]) { 184 | return output.errors[fieldName]; 185 | } 186 | 187 | return []; 188 | }; 189 | 190 | /** 191 | * Gets all the warnings of a field, or of the whole object 192 | * @param {string} [fieldName] - The name of the field. 193 | * @return {array | object} The field's warnings, or all warnings 194 | */ 195 | const getWarnings = (fieldName) => { 196 | if (!fieldName) { 197 | return output.warnings; 198 | } 199 | 200 | if (output.warnings[fieldName]) { 201 | return output.warnings[fieldName]; 202 | } 203 | 204 | return []; 205 | }; 206 | 207 | /** 208 | * Checks if a certain field (or the whole suite) has errors 209 | * @param {string} [fieldName] 210 | * @return {boolean} 211 | */ 212 | const hasErrors = (fieldName) => { 213 | if (!fieldName) { 214 | return hasValidationErrors; 215 | } 216 | 217 | return Boolean(output.getErrors(fieldName).length); 218 | }; 219 | 220 | /** 221 | * Checks if a certain field (or the whole suite) has warnings 222 | * @param {string} [fieldName] - The name of the field. 223 | * @return {boolean} 224 | */ 225 | const hasWarnings = (fieldName) => { 226 | if (!fieldName) { 227 | return hasValidationWarnings; 228 | } 229 | 230 | return Boolean(output.getWarnings(fieldName).length); 231 | }; 232 | 233 | const output = { 234 | name, 235 | failCount: 0, 236 | warnCount: 0, 237 | testCount: 0, 238 | testsPerformed: {}, 239 | errors: {}, 240 | warnings: {}, 241 | skipped: [] 242 | }; 243 | 244 | Object.defineProperties(output, { 245 | hasErrors: { 246 | value: hasErrors, 247 | writable: true, 248 | configurable: true, 249 | enumerable: false 250 | }, 251 | hasWarnings: { 252 | value: hasWarnings, 253 | writable: true, 254 | configurable: true, 255 | enumerable: false 256 | }, 257 | getErrors: { 258 | value: getErrors, 259 | writable: true, 260 | configurable: true, 261 | enumerable: false 262 | }, 263 | getWarnings: { 264 | value: getWarnings, 265 | writable: true, 266 | configurable: true, 267 | enumerable: false 268 | }, 269 | done: { 270 | value: done, 271 | writable: true, 272 | configurable: true, 273 | enumerable: false 274 | }, 275 | after: { 276 | value: after, 277 | writable: true, 278 | configurable: true, 279 | enumerable: false 280 | }, 281 | cancel: { 282 | value: cancel, 283 | writable: true, 284 | configurable: true, 285 | enumerable: false 286 | } 287 | }); 288 | 289 | return { 290 | initFieldCounters, 291 | bumpTestError, 292 | bumpTestWarning, 293 | bumpTestCounter, 294 | fail, 295 | addToSkipped, 296 | runCompletionCallbacks, 297 | markAsync, 298 | markAsDone, 299 | output 300 | }; 301 | }; 302 | 303 | export default passableResult; 304 | -------------------------------------------------------------------------------- /src/core/test/spec.js: -------------------------------------------------------------------------------- 1 | import passable from '../passable'; 2 | import test from '.'; 3 | import { expect } from 'chai'; 4 | import { FAIL, WARN } from '../../index'; 5 | import { clone, noop, random } from 'lodash'; 6 | import { lorem } from 'faker'; 7 | import sinon from 'sinon'; 8 | 9 | describe('Test Passables "test" function', () => { 10 | let output; 11 | 12 | describe('Supplied test callback is a function', () => { 13 | const allTests = []; 14 | 15 | beforeEach(() => { 16 | passable(lorem.word(), noop); 17 | allTests.length = 0; 18 | 19 | for (let i = 0; i < random(1, 15); i++) { 20 | allTests.push(() => null); 21 | } 22 | }); 23 | }); 24 | 25 | describe('Supplied test callback is a promise', () => { 26 | const allTests = []; 27 | 28 | beforeEach(() => { 29 | passable(lorem.word(), noop); 30 | allTests.length = 0; 31 | 32 | for (let i = 0; i < random(1, 15); i++) { 33 | allTests.push(Promise.resolve()); 34 | } 35 | }); 36 | }); 37 | 38 | describe('Supplied test callback is not supported', () => { 39 | 40 | beforeEach(() => { 41 | passable(lorem.word(), noop); 42 | }); 43 | 44 | 45 | [0, 1, [], [55], {}, false, true, null, undefined].forEach((testCb) => { 46 | it(`Should return without adding pending test for ${testCb}`, () => { 47 | output = passable(lorem.word(), () => { 48 | test(lorem.word(), lorem.sentence(), testCb); 49 | }); 50 | expect(output.testCount).to.equal(0); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('Running test callback', () => { 56 | let allTests; 57 | 58 | beforeEach(() => { 59 | allTests = []; 60 | for (let i = 0; i < random(1, 15); i++) { 61 | allTests.push(sinon.spy()); 62 | } 63 | passable(lorem.word(), () => allTests.forEach((t) => { 64 | test(lorem.word(), lorem.sentence(), t); 65 | })); 66 | }); 67 | 68 | it('Should call all test callbacks', () => { 69 | allTests.forEach((fn) => { 70 | expect(fn.calledOnce).to.equal(true); 71 | }); 72 | }); 73 | 74 | it('Should bump test counters for each of the tests', () => { 75 | const count = random(1, 15); 76 | 77 | output = passable(lorem.word(), () => { 78 | for (let i = 0; i < count; i++) { 79 | test(lorem.word(), lorem.sentence(), noop); 80 | } 81 | }); 82 | 83 | expect(output.testCount).to.equal(count); 84 | }); 85 | 86 | describe('Test is async', () => { 87 | 88 | describe('When returning promise to test callback', () => { 89 | let f1, f2, f3, f4, f5, f6, output; 90 | const rejectionMessage = lorem.sentence(); 91 | 92 | beforeEach(() => { 93 | f1 = lorem.word(); 94 | f2 = lorem.word(); 95 | f3 = lorem.word(); 96 | f4 = lorem.word(); 97 | f5 = lorem.word(); 98 | f6 = lorem.word(); 99 | 100 | const rejectLater = () => new Promise((res, rej) => { 101 | setTimeout(rej, 500); 102 | }); 103 | 104 | output = passable(lorem.word(), (test) => { 105 | test(f1, f1, () => Promise.reject()); 106 | test(f2, f2, () => new Promise((resolve, reject) => { 107 | setTimeout(reject, 200); 108 | })); 109 | test(f3, () => Promise.reject(lorem.word())); 110 | test(f3, () => Promise.reject(rejectionMessage)); 111 | test(f4, f4, () => Promise.reject(rejectionMessage)); 112 | test(f5, f5, () => new Promise((resolve) => { 113 | setTimeout(resolve, 100); 114 | })); 115 | test(f6, f6, async() => await rejectLater()); 116 | }); 117 | }); 118 | 119 | it('Should fail for rejected promise', (done) => { 120 | [f1, f2, f3, f4, f6].forEach((field) => 121 | expect(output.hasErrors(field)).to.equal(false) 122 | ); 123 | 124 | setTimeout(() => { 125 | [f1, f2, f3, f4, f6].forEach((field) => 126 | expect(output.hasErrors(field)).to.equal(true) 127 | ); 128 | 129 | [f1, f2, f6].forEach((field) => 130 | expect(output.getErrors(field)).to.include(field) 131 | ); 132 | 133 | done(); 134 | }, 550); 135 | }); 136 | 137 | it('Should fail with rejection message when provided', (done) => { 138 | setTimeout(() => { 139 | [f3, f4].forEach((field) => { 140 | expect(output.getErrors(field)).to.include(rejectionMessage); 141 | }); 142 | 143 | done(); 144 | }, 550); 145 | }); 146 | 147 | it('Should pass for fulfilled promises', (done) => { 148 | expect(output.hasErrors(f5)).to.equal(false); 149 | 150 | setTimeout(() => { 151 | expect(output.hasErrors(f5)).to.equal(false); 152 | done(); 153 | }, 500); 154 | }); 155 | 156 | }); 157 | 158 | describe('When passing a Promise as a test', () => { 159 | describe('failing', () => { 160 | let f1, f2, f3, f4; 161 | const rejectionMessage = lorem.sentence(); 162 | 163 | beforeEach(() => { 164 | f1 = lorem.word(); 165 | f2 = lorem.word(); 166 | f3 = lorem.word(); 167 | f4 = lorem.word(); 168 | 169 | output = passable(lorem.word(), (test) => { 170 | test(f1, f1, new Promise((_, reject) => setImmediate(reject))); 171 | test(f2, new Promise((_, reject) => setImmediate(() => reject(rejectionMessage)))); 172 | test(f3, lorem.sentence(), new Promise((_, reject) => setImmediate(() => reject(lorem.word())))); 173 | test(f3, f3, new Promise((_, reject) => setImmediate(() => reject(rejectionMessage)))); 174 | test(f4, f4, new Promise((resolve) => setTimeout(resolve))); 175 | test(f4, f4, new Promise((resolve) => setTimeout(resolve, 500))); 176 | test(lorem.word(), lorem.sentence(), noop); 177 | }); 178 | }); 179 | 180 | it('Should immediately register tests', () => { 181 | expect(output.testCount).to.equal(7); 182 | }); 183 | 184 | it('Should run async test promise', (done) => { 185 | passable(lorem.word(), (test) => { 186 | test(f1, lorem.sentence(), new Promise(() => done())); 187 | test(lorem.word(), lorem.sentence(), noop); 188 | }); 189 | }); 190 | 191 | it('Should fail with rejection message when provided', (done) => { 192 | setTimeout(() => { 193 | [f2, f3].forEach((field) => { 194 | expect(output.getErrors(field)).to.include(rejectionMessage); 195 | }); 196 | 197 | done(); 198 | }, 550); 199 | }); 200 | 201 | it('Should only mark test as failing after rejection', (done) => { 202 | expect(output.failCount).to.equal(0); 203 | 204 | setTimeout(() => { 205 | expect(output.failCount).to.equal(4); 206 | done(); 207 | }, 10); 208 | }); 209 | }); 210 | 211 | describe('passing', () => { 212 | beforeEach(() => { 213 | output = passable(lorem.word(), (test) => { 214 | test(lorem.word(), lorem.sentence(), new Promise((resolve, reject) => setImmediate(resolve))); 215 | }); 216 | }); 217 | 218 | it('Should keep test unchanged after resolution', (done) => { 219 | const res = clone(output); 220 | setTimeout(() => { 221 | expect(output).to.deep.equal(res); 222 | done(); 223 | }, 10); 224 | }); 225 | }); 226 | }); 227 | }); 228 | 229 | describe('sync test behavior', () => { 230 | it('should mark a test as failed for a thrown error', () => { 231 | const name = lorem.word(); 232 | output = passable(lorem.word(), (test) => { 233 | test(name, lorem.sentence(), () => { throw new Error(); }); 234 | test(lorem.word(), lorem.sentence(), noop); 235 | }); 236 | expect(output.failCount).to.equal(1); 237 | expect(output.hasErrors(name)).to.equal(true); 238 | }); 239 | 240 | it('should mark a test as failed for explicit `false`', () => { 241 | const name = lorem.word(); 242 | output = passable(lorem.word(), (test) => { 243 | test(name, lorem.sentence(), () => false); 244 | test(lorem.word(), lorem.sentence(), noop); 245 | }); 246 | expect(output.failCount).to.equal(1); 247 | expect(output.hasErrors(name)).to.equal(true); 248 | }); 249 | 250 | it('should implicitly pass test', () => { 251 | const name = lorem.word(); 252 | output = passable(lorem.word(), (test) => { 253 | test(name, lorem.sentence(), noop); 254 | }); 255 | expect(output.failCount).to.equal(0); 256 | expect(output.testCount).to.equal(1); 257 | }); 258 | }); 259 | }); 260 | }); 261 | -------------------------------------------------------------------------------- /dist/passable.min.js: -------------------------------------------------------------------------------- 1 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).passable=n()}(this,(function(){"use strict";function t(n){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(n)}function n(t,n){for(var e=0;et.length)&&(n=t.length);for(var e=0,r=new Array(n);eNumber(n)}function v(t,n){return b(t)&&b(n)&&Number(t)>=Number(n)}function w(t,n){return b(t)&&b(n)&&Number(t)n},longerThanOrEquals:function(t,n){return t.length>=n},shorterThan:function(t,n){return t.length2?r-2:0),i=2;i0&&void 0!==arguments[0]?arguments[0]:{},n=i({},P,{},t);if(s())return function(t){var e=new Proxy(n,{get:function(n,r){if(u(n,r))return function(){for(var o=arguments.length,i=new Array(o),u=0;u1?e-1:0),o=1;o PassableNS.IEnforceInstance; 5 | const Enforce: PassableNS.IEnforceConstructor; 6 | const validate: PassableNS.IValidate; 7 | const WARN: PassableNS.IWARN; 8 | const FAIL: PassableNS.IFAIL; 9 | const VERSION: PassableNS.IVERSION; 10 | 11 | export default passable; 12 | export { enforce, Enforce, validate, WARN, FAIL, VERSION }; 13 | 14 | interface Passable { 15 | (name: string, testFn: (test: (name: string, errorMessage: string, callback: PassableNS.IFunctionOrPromise) => void, 16 | draft: PassableNS.IValidationResult) => void, 17 | specific?: string | string[] | {only?: string | string[], not?: string | string[]}): 18 | PassableNS.IValidationResult, 19 | enforce(value): PassableNS.IEnforceInstance; 20 | test(name: string, errorMessage: string, callback: PassableNS.IFunctionOrPromise): void; 21 | draft(): PassableNS.IEnforceInstance; 22 | Enforce: PassableNS.IEnforceConstructor; 23 | any: PassableNS.IAny; 24 | validate: PassableNS.IValidate; 25 | VERSION: PassableNS.IVERSION; 26 | WARN: PassableNS.IWARN; 27 | FAIL: PassableNS.IFAIL; 28 | } 29 | 30 | namespace PassableNS { 31 | 32 | export interface IValidationResult { 33 | /** 34 | * The name of the form being validated 35 | */ 36 | name: string; 37 | /** 38 | * Overall errors count in current validation suite 39 | */ 40 | failCount: number; 41 | /** 42 | * All skipped fields in suite (empty, unless the specific option is used) 43 | */ 44 | skipped: string[]; 45 | /** 46 | * Overall warnings count in current validation suite 47 | */ 48 | testCount: number; 49 | /** 50 | * Detailed stats per field (structure detailed below) 51 | */ 52 | testsPerformed: { 53 | [fieldName: string]: { 54 | /** 55 | * Overall test count in this field 56 | */ 57 | testCount: number; 58 | /** 59 | * Overall errors count for this field 60 | */ 61 | failCount: number; 62 | /** 63 | * Overall warnings count for this field 64 | */ 65 | warnCount: number; 66 | } 67 | }; 68 | /** 69 | * Actual errors per each field 70 | */ 71 | errors: { 72 | [fieldName: string]: string[]; 73 | }; 74 | /** 75 | * Actual errors per each field 76 | */ 77 | warnings: { 78 | [fieldName: string]: string[]; 79 | }; 80 | /** 81 | * Overall warnings count for this form 82 | */ 83 | warnCount: number; 84 | /** 85 | * Getter function which allows accessing the errors array of a certain field (or the whole suite if not supplied) 86 | */ 87 | getErrors: (field?: string) => any[]; 88 | /** 89 | * Getter function which allows accessing the warnings array of a certain field (or the whole suite if not supplied) 90 | */ 91 | getWarnings: (field?: string) => any[]; 92 | /** 93 | * Returns whether a certain field (or the whole suite if not supplied) has errors 94 | */ 95 | hasErrors: (field?: string) => boolean; 96 | /** 97 | * Returns whether a certain field (or the whole suite if not supplied) has warnings 98 | */ 99 | hasWarnings: (field?: string) => boolean; 100 | 101 | /** 102 | * Registers a completion callback for the validation suite 103 | */ 104 | done: (callback: (res: PassableNS.IValidationResult) => void) => PassableNS.IValidationResult; 105 | 106 | /** 107 | * Registers a completion callback for a specific field 108 | */ 109 | after: (fieldName: string, callback: (res: PassableNS.IValidationResult) => void) => PassableNS.IValidationResult; 110 | 111 | /** 112 | * Cancels Async tests callbacks (after/done) 113 | */ 114 | cancel: () => PassableNS.IValidationResult; 115 | } 116 | 117 | export type IFunctionOrPromise = () => void | Promise; 118 | 119 | export type IVERSION = string; 120 | export type IWARN = 'warn'; 121 | export type IFAIL = 'fail'; 122 | 123 | export interface IValidate { 124 | (): boolean; 125 | } 126 | export interface IAny { 127 | (): boolean; 128 | } 129 | 130 | type IEnforceChain = { 131 | [K in keyof T]: IEnforceInstance 132 | }; 133 | 134 | export type IEnforceConstructor = { 135 | new(): (value) => IEnforceInstance; 136 | new boolean }> 137 | (arg: T): (value) => IEnforceInstance>; 138 | }; 139 | 140 | export interface IEnforceInstance { 141 | /** 142 | * Checks if a value contains a regex match by Regex expression 143 | * 144 | * @example 145 | * enforce(1984).matches(/[0-9]/) // truthy 146 | * 147 | * enforce('nineteen eighty four').matches(/[0-9]/) // falsy 148 | */ 149 | matches(regex: RegExp): IEnforceInstance & T; 150 | 151 | /** 152 | * Checks if a value contains a regex match by a string expression 153 | * 154 | * @example 155 | * enforce(1984).matches('[0-9]') //truthy 156 | * 157 | * enforce('nineteen eighty four').matches('[0-9]') // falsy 158 | */ 159 | matches(regexAsString: string): IEnforceInstance & T; 160 | 161 | /** 162 | * Checks if a value doesn't contains a regex match by Regex expression 163 | * 164 | * @example 165 | * enforce(1984).notMatches(/[0-9]/) // falsy 166 | */ 167 | notMatches(regex: RegExp): IEnforceInstance & T; 168 | 169 | /** 170 | * Checks if a value doesn't contains a regex match by string expression 171 | * 172 | * @example 173 | * enforce('nineteen eighty four').notMatches('[0-9]') // truthy 174 | */ 175 | notMatches(regexAsString: string): IEnforceInstance & T; 176 | 177 | /** 178 | * Checks if your enforce value is contained in another array 179 | * 180 | * @example 181 | * enforce('hello').inside(['hello', 'world']) // truthy 182 | * 183 | * enforce(3).inside([1, 2]) // falsy 184 | * 185 | * enforce(false).inside([true, false]) // truthy 186 | */ 187 | inside(array: number[] | string[] | boolean[]): IEnforceInstance & T; 188 | /** 189 | * Checks if your enforce value is contained in another string 190 | * 191 | * @example 192 | * enforce('da').inside('tru dat.') // truthy 193 | * 194 | * enforce('ad').inside('tru dat.') // falsy 195 | */ 196 | inside(text: string): IEnforceInstance & T; 197 | 198 | /** 199 | * Checks if your enforce value is not contained in another array 200 | * 201 | * @example 202 | * enforce('hello').notInside(['hello', 'world']) // falsy 203 | */ 204 | notInside(array: number[] | string[] | boolean[]): IEnforceInstance & T; 205 | /** 206 | * Checks if your enforce value is not contained in another string 207 | * 208 | * @example 209 | * enforce('ad').notInside('tru dat.') // truthy 210 | */ 211 | notInside(text: string): IEnforceInstance & T; 212 | 213 | /** 214 | * Checks if a value is of type Array 215 | * 216 | * @example 217 | * enforce(['hello']).isArray() // truthy 218 | * 219 | * enforce('hello').isArray() // falsy 220 | */ 221 | isArray(): IEnforceInstance & T; 222 | 223 | /** 224 | * Checks if a value is of any type other than array 225 | * 226 | * @example 227 | * enforce(['hello']).isNotArray() // falsy 228 | * 229 | * enforce('hello').isNotArray() // truthy 230 | */ 231 | isNotArray(): IEnforceInstance & T; 232 | 233 | /** 234 | * Checks if a value is of type String 235 | * 236 | * @example 237 | * enforce('hello').isString() // truthy 238 | * 239 | * enforce(['hello']).isString() // falsy 240 | */ 241 | isString(): IEnforceInstance & T; 242 | 243 | /** 244 | * Checks if a value is of any type other than string 245 | * 246 | * @example 247 | * enforce('hello').isNotString() // falsy 248 | * 249 | * enforce(['hello']).isNotString() // truthy 250 | */ 251 | isNotString(): IEnforceInstance & T; 252 | 253 | /** 254 | * Checks if a value is of type number 255 | * 256 | * @example 257 | * enforce(143).isNumber() // truthy 258 | * 259 | * enforce(NaN).isNumber() // truthy! (NaN is of type 'number!') 260 | */ 261 | isNumber(): IEnforceInstance & T; 262 | 263 | /** 264 | * Checks if a value is of any type other than number 265 | * 266 | * @example 267 | * enforce(143).isNotNumber() // falsy 268 | * 269 | * enforce('143').isNotNumber() // truthy 270 | */ 271 | isNotNumber(): IEnforceInstance & T; 272 | 273 | /** 274 | * Checks if your enforce value is empty, false, zero, null or undefined 275 | * 276 | * @example 277 | * enforce([]).isEmpty() // truthy 278 | * 279 | * enforce('').isEmpty() // truthy 280 | * 281 | * enforce({}).isEmpty() // truthy 282 | */ 283 | isEmpty(): IEnforceInstance & T; 284 | 285 | /** 286 | * Checks that your enforce value is not empty, false, or zero 287 | * 288 | * @example 289 | * 290 | * enforce([1]).isNotEmpty() // truthy 291 | * 292 | * enforce({1:1}).isNotEmpty() // truthy 293 | * 294 | * enforce([]).isNotEmpty() // falsy 295 | */ 296 | isNotEmpty(): IEnforceInstance & T; 297 | 298 | /** 299 | * Checks that your enforce value is a numeric value 300 | * 301 | * @example 302 | * 303 | * enforce('-0x42').isNumeric() // falsy 304 | * 305 | * enforce('0xFF').isNumeric() // truthy 306 | */ 307 | isNumeric(): IEnforceInstance & T; 308 | 309 | /** 310 | * Checks that your enforce value is not a numeric value 311 | * 312 | * @example 313 | * 314 | * enforce('7.2acdgs').isNotNumeric() // truthy 315 | * 316 | * enforce('-10').isNotNumeric() // falsy 317 | */ 318 | isNotNumeric(): IEnforceInstance & T; 319 | 320 | /** 321 | * Checks that your numeric enforce value is smaller than another value 322 | * 323 | * @example 324 | * 325 | * enforce(0).lessThan(1) // truthy 326 | * 327 | * enforce('1').lessThan(0) // falsy 328 | */ 329 | lessThan(size: number): IEnforceInstance & T; 330 | 331 | /** 332 | * Checks that your numeric enforce value is smaller than another value 333 | * 334 | * @example 335 | * 336 | * enforce(0).lt(1) // truthy 337 | * 338 | * enforce('1').lt(0) // falsy 339 | */ 340 | 341 | lt(size: number): IEnforceInstance & T; 342 | /** 343 | * Checks that your numeric enforce value is smaller than or equals another value 344 | * 345 | * @example 346 | * 347 | * enforce(0).lessThanOrEquals(1) // truthy 348 | * 349 | * enforce('1').lessThanOrEquals(1) // truthy 350 | * 351 | * enforce(2).lessThanOrEquals(1) // falsy 352 | */ 353 | lessThanOrEquals(size: number): IEnforceInstance & T; 354 | 355 | /** 356 | * Checks that your numeric enforce value is smaller than or equals another value 357 | * 358 | * @example 359 | * 360 | * enforce(0).lte(1) // truthy 361 | * 362 | * enforce('1').lte(1) // truthy 363 | * 364 | * enforce(2).lte('1') // falsy 365 | */ 366 | lte(size: number): IEnforceInstance & T; 367 | 368 | /** 369 | * Checks that your numeric enforce value is greater than another value 370 | * 371 | * @example 372 | * 373 | * enforce(1).greaterThan(0) // truthy 374 | * 375 | * enforce('0').greaterThan('1') // falsy 376 | */ 377 | greaterThan(size: number): IEnforceInstance & T; 378 | 379 | /** 380 | * Checks that your numeric enforce value is greater than another value 381 | * 382 | * @example 383 | * 384 | * enforce(1).gt(0) // truthy 385 | * 386 | * enforce(0).gt('1') // falsy 387 | */ 388 | 389 | gt(size: number): IEnforceInstance & T; 390 | /** 391 | * Checks that your numeric enforce value is greater than or equals another value 392 | * 393 | * @example 394 | * 395 | * enforce(1).greaterThanOrEquals(1) // truthy 396 | * 397 | * enforce('1').greaterThanOrEquals(0) // truthy 398 | * 399 | * enforce('2').greaterThanOrEquals(3) // falsy 400 | */ 401 | greaterThanOrEquals(size: number): IEnforceInstance & T; 402 | 403 | /** 404 | * Checks that your numeric enforce value is greater than or equals another value 405 | * 406 | * @example 407 | * 408 | * enforce(1).gte(1) // truthy 409 | * 410 | * enforce('1').gte(0) // truthy 411 | * 412 | * enforce(2).gte('3') // falsy 413 | */ 414 | gte(size: number): IEnforceInstance & T; 415 | 416 | /** 417 | * Checks that your enforce value equals a given number 418 | * 419 | * @example 420 | * 421 | * enforce('1').numberEquals(1) // truthy 422 | * 423 | * enforce(2).numberEquals(2) // truthy 424 | * 425 | * enforce('2').numberEquals(0) // falsy 426 | */ 427 | numberEquals(size: number): IEnforceInstance & T; 428 | 429 | /** 430 | * Checks that your enforce value does not equal a given number 431 | * 432 | * @example 433 | * 434 | * enforce(3).numberNotEquals(1) // truthy 435 | * 436 | * enforce('3').numberNotEquals(3) // falsy 437 | */ 438 | numberNotEquals(size: number): IEnforceInstance & T; 439 | 440 | /** 441 | * Checks that your enforce value is longer than a given number 442 | * 443 | * @example 444 | * 445 | * enforce(['one']).longerThan(0) // truthy 446 | * 447 | * enforce('').longerThan(0) // falsy 448 | */ 449 | longerThan(size: number): IEnforceInstance & T; 450 | 451 | /** 452 | * Checks that your enforce value is longer than or equals another value 453 | * 454 | * @example 455 | * 456 | * enforce([1]).longerThanOrEquals(0) // truthy 457 | * 458 | * enforce('').longerThanOrEquals(1) // falsy 459 | */ 460 | longerThanOrEquals(size: number): IEnforceInstance & T; 461 | 462 | /** 463 | * Checks that your enforce value is shorter than a given number 464 | * 465 | * @example 466 | * 467 | * enforce([]).shorterThan(1) // truthy 468 | * 469 | * enforce('0').shorterThan(0) // falsy 470 | */ 471 | shorterThan(size: number): IEnforceInstance & T; 472 | 473 | /** 474 | * Checks that your enforce value is shorter than or equals another value 475 | * 476 | * @example 477 | * 478 | * enforce([]).shorterThanOrEquals(1) // truthy 479 | * 480 | * enforce('0').shorterThanOrEquals(0) // falsy 481 | */ 482 | shorterThanOrEquals(size: number): IEnforceInstance & T; 483 | 484 | /** 485 | * Checks that your enforce value equals a given number 486 | * 487 | * @example 488 | * 489 | * enforce([1]).lengthEquals(1) // truthy 490 | * 491 | * enforce('0').lengthEquals(0) // falsy 492 | */ 493 | lengthEquals(size: number): IEnforceInstance & T; 494 | 495 | /** 496 | * Checks that your enforce value does not equal a given number 497 | * 498 | * @example 499 | * 500 | * enforce([]).lengthNotEquals(1) // truthy 501 | * 502 | * enforce('').lengthNotEquals(0) // falsy 503 | */ 504 | lengthNotEquals(size: number): IEnforceInstance & T; 505 | 506 | /** 507 | * Checks that your enforce value strictly equals (===) a given value 508 | * 509 | * @example 510 | * 511 | * enforce(1).equals(1) // truthy 512 | * 513 | * enforce('hello').equals('hello') // truthy 514 | * 515 | * enforce('1').equals(1) // falsy 516 | * 517 | * enforce([1]).equals([1]) // falsy 518 | */ 519 | equals(value: any): IEnforceInstance & T; 520 | 521 | /** 522 | * Checks that your enforce value doesn't strictly equal (===) a given value 523 | * 524 | * @example 525 | * 526 | * enforce('1').notEquals(1) // truthy 527 | * 528 | * enforce([1]).notEquals([1]) // truthy 529 | * 530 | * enforce(1).notEquals(1) // falsy 531 | * 532 | * enforce('hello').notEquals('hello') // falsy 533 | */ 534 | notEquals(value: any): IEnforceInstance & T; 535 | } 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /docs/enforce.md: -------------------------------------------------------------------------------- 1 | # Enforce 2 | For assertions, Passable is bundled with [Enforce](npmjs.com/package/n4s). Enforce is a validation assertions library. It allows you to run your data against rules and conditions and test whether it passes your validations. It is intended for validation logic that gets repeated over and over again and should not be written manually. It comes with a wide-variety of pre-built rules, but it can also be extended to support your own repeated custom logic. 3 | 4 | The way Enforce operates is similar to most common assertion libraries. You pass it a value, and one or more rules to test your value against - if the validation fails, it throws an Error, otherwise - it will move on to the next rule rule in the chain. 5 | 6 | ```js 7 | import { passable } from 'passable' 8 | 9 | enforce(4) 10 | .isNumber(); 11 | // passes 12 | 13 | enforce(4) 14 | .isNumber() 15 | .greaterThan(2); 16 | // passes 17 | 18 | enforce(4) 19 | .lessThan(2) // throws an error, will not carry on to the next rule 20 | .greaterThan(3); 21 | ``` 22 | 23 | ## Content 24 | - [List of Enforce rules](#list-of-enforce-rules) 25 | - [Custom Enforce Rules](#custom-enforce-rules) 26 | 27 | Enforce exposes all predefined and custom rules. You may use chaining to make multiple enfocements for the same value. 28 | 29 | # List of Enforce rules 30 | Enforce rules are functions that allow you to test your data against different criteria. The following rules are supported out-of-the-box. 31 | 32 | - [equals](#equals) 33 | - [notEquals](#notequals) 34 | - [isEmpty](#isempty) 35 | - [isNotEmpty](#isnotempty) 36 | - [isNumeric](#isnumeric) 37 | - [isNotNumeric](#isnotnumeric) 38 | - [greaterThan](#greaterthan) 39 | - [greaterThanOrEquals](#greaterthanorequals) 40 | - [lengthEquals](#lengthequals) 41 | - [lengthNotEquals](#lengthnotequals) 42 | - [lessThan](#lessthan) 43 | - [lessThanOrEquals](#lessthanorequals) 44 | - [longerThan](#longerthan) 45 | - [longerThanOrEquals](#longerthanorequals) 46 | - [numberEquals](#numberequals) 47 | - [numberNotEquals](#numbernotequals) 48 | - [shorterThan](#shorterthan) 49 | - [shorterThanOrEquals](#shorterthanorequals) 50 | - [matches](#matches) 51 | - [notMatches](#notmatches) 52 | - [inside](#inside) 53 | - [notInside](#notinside) 54 | - [isTruthy](#istruthy) 55 | - [isFalsy](#isfalsy) 56 | - [isArray](#isarray) 57 | - [isNotArray](#isnotarray) 58 | - [isNumber](#isnumber) 59 | - [isNotNumber](#isnotnumber) 60 | - [isString](#isstring) 61 | - [isNotString](#isnotstring) 62 | - [isOdd](#isodd) 63 | - [isEven](#iseven) 64 | 65 | ### equals 66 | ### Description 67 | Checks if your enforced value strictly equals (`===`) another. 68 | 69 | It is not recommended to use this rule to compare arrays or objects, as it does not perform any sort of deep comparison on the value. 70 | 71 | For numeric value comparison, you should use `numberEquals`, which coerces numeric strings into numbers before comparing. 72 | 73 | ### Arguments 74 | * `value`: Any value you wish to check your enforced value against 75 | 76 | ### Usage examples: 77 | 78 | ```js 79 | enforce(1).equals(1); 80 | 81 | enforce('hello').equals('hello'); 82 | 83 | const a = [1, 2, 3]; 84 | 85 | enforce(a).equals(a); 86 | // passes 87 | ``` 88 | 89 | ```js 90 | enforce('1').equals(1); 91 | enforce([1, 2, 3]).equals([1, 2, 3]); 92 | // throws 93 | ``` 94 | 95 | 96 | ## notEquals 97 | ### Description 98 | Checks if your enforced value does not strictly equal (`===`) another. 99 | 100 | Reverse implementation of `equals`. 101 | 102 | ### Usage examples: 103 | 104 | ```js 105 | enforce('1').notEquals(1); 106 | enforce([1, 2, 3]).notEquals([1, 2, 3]); 107 | // passes 108 | ``` 109 | 110 | ```js 111 | enforce(1).notEquals(1); 112 | enforce('hello').notEquals('hello'); 113 | 114 | const a = [1, 2, 3]; 115 | 116 | enforce(a).notEquals(a); 117 | // throws 118 | ``` 119 | 120 | 121 | ## isEmpty 122 | ### Description 123 | Checks if your enforced value is empty, false, zero, null or undefined. 124 | 125 | Expected results are: 126 | * object: checks against count of keys (`0` is empty) 127 | * array/string: checks against length. (`0` is empty) 128 | * number: checks the value of the number. (`0` and `NaN` are empty) 129 | * boolean: `false` is empty. 130 | * undefined/null: are both empty. 131 | 132 | ### Usage examples: 133 | 134 | ```js 135 | enforce([]).isEmpty(); 136 | enforce('').isEmpty(); 137 | enforce({}).isEmpty(); 138 | enforce(0).isEmpty(); 139 | enforce(NaN).isEmpty(); 140 | enforce(undefined).isEmpty(); 141 | enforce(null).isEmpty(); 142 | enforce(false).isEmpty(); 143 | // passes 144 | ``` 145 | 146 | ```js 147 | enforce([1]).isEmpty(); 148 | enforce('1').isEmpty(); 149 | enforce({1:1}).isEmpty(); 150 | enforce(1).isEmpty(); 151 | enforce(true).isEmpty(); 152 | // throws 153 | ``` 154 | 155 | 156 | ## isNotEmpty 157 | ### Description 158 | Checks that your enforced value is not empty, false, or zero. 159 | Reverse implementation of `isEmpty`. 160 | 161 | ### Usage examples: 162 | 163 | ```js 164 | enforce([1]).isNotEmpty(); 165 | enforce('1').isNotEmpty(); 166 | enforce({1:1}).isNotEmpty(); 167 | // passes 168 | ``` 169 | 170 | ```js 171 | enforce([]).isNotEmpty(); 172 | enforce('').isNotEmpty(); 173 | enforce({}).isNotEmpty(); 174 | enforce(0).isNotEmpty(); 175 | // throws 176 | ``` 177 | 178 | 179 | ## isNumeric 180 | ### Description 181 | Checks if a value is a representation of a real number 182 | 183 | ### Usage examples: 184 | 185 | ```js 186 | enforce(143).isNumeric(); 187 | enforce('143').isNumeric(); 188 | // passes 189 | ``` 190 | 191 | ```js 192 | enforce(NaN).isNumeric(); 193 | enforce('1hello').isNumeric(); 194 | enforce('hi').isNumeric(); 195 | // throws 196 | ``` 197 | 198 | 199 | ## isNotNumeric 200 | ### Description 201 | Checks if a value is not a representation of a real number. 202 | Reverse implementation of `isNumeric`. 203 | 204 | ### Usage examples: 205 | 206 | ```js 207 | enforce(NaN).isNotNumeric(); 208 | enforce('Hello World!').isNotNumeric(); 209 | // passes 210 | ``` 211 | 212 | ```js 213 | enforce(731).isNotNumeric(); 214 | enforce('42').isNotNumeric(); 215 | // throws 216 | ``` 217 | 218 | 219 | ## greaterThan 220 | 221 | - alias: `gt` 222 | 223 | ### Description 224 | Checks that your numeric enforced value is larger than a given numeric value. 225 | 226 | ### Arguments 227 | * `value`: `number | string` | A numeric value against which you want to check your enforced value. 228 | 229 | Strings are parsed using `Number()`, values which are non fully numeric always return false; 230 | 231 | ### Usage 232 | 233 | ```js 234 | enforce(1).greaterThan(0); 235 | enforce('10').greaterThan(0); 236 | enforce(900).gt('100'); 237 | // passes 238 | ``` 239 | 240 | ```js 241 | enforce(100).greaterThan(100); 242 | enforce('100').greaterThan(110); 243 | enforce([100]).gt(1); 244 | // throws 245 | ``` 246 | 247 | 248 | ## greaterThanOrEquals 249 | - alias: `gte()` 250 | 251 | ### Description 252 | Checks that your numeric enforced value is larger than or equals to a given numeric value. 253 | 254 | ### Arguments 255 | * `value`: `number | string` | A numeric value against which you want to check your enforced value. 256 | 257 | Strings are parsed using `Number()`, values which are non fully numeric always return false; 258 | 259 | ### Usage 260 | 261 | ```js 262 | enforce(1).greaterThanOrEquals(0); 263 | enforce('10').greaterThanOrEquals(0); 264 | enforce(900).greaterThanOrEquals('100'); 265 | enforce(100).greaterThanOrEquals('100'); 266 | enforce(900).gte('900'); 267 | enforce('1337').gte(1337); 268 | // passes 269 | ``` 270 | 271 | ```js 272 | enforce(100).greaterThanOrEquals('120'); 273 | enforce('100').greaterThanOrEquals(110); 274 | enforce([100]).gte(1); 275 | // throws 276 | ``` 277 | 278 | 279 | ## lengthEquals 280 | ### Description 281 | Checks that your enforced value is equal to the given number. 282 | 283 | ### Arguments 284 | * `size`: `number` | the number which you would like your initial value to be tested against. 285 | 286 | The `value` argument can be of the following types: 287 | * array: checks against length. 288 | * string: checks against length. 289 | 290 | ### Usage examples: 291 | 292 | ```js 293 | enforce([1]).lengthEquals(1); 294 | enforce('a').lengthEquals(1); 295 | // passes 296 | ``` 297 | 298 | ```js 299 | enforce([1, 2]).lengthEquals(1); 300 | enforce('').lengthEquals(1); 301 | // throws 302 | ``` 303 | 304 | ## lengthNotEquals 305 | ### Description 306 | Checks that your enforced value is not equal to the given number. 307 | Reverse implementation of `lengthEquals`. 308 | 309 | ### Arguments 310 | * `size`: `number` | the number which you would like your initial value to be tested against. 311 | 312 | The `value` argument can be of the following types: 313 | * array: checks against length. 314 | * string: checks against length. 315 | 316 | ### Usage examples: 317 | 318 | ```js 319 | enforce([1]).lengthNotEquals(0); 320 | enforce('a').lengthNotEquals(3); 321 | // passes 322 | ``` 323 | 324 | ```js 325 | enforce([1]).lengthNotEquals(1); 326 | enforce('').lengthNotEquals(0); 327 | // throws 328 | ``` 329 | 330 | 331 | ## lessThan 332 | 333 | - alias: `lt()` 334 | 335 | ### Description 336 | Checks that your numeric enforced value is smaller than a given numeric value. 337 | 338 | ### Arguments 339 | * `value`: `number | string` | A numeric value against which you want to check your enforced value. 340 | 341 | Strings are parsed using `Number()`, values which are non fully numeric always return false; 342 | 343 | ### Usage 344 | 345 | ```js 346 | enforce(0).lessThan(1); 347 | enforce(2).lessThan('10'); 348 | enforce('90').lt(100); 349 | // passes 350 | ``` 351 | 352 | ```js 353 | enforce(100).lessThan(100); 354 | enforce('110').lessThan(100); 355 | enforce([0]).lt(1); 356 | // throws 357 | ``` 358 | 359 | 360 | ## lessThanOrEquals 361 | 362 | - alias: `lte()` 363 | 364 | ### Description 365 | Checks that your numeric enforced value is smaller than or equals to a given numeric value. 366 | 367 | ### Arguments 368 | * `value`: `number | string` | A numeric value against which you want to check your enforced value. 369 | 370 | Strings are parsed using `Number()`, values which are non fully numeric always return false; 371 | 372 | ### Usage 373 | 374 | ```js 375 | enforce(0).lessThanOrEquals(1); 376 | enforce(2).lessThanOrEquals('10'); 377 | enforce('90').lte(100); 378 | enforce(100).lte('100'); 379 | // passes 380 | ``` 381 | 382 | ```js 383 | enforce(100).lessThanOrEquals(90); 384 | enforce('110').lessThanOrEquals(100); 385 | enforce([0]).lte(1); 386 | // throws 387 | ``` 388 | 389 | 390 | ## longerThan 391 | ### Description 392 | Checks that your enforced value is longer than a given number. 393 | 394 | ### Arguments 395 | * `size`: `number` | the number which you would like your initial value to be tested against. 396 | 397 | The `value` argument can be of the following types: 398 | * array: checks against length. 399 | * string: checks against length. 400 | 401 | ### Usage examples: 402 | 403 | ```js 404 | enforce([1]).longerThan(0); 405 | enforce('ab').longerThan(1); 406 | // passes 407 | ``` 408 | 409 | ```js 410 | enforce([1]).longerThan(2); 411 | enforce('').longerThan(0); 412 | // throws 413 | ``` 414 | 415 | 416 | ## longerThanOrEquals 417 | ### Description 418 | Checks that your enforced value is longer than or equals to a given number. 419 | 420 | ### Arguments 421 | * `size`: `number` | the number which you would like your initial value to be tested against. 422 | 423 | The `value` argument can be of the following types: 424 | * array: checks against length. 425 | * string: checks against length. 426 | 427 | ### Usage examples: 428 | 429 | ```js 430 | enforce([1]).longerThanOrEquals(0); 431 | enforce('ab').longerThanOrEquals(1); 432 | enforce([1]).longerThanOrEquals(1); 433 | enforce('a').longerThanOrEquals(1); 434 | // passes 435 | ``` 436 | 437 | ```js 438 | enforce([1]).longerThanOrEquals(2); 439 | enforce('').longerThanOrEquals(1); 440 | // throws 441 | ``` 442 | 443 | 444 | ## numberEquals 445 | ### Description 446 | Checks that your numeric enforced value is equals another value. 447 | 448 | ### Arguments 449 | * `value`: `number | string` | A numeric value against which you want to check your enforced value. 450 | 451 | Strings are parsed using `Number()`, values which are non fully numeric always return false; 452 | 453 | ### Usage 454 | 455 | ```js 456 | enforce(0).numberEquals(0); 457 | enforce(2).numberEquals('2'); 458 | // passes 459 | ``` 460 | 461 | ```js 462 | enforce(100).numberEquals(10); 463 | enforce('110').numberEquals(100); 464 | enforce([0]).numberEquals(1); 465 | // throws 466 | ``` 467 | 468 | 469 | ## numberNotEquals 470 | ### Description 471 | Checks that your numeric enforced value does not equal another value. 472 | Reverse implementation of `numberEquals`. 473 | 474 | ### Arguments 475 | * `value`: `number | string` | A numeric value against which you want to check your enforced value. 476 | 477 | Strings are parsed using `Number()`, values which are non fully numeric always return false; 478 | 479 | ### Usage 480 | 481 | ```js 482 | enforce(2).numberNotEquals(0); 483 | enforce('11').numberNotEquals('10'); 484 | // passes 485 | ``` 486 | 487 | ```js 488 | enforce(100).numberNotEquals(100); 489 | enforce('110').numberNotEquals(100); 490 | // throws 491 | ``` 492 | 493 | 494 | ## shorterThan 495 | ### Description 496 | Checks that your enforced value is shorter than a given number. 497 | 498 | ### Arguments 499 | * `size`: `number` | the number which you would like your initial value to be tested against. 500 | 501 | The `value` argument can be of the following types: 502 | * array: checks against length. 503 | * string: checks against length. 504 | 505 | ### Usage examples: 506 | 507 | ```js 508 | enforce([]).shorterThan(1); 509 | enforce('a').shorterThan(2); 510 | // passes 511 | ``` 512 | 513 | ```js 514 | enforce([1]).shorterThan(0); 515 | enforce('').shorterThan(0); 516 | // throws 517 | ``` 518 | 519 | 520 | ## shorterThanOrEquals 521 | ### Description 522 | Checks that your enforced value is shorter than or equals to a given number. 523 | 524 | ### Arguments 525 | * `size`: `number` | the number which you would like your initial value to be tested against. 526 | 527 | The `value` argument can be of the following types: 528 | * array: checks against length. 529 | * string: checks against length. 530 | 531 | ### Usage examples: 532 | 533 | #### Passing examples: 534 | ```js 535 | enforce([]).shorterThanOrEquals(1); 536 | enforce('a').shorterThanOrEquals(2); 537 | enforce([]).shorterThanOrEquals(0); 538 | enforce('a').shorterThanOrEquals(1); 539 | // passes 540 | ``` 541 | 542 | ```js 543 | enforce([1]).shorterThanOrEquals(0); 544 | enforce('ab').shorterThanOrEquals(1); 545 | // throws 546 | ``` 547 | 548 | 549 | ## matches 550 | ### Description 551 | Checks if a value contains a regex match. 552 | 553 | ### Arguments 554 | * `regexp`: either a `RegExp` object, or a RegExp valid string 555 | 556 | ### Usage examples: 557 | 558 | ```js 559 | enforce(1984).matches(/[0-9]/); 560 | enforce(1984).matches('[0-9]'); 561 | enforce('1984').matches(/[0-9]/); 562 | enforce('1984').matches('[0-9]'); 563 | enforce('198four').matches(/[0-9]/); 564 | enforce('198four').matches('[0-9]'); 565 | // passes 566 | ``` 567 | 568 | ```js 569 | enforce('ninety eighty four').matches(/[0-9]/); 570 | enforce('ninety eighty four').matches('[0-9]'); 571 | // throws 572 | ``` 573 | 574 | 575 | ## notMatches 576 | ### Description 577 | Checks if a value does not contain a regex match. 578 | Reverse implementation of `matches`. 579 | 580 | ### Usage examples: 581 | 582 | ```js 583 | enforce(1984).notMatches(/[0-9]/); 584 | // throws 585 | ``` 586 | 587 | ```js 588 | enforce('ninety eighty four').notMatches('[0-9]'); 589 | // passes 590 | ``` 591 | 592 | 593 | ## inside 594 | ### Description 595 | Checks if your enforced value is contained in another array or string. 596 | Your enforced value can be of the following types: 597 | * `string` 598 | * `number` 599 | * `boolean` 600 | 601 | ### Arguments 602 | * `container`: a `string` or an `array` which may contain the value specified. 603 | 604 | ### Usage examples: 605 | 606 | #### inside: array 607 | Checks for membership in an array. 608 | 609 | - string: checks if a string is an element in an array 610 | 611 | ```js 612 | enforce('hello').inside(['hello', 'world']); 613 | // passes 614 | ``` 615 | 616 | ```js 617 | enforce('hello!').inside(['hello', 'world']); 618 | // throws 619 | ``` 620 | - number: checks if a number is an element in an array 621 | 622 | ```js 623 | enforce(1).inside([1, 2]); 624 | // passes 625 | ``` 626 | 627 | ```js 628 | enforce(3).inside([1, 2]); 629 | // throws 630 | ``` 631 | 632 | - boolean: checks if a number is an element in an array 633 | 634 | ```js 635 | enforce(false).inside([true, false]); 636 | // passes 637 | ``` 638 | 639 | ```js 640 | enforce(true).inside([1,2,3]); 641 | // throws 642 | ``` 643 | 644 | #### inside: string 645 | - string: checks if a string is inside another string 646 | 647 | ```js 648 | enforce('da').inside('tru dat.'); 649 | // passes 650 | ``` 651 | 652 | ```js 653 | enforce('ad').inside('tru dat.'); 654 | // throws 655 | ``` 656 | 657 | 658 | ## notInside 659 | ### Description 660 | Checks if a given value is not contained in another array or string. 661 | Reverse implementation of `inside`. 662 | 663 | ### Usage examples: 664 | 665 | ```js 666 | enforce('ad').notInside('tru dat.'); 667 | enforce('hello!').notInside(['hello', 'world']); 668 | // passes 669 | ``` 670 | 671 | ```js 672 | enforce('hello').notInside(['hello', 'world']); 673 | enforce('da').notInside('tru dat.'); 674 | // throws 675 | ``` 676 | 677 | 678 | ## isTruthy 679 | ### Description 680 | Checks if a value is truthy; Meaning: if it can be coerced into boolean `true`. 681 | Anything not in the following list is considered to be truthy. 682 | 683 | * `undefined` 684 | * `null` 685 | * `false` 686 | * `0` 687 | * `NaN` 688 | * empty string (`""`) 689 | 690 | ### Usage examples: 691 | 692 | ```js 693 | enforce("hello").isTruthy(); 694 | enforce(true).isTruthy(); 695 | enforce(1).isTruthy(); 696 | // passes 697 | ``` 698 | 699 | ```js 700 | enforce(false).isTruthy(); 701 | enforce(null).isTruthy(); 702 | enforce(undefined).isTruthy(); 703 | enforce(0).isTruthy(); 704 | enforce(NaN).isTruthy(); 705 | enforce("").isTruthy(); 706 | // throws 707 | ``` 708 | 709 | 710 | ## isFalsy 711 | ### Description 712 | Checks if a value is falsy; Meaning: if it can be coerced into boolean `false`. 713 | Reverse implementation of `isTruthy`. 714 | 715 | Anything not in the following list is considered to be truthy: 716 | * `undefined` 717 | * `null` 718 | * `false` 719 | * `0` 720 | * `NaN` 721 | * empty string (`""`) 722 | 723 | ### Usage examples: 724 | 725 | ```js 726 | enforce(false).isFalsy(); 727 | enforce(0).isFalsy(); 728 | enforce(undefined).isFalsy(); 729 | // passes 730 | ``` 731 | 732 | ```js 733 | enforce(1).isFalsy(); 734 | enforce(true).isFalsy(); 735 | enforce('hi').isFalsy(); 736 | // throws 737 | ``` 738 | 739 | 740 | ## isArray 741 | ### Description 742 | Checks if a value is of type `Array`. 743 | 744 | ### Usage examples: 745 | 746 | ```js 747 | enforce(['hello']).isArray(); 748 | // passes 749 | ``` 750 | 751 | ```js 752 | enforce('hello').isArray(); 753 | // throws 754 | ``` 755 | 756 | 757 | ## isNotArray 758 | ### Description 759 | Checks if a value is of any type other than `Array`. 760 | Reverse implementation of `isArray`. 761 | 762 | ### Usage examples: 763 | 764 | ```js 765 | enforce(['hello']).isNotArray(); 766 | // throws 767 | ``` 768 | 769 | ```js 770 | enforce('hello').isNotArray(); 771 | // passes 772 | ``` 773 | 774 | 775 | ## isNumber 776 | ### Description 777 | Checks if a value is of type `number`. 778 | 779 | ### Usage examples: 780 | 781 | ```js 782 | enforce(143).isNumber(); 783 | enforce(NaN).isNumber(); // (NaN is of type 'number!') 784 | // passes 785 | ``` 786 | 787 | ```js 788 | enforce([]).isNumber(); 789 | enforce("143").isNumber(); 790 | // throws 791 | ``` 792 | 793 | 794 | ## isNotNumber 795 | ### Description 796 | Checks if a value is of any type other than `number`. 797 | Reverse implementation of `isNumber`. 798 | 799 | ### Usage examples: 800 | 801 | ```js 802 | enforce('143').isNotNumber(); 803 | enforce(143).isNotNumber(); 804 | // passes 805 | ``` 806 | 807 | ```js 808 | enforce(143).isNotNumber(); 809 | enforce(NaN).isNotNumber(); // throws (NaN is of type 'number!') 810 | // throws 811 | ``` 812 | 813 | 814 | ## isString 815 | ### Description 816 | Checks if a value is of type `String`. 817 | 818 | ### Usage examples: 819 | 820 | ```js 821 | enforce('hello').isString(); 822 | // passes 823 | ``` 824 | 825 | ```js 826 | enforce(['hello']).isString(); 827 | enforce(1984).isString(); 828 | // throws 829 | ``` 830 | 831 | 832 | ## isNotString 833 | ### Description 834 | Checks if a value is of any type other than `String`. 835 | Reverse implementation of `isString`. 836 | 837 | ### Usage examples: 838 | 839 | ```js 840 | enforce('hello').isNotString(); 841 | // throws 842 | ``` 843 | 844 | ```js 845 | enforce(['hello']).isNotString(); 846 | // passes 847 | ``` 848 | 849 | 850 | ## isOdd 851 | ### Description 852 | Checks if a value is an odd numeric value. 853 | 854 | ### Usage examples: 855 | 856 | ```js 857 | enforce('1').isOdd(); 858 | enforce(9).isOdd(); 859 | // passes 860 | ``` 861 | 862 | ```js 863 | enforce(2).isOdd(); 864 | enforce('4').isOdd(); 865 | enforce('1withNumber').isOdd(); 866 | enforce([1]).isOdd(); 867 | // throws 868 | ``` 869 | 870 | 871 | ## isEven 872 | ### Description 873 | Checks if a value is an even numeric value. 874 | 875 | ### Usage examples: 876 | 877 | ```js 878 | enforce(0).isEven(); 879 | enforce('2').isEven(); 880 | // passes 881 | ``` 882 | 883 | ```js 884 | enforce(1).isEven(); 885 | enforce('3').isEven(); 886 | enforce('2withNumber').isEven(); 887 | enforce([0]).isEven(); 888 | // throws 889 | ``` 890 | 891 | 892 | # Custom enforce rules 893 | To make it easier to reuse logic across your application, sometimes you would want to encapsulate bits of logic in rules that you can use later on, for example, "what's considered a valid email". 894 | 895 | Your custom rules are essentially a single javascript object containing your rules. 896 | ```js 897 | const myCustomRules = { 898 | isValidEmail: (value) => value.indexOf('@') > -1, 899 | hasKey: (value, {key}) => value.hasOwnProperty(key), 900 | passwordsMatch: (passConfirm, options) => passConfirm === options.passConfirm && options.passIsValid 901 | } 902 | ``` 903 | Just like the predefined rules, your custom rules can accepts two parameters: 904 | * `value` The actual value you are testing against. 905 | * `args` (optional) the arguments which you pass on when running your tests. 906 | 907 | 908 | You can extend enforce with your custom rules by creating a new instance of `Enforce` and adding the rules object as the argument. 909 | 910 | ```js 911 | import { Enforce } from 'passable'; 912 | 913 | const myCustomRules = { 914 | isValidEmail: (value) => value.indexOf('@') > -1, 915 | hasKey: (value, key) => value.hasOwnProperty(key), 916 | passwordsMatch: (passConfirm, options) => passConfirm === options.passConfirm && options.passIsValid 917 | } 918 | 919 | const enforce = new Enforce(myCustomRules); 920 | 921 | enforce(user.email).isValidEmail(); 922 | ``` 923 | --------------------------------------------------------------------------------