├── .eslintignore ├── index.js ├── jest.config.js ├── media ├── email.gif ├── logo.png ├── url.gif ├── number.gif └── password.gif ├── .eslintrc.js ├── src ├── index.js ├── testFunctions │ ├── types │ │ ├── number.js │ │ ├── string.js │ │ ├── boolean.js │ │ ├── float.js │ │ ├── function.js │ │ ├── int.js │ │ ├── string-int.js │ │ ├── string-date.js │ │ ├── string-float.js │ │ ├── date.js │ │ ├── string-boolean.js │ │ ├── url.js │ │ ├── email.js │ │ ├── array.js │ │ ├── password.js │ │ └── index.js │ ├── index.js │ └── primitives.js ├── Validator │ ├── util.js │ └── index.js ├── Rule │ ├── util.js │ └── index.js └── util.js ├── test ├── Rule │ ├── boolean.test.js │ ├── getErrors.test.js │ ├── string-boolean.test.js │ ├── multileTypes.test.js │ ├── email.test.js │ ├── function.test.js │ ├── util.test.js │ ├── rule.test.js │ ├── url.test.js │ ├── string-date.test.js │ ├── float.test.js │ ├── string-int.test.js │ ├── date.test.js │ ├── string-float.test.js │ ├── array.test.js │ ├── password.test.js │ ├── string.test.js │ ├── int.test.js │ └── number.test.js └── Validator │ ├── validator.util.test.js │ └── Validator.test.js ├── package.json ├── CONTRIBUTING.md ├── LICENSE ├── .gitignore ├── index.d.ts └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src'); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | }; 3 | -------------------------------------------------------------------------------- /media/email.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oussamahamdaoui/forgJs/HEAD/media/email.gif -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oussamahamdaoui/forgJs/HEAD/media/logo.png -------------------------------------------------------------------------------- /media/url.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oussamahamdaoui/forgJs/HEAD/media/url.gif -------------------------------------------------------------------------------- /media/number.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oussamahamdaoui/forgJs/HEAD/media/number.gif -------------------------------------------------------------------------------- /media/password.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oussamahamdaoui/forgJs/HEAD/media/password.gif -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": ["plugin:jest/recommended","airbnb-base"] 3 | }; 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const Validator = require('./Validator'); 2 | const Rule = require('./Rule'); 3 | 4 | module.exports = { Validator, Rule }; 5 | -------------------------------------------------------------------------------- /src/testFunctions/types/number.js: -------------------------------------------------------------------------------- 1 | const { NUMBER } = require('../primitives'); 2 | 3 | const number = { 4 | ...NUMBER, 5 | }; 6 | module.exports = number; 7 | -------------------------------------------------------------------------------- /src/testFunctions/types/string.js: -------------------------------------------------------------------------------- 1 | const { STRING } = require('../primitives'); 2 | 3 | const string = { 4 | ...STRING, 5 | }; 6 | module.exports = string; 7 | -------------------------------------------------------------------------------- /src/testFunctions/types/boolean.js: -------------------------------------------------------------------------------- 1 | const { BOOLEAN } = require('../primitives'); 2 | 3 | const boolean = { 4 | ...BOOLEAN, 5 | }; 6 | 7 | module.exports = boolean; 8 | -------------------------------------------------------------------------------- /src/testFunctions/types/float.js: -------------------------------------------------------------------------------- 1 | const { NUMBER } = require('../primitives'); 2 | 3 | const float = { 4 | ...NUMBER, 5 | type: val => Number(val) === val && val % 1 !== 0, 6 | }; 7 | module.exports = float; 8 | -------------------------------------------------------------------------------- /src/testFunctions/types/function.js: -------------------------------------------------------------------------------- 1 | const fn = { 2 | type: val => val && {}.toString.call(val) === '[object Function]', 3 | result: (val, obj) => obj.toBe.test(val(obj.of)), 4 | }; 5 | 6 | module.exports = fn; 7 | -------------------------------------------------------------------------------- /src/testFunctions/types/int.js: -------------------------------------------------------------------------------- 1 | const { NUMBER } = require('../primitives'); 2 | const { isInt } = require('../../util'); 3 | 4 | const int = { 5 | ...NUMBER, 6 | type: isInt, 7 | }; 8 | module.exports = int; 9 | -------------------------------------------------------------------------------- /src/testFunctions/types/string-int.js: -------------------------------------------------------------------------------- 1 | const { STRING } = require('../primitives'); 2 | const { mergeRule } = require('../../util'); 3 | const int = require('./int'); 4 | 5 | 6 | module.exports = mergeRule(STRING, int, val => Number(val)); 7 | -------------------------------------------------------------------------------- /src/testFunctions/types/string-date.js: -------------------------------------------------------------------------------- 1 | const { STRING } = require('../primitives'); 2 | const { mergeRule } = require('../../util'); 3 | const date = require('./date'); 4 | 5 | 6 | module.exports = mergeRule(STRING, date, val => new Date(val)); 7 | -------------------------------------------------------------------------------- /src/testFunctions/types/string-float.js: -------------------------------------------------------------------------------- 1 | const { STRING } = require('../primitives'); 2 | const { mergeRule } = require('../../util'); 3 | const float = require('./float'); 4 | 5 | 6 | module.exports = mergeRule(STRING, float, val => Number(val)); 7 | -------------------------------------------------------------------------------- /src/testFunctions/types/date.js: -------------------------------------------------------------------------------- 1 | const date = { 2 | after: (val, min) => val - min > 0, 3 | before: (val, max) => val - max < 0, 4 | between: (val, range) => val - range[0] > 0 && val - range[1] < 0, 5 | equal: (val, equal) => val - equal === 0, 6 | type: val => val instanceof Date, 7 | }; 8 | 9 | module.exports = date; 10 | -------------------------------------------------------------------------------- /src/testFunctions/types/string-boolean.js: -------------------------------------------------------------------------------- 1 | const { STRING, BOOLEAN } = require('../primitives'); 2 | const { mergeRule } = require('../../util'); 3 | 4 | const castBoolean = (val) => { 5 | if (val === 'true') return true; 6 | if (val === 'false') return false; 7 | return 'a'; 8 | }; 9 | 10 | module.exports = mergeRule(STRING, BOOLEAN, castBoolean); 11 | -------------------------------------------------------------------------------- /src/testFunctions/types/url.js: -------------------------------------------------------------------------------- 1 | const { STRING } = require('../primitives'); 2 | const { isString, URL_REGEX } = require('../../util'); 3 | 4 | 5 | const url = { 6 | ...STRING, 7 | type: val => isString(val) && URL_REGEX.test(val), 8 | domain: (val, f) => f(val.match(URL_REGEX)[3]), 9 | protocol: (val, f) => f(val.match(URL_REGEX)[1]), 10 | }; 11 | 12 | module.exports = url; 13 | -------------------------------------------------------------------------------- /src/testFunctions/types/email.js: -------------------------------------------------------------------------------- 1 | const { STRING } = require('../primitives'); 2 | const { isString } = require('../../util'); 3 | 4 | const email = { 5 | ...STRING, 6 | type: val => isString(val) && /\S+@\S+\.\S+/.test(val), 7 | user: (val, f) => f(val.match(/(\S+)@\S+\.\S+/)[1]), 8 | domain: (val, f) => f(val.match(/\S+@(\S+)\.\S+/)[1]), 9 | 10 | }; 11 | module.exports = email; 12 | -------------------------------------------------------------------------------- /src/testFunctions/types/array.js: -------------------------------------------------------------------------------- 1 | const { isArray } = require('../../util'); 2 | 3 | const array = { 4 | of: (arr, rule) => { 5 | let ret = true; 6 | arr.forEach((el) => { 7 | if (rule.test(el) === false) { 8 | ret = false; 9 | } 10 | }); 11 | return ret; 12 | }, 13 | notEmpty: val => val.length !== 0, 14 | length: (val, len) => val.length === len, 15 | type: isArray, 16 | }; 17 | 18 | module.exports = array; 19 | -------------------------------------------------------------------------------- /test/Rule/boolean.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('boolean returns true if true tested', () => { 4 | const rule = new Rule({ 5 | type: 'boolean', 6 | }, null); 7 | 8 | expect(rule.test(true)).toBe(true); 9 | }); 10 | 11 | test('boolean returns true if false tested', () => { 12 | const rule = new Rule({ 13 | type: 'boolean', 14 | toBe: false, 15 | }, null); 16 | 17 | expect(rule.test(false)).toBe(true); 18 | }); 19 | -------------------------------------------------------------------------------- /test/Validator/validator.util.test.js: -------------------------------------------------------------------------------- 1 | const { traverse, getValFromPath } = require('../../src/Validator/util'); 2 | 3 | test('test getValFromPath returns 5 path is correct', () => { 4 | const obj = { 5 | a: { 6 | b: 5, 7 | }, 8 | }; 9 | expect(getValFromPath('a.b', obj)).toBe(5); 10 | }); 11 | 12 | 13 | test('test travers', (done) => { 14 | const obj = { 15 | a: { 16 | b: 5, 17 | }, 18 | }; 19 | traverse(obj, (val, path) => { 20 | expect(val).toBe(5); 21 | expect(path).toBe('a.b'); 22 | done(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/Rule/getErrors.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type rule.getError() returns array of errors when input is wrong', () => { 4 | const strRule = new Rule({ 5 | type: 'string', 6 | }, 'strRule must be string'); 7 | 8 | expect(strRule.getError(1)).toEqual(['strRule must be string']); 9 | }); 10 | 11 | test('type rule.getError() call error function (if the error of the rule is function)', () => { 12 | const strRule = new Rule({ 13 | type: 'string', 14 | }, (key, value) => `${key} must be a string, ${value} is not a string`); 15 | 16 | expect(strRule.getError('something', 1)).toEqual(['something must be a string, 1 is not a string']); 17 | }); 18 | -------------------------------------------------------------------------------- /src/Validator/util.js: -------------------------------------------------------------------------------- 1 | const Rule = require('../Rule'); 2 | 3 | function traverse(o, fn, p) { 4 | let path = p || ''; 5 | 6 | Object.keys(o).forEach((i) => { 7 | if (o[i] !== null && typeof (o[i]) === 'object' && !(o[i] instanceof Rule)) { 8 | traverse(o[i], fn, `${path}.${i}`); 9 | } else { 10 | fn.apply(null, [o[i], `${path}.${i}`.substring(1)]); 11 | path = ''; 12 | } 13 | }); 14 | } 15 | 16 | 17 | function getValFromPath(p, obj) { 18 | const path = p.split('.'); 19 | if (path.length === 1) { 20 | return obj[path[0]]; 21 | } 22 | 23 | const key = path.shift(); 24 | return getValFromPath(path.join('.'), obj[key]); 25 | } 26 | 27 | module.exports = { getValFromPath, traverse }; 28 | -------------------------------------------------------------------------------- /test/Rule/string-boolean.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('non valid string boolean', () => { 4 | const stringBoolean = new Rule({ 5 | type: 'string-boolean', 6 | }, null); 7 | 8 | expect(stringBoolean.test('hello')).toBe(false); 9 | }); 10 | 11 | test('valid string boolean', () => { 12 | const stringBoolean = new Rule({ 13 | type: 'string-boolean', 14 | }, null); 15 | 16 | expect(stringBoolean.test('true')).toBe(true); 17 | expect(stringBoolean.test('false')).toBe(true); 18 | }); 19 | 20 | test('string boolean is true', () => { 21 | const stringBoolean = new Rule({ 22 | type: 'string-boolean', 23 | toBe: true, 24 | }, null); 25 | 26 | expect(stringBoolean.test('true')).toBe(true); 27 | expect(stringBoolean.test('false')).toBe(false); 28 | }); 29 | -------------------------------------------------------------------------------- /test/Rule/multileTypes.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('returns true if int or float or number', () => { 4 | const intRule = new Rule({ 5 | type: 'int|float|number', 6 | }, null); 7 | 8 | expect(intRule.test(1.2)).toBe(true); 9 | }); 10 | 11 | test('reterns false if not int or float', () => { 12 | const intRule = new Rule({ 13 | type: 'int|float|number', 14 | }, null); 15 | 16 | expect(intRule.test([])).toBe(false); 17 | }); 18 | 19 | test('reterns false if not (int and number)', () => { 20 | const intRule = new Rule({ 21 | type: 'int&number', 22 | }, null); 23 | 24 | expect(intRule.test([])).toBe(false); 25 | }); 26 | 27 | test('reterns true if int and number', () => { 28 | const intRule = new Rule({ 29 | type: 'int&number', 30 | }, null); 31 | 32 | expect(intRule.test(3)).toBe(true); 33 | }); 34 | -------------------------------------------------------------------------------- /src/testFunctions/types/password.js: -------------------------------------------------------------------------------- 1 | const { STRING } = require('../primitives'); 2 | 3 | const password = { 4 | ...STRING, 5 | numbers: (val, number) => !!val.match(/(\d)/g) && val.match(/(\d)/g).length >= number, 6 | uppercase: (val, number) => !!val.match(/([A-Z])/g) && val.match(/([A-Z])/g).length >= number, 7 | specialChars: (val, number) => !!val.match(/([^a-zA-Z])/g) && val.match(/([^a-zA-Z])/g).length >= number, 8 | matchesOneOf: (val, arr) => { 9 | for (let i = 0; i < arr.length; i += 1) { 10 | if (val.indexOf(arr[i]) !== -1) { 11 | return true; 12 | } 13 | } 14 | return false; 15 | }, 16 | 17 | matchesAllOf: (val, arr) => { 18 | for (let i = 0; i < arr.length; i += 1) { 19 | if (val.indexOf(arr[i]) === -1) { 20 | return false; 21 | } 22 | } 23 | return true; 24 | }, 25 | }; 26 | module.exports = password; 27 | -------------------------------------------------------------------------------- /test/Rule/email.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type email false', () => { 4 | const emailRule = new Rule({ 5 | type: 'email', 6 | }, null); 7 | 8 | expect(emailRule.test([])).toBe(false); 9 | }); 10 | 11 | test('type email', () => { 12 | const emailRule = new Rule({ 13 | type: 'email', 14 | }, null); 15 | 16 | expect(emailRule.test('frfrfr')).toBe(false); 17 | }); 18 | 19 | test('type email true', () => { 20 | const emailRule = new Rule({ 21 | type: 'email', 22 | }, null); 23 | 24 | expect(emailRule.test('dedede@afe.fr')).toBe(true); 25 | }); 26 | 27 | test('type user and domain true', () => { 28 | const emailRule = new Rule({ 29 | type: 'email', 30 | user: user => user === 'dedede', 31 | domain: domain => ['outlook', 'gmail'].indexOf(domain) !== -1, 32 | }, null); 33 | 34 | expect(emailRule.test('dedede@gmail.fr')).toBe(true); 35 | }); 36 | -------------------------------------------------------------------------------- /src/testFunctions/types/index.js: -------------------------------------------------------------------------------- 1 | const int = require('./int'); 2 | const float = require('./float'); 3 | const number = require('./number'); 4 | const string = require('./string'); 5 | const password = require('./password'); 6 | const email = require('./email'); 7 | const url = require('./url'); 8 | const boolean = require('./boolean'); 9 | const date = require('./date'); 10 | const array = require('./array'); 11 | const func = require('./function'); 12 | const stringInt = require('./string-int'); 13 | const stringFloat = require('./string-float'); 14 | const stringDate = require('./string-date'); 15 | const stringBoolean = require('./string-boolean'); 16 | 17 | 18 | module.exports = { 19 | int, 20 | float, 21 | number, 22 | string, 23 | password, 24 | email, 25 | url, 26 | boolean, 27 | date, 28 | array, 29 | function: func, 30 | 'string-int': stringInt, 31 | 'string-float': stringFloat, 32 | 'string-date': stringDate, 33 | 'string-boolean': stringBoolean, 34 | }; 35 | -------------------------------------------------------------------------------- /test/Rule/function.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('returns true if function', () => { 4 | const functionRule = new Rule({ 5 | type: 'function', 6 | }, null); 7 | 8 | expect(functionRule.test(() => {})).toBe(true); 9 | }); 10 | 11 | test('returns true if function result matches a Rule', () => { 12 | function someFunctionThatReturnsAnInt(int) { 13 | return int * 5; 14 | } 15 | 16 | const functionTest = new Rule({ 17 | type: 'function', 18 | result: { 19 | of: 5, 20 | toBe: new Rule('int'), 21 | }, 22 | }, null); 23 | 24 | expect(functionTest.test(someFunctionThatReturnsAnInt)).toBe(true); 25 | }); 26 | 27 | test('returns false if function result doesn\'t match a Rule', () => { 28 | const functionRule = new Rule({ 29 | type: 'function', 30 | result: { 31 | of: 5, 32 | toBe: new Rule('int'), 33 | }, 34 | }, null); 35 | 36 | expect(functionRule.test(() => 'val - 10')).toBe(false); 37 | }); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cesium133/forgjs", 3 | "version": "1.1.12", 4 | "description": "forgJs is a javascript lightweight object validator.", 5 | "main": "index.js", 6 | "typings": "./index.d.ts", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "devDependencies": { 11 | "eslint": "^5.9.0", 12 | "eslint-config-airbnb-base": "^13.1.0", 13 | "eslint-plugin-import": "^2.14.0", 14 | "eslint-plugin-jest": "^22.1.0", 15 | "jest": "^29.3.1" 16 | }, 17 | "scripts": { 18 | "test": "jest" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/oussamahamdaoui/forgJs.git" 23 | }, 24 | "keywords": [ 25 | "javascript", 26 | "object", 27 | "validator", 28 | "light" 29 | ], 30 | "author": "Oussama Hamdaoui ", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/oussamahamdaoui/forgJs/issues" 34 | }, 35 | "homepage": "https://github.com/oussamahamdaoui/forgJs#readme" 36 | } 37 | -------------------------------------------------------------------------------- /test/Rule/util.test.js: -------------------------------------------------------------------------------- 1 | const { looseEqual, flattenObject } = require('../../src/util'); 2 | 3 | test('looseEqual object false', () => { 4 | const a = { 5 | a: 'hello', 6 | b: 'cccc', 7 | }; 8 | 9 | const b = { 10 | a: 'cd', 11 | b: 'cc', 12 | }; 13 | 14 | expect(looseEqual(a, b)).toBe(false); 15 | }); 16 | 17 | test('looseEqual object true', () => { 18 | const a = { 19 | a: 'hello', 20 | b: 'cccc', 21 | }; 22 | 23 | const b = { 24 | a: 'hello', 25 | b: 'cccc', 26 | }; 27 | 28 | expect(looseEqual(a, b)).toBe(true); 29 | }); 30 | 31 | test('looseEqual object and array fals', () => { 32 | const a = { 33 | a: 'hello', 34 | b: 'cccc', 35 | }; 36 | 37 | const b = [ 38 | 'hello', 39 | 'cccc', 40 | ]; 41 | 42 | expect(looseEqual(a, b)).toBe(false); 43 | }); 44 | 45 | 46 | test('flatten Object', () => { 47 | const a = { 48 | a: 'hello', 49 | b: { 50 | d: 'dazda', 51 | }, 52 | }; 53 | expect(flattenObject(a)).toEqual({ a: 'hello', 'b.d': 'dazda' }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/Rule/rule.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('adding a custom rule', () => { 4 | Rule.addCustom('customInteger', { 5 | min: (val, min) => val - min > 0, 6 | max: (val, max) => val - max < 0, 7 | equal: (val, equal) => val === equal, 8 | type: val => Number.isInteger(val) && val > 0 && val < 100, 9 | }); 10 | 11 | const customInteger = new Rule({ 12 | type: 'customInteger', 13 | min: 10, 14 | }, null); 15 | 16 | expect(customInteger.test(11)).toBe(true); 17 | 18 | expect(customInteger.test(200)).toBe(false); 19 | }); 20 | 21 | test('throwing when type not exist', () => { 22 | expect(() => { 23 | const customInteger = new Rule({ 24 | type: 'something', 25 | min: 10, 26 | }, null); 27 | 28 | customInteger.test(11); 29 | }).toThrow(); 30 | }); 31 | 32 | test('throwing when type is undefined', () => { 33 | expect(() => { 34 | const customInteger = new Rule({ 35 | min: 10, 36 | }, null); 37 | 38 | customInteger.test(11); 39 | }).toThrow(); 40 | }); 41 | -------------------------------------------------------------------------------- /test/Rule/url.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type email false', () => { 4 | const urlRule = new Rule({ 5 | type: 'url', 6 | }, null); 7 | 8 | expect(urlRule.test([])).toBe(false); 9 | }); 10 | 11 | test('type url false', () => { 12 | const urlRule = new Rule({ 13 | type: 'url', 14 | }, null); 15 | 16 | expect(urlRule.test('frfrfr')).toBe(false); 17 | }); 18 | 19 | 20 | test('type url true', () => { 21 | const urlRule = new Rule({ 22 | type: 'url', 23 | }, null); 24 | 25 | expect(urlRule.test('https://google.fr')).toBe(true); 26 | }); 27 | 28 | 29 | test('type domain true', () => { 30 | const urlRule = new Rule({ 31 | type: 'url', 32 | domain: domain => domain === 'google.fr', 33 | }, null); 34 | 35 | expect(urlRule.test('https://google.fr')).toBe(true); 36 | }); 37 | 38 | test('type protocol true', () => { 39 | const urlRule = new Rule({ 40 | type: 'url', 41 | protocol: prot => prot === 'https', 42 | }, null); 43 | 44 | expect(urlRule.test('https://google.fr')).toBe(true); 45 | }); 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Thank you everyone for contributing to make this code better, if you have suggestions or ideas to improve the code please feel free to leave a comment here #29. 4 | Rules: 5 | 6 | ### 1 Please use this template which will help developers to test and better understand your request 7 | 8 | ```javascript 9 | const someRule= new Rule({ 10 | type: 'yourType', 11 | prop1: val1, 12 | prop2: val2, ... 13 | }, null); 14 | 15 | someRule.test(validValue) // returns true 16 | someRule.test(invalidValue) // returns false 17 | ``` 18 | 19 | ### 2 Please if you think a comment is a good feature to be added like the comment instead of creating a new one. 20 | 21 | ### 3 Before submitting a new comment check if the same comment is not already present 22 | 23 | ### 4 If you submit a PR (pull request) and you only change the Readme please add `[ci skip]` to your commit message 24 | 25 | ### 5 If you have any questions ask them in the FAQ 26 | 27 | ### 6 Please have fun, and if you feel like not following the rules then don't follow them 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 oussamahamdaoui 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/Rule/util.js: -------------------------------------------------------------------------------- 1 | const { isFunction } = require('./../util'); 2 | 3 | /** 4 | * If the error is function, calls this function with two arguments: path and value 5 | * and returns the result. If error is string, just returns it. 6 | * 7 | * @param {Function|string} error representation of error 8 | * @param {string} path 9 | * @param {any} value 10 | * @returns {String} 11 | */ 12 | const getErrorFromFunctionOrString = (error, path, value) => { 13 | if (isFunction(error)) { 14 | return error(path, value); 15 | } 16 | return error; 17 | }; 18 | 19 | /** 20 | * Returns error message for error. If error does not have any property 21 | * named key, returns default error message. 22 | * 23 | * @param {Object} error object error 24 | * @param {any} path 25 | * @param {any} value 26 | * @param {String} key name of error's property which value is error message 27 | * @returns {String} 28 | */ 29 | const getErrorFromObject = (error, path, value, key) => { 30 | if (!error[key]) { 31 | return `${path} doesn't satisfy the ${key} rule`; // Here should be the default error message. 32 | } 33 | return getErrorFromFunctionOrString(error[key], path, value); 34 | }; 35 | 36 | module.exports = { getErrorFromObject, getErrorFromFunctionOrString }; 37 | -------------------------------------------------------------------------------- /test/Rule/string-date.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type test not valid string is false', () => { 4 | const stringDate = new Rule({ 5 | type: 'string-date', 6 | }, null); 7 | 8 | expect(stringDate.test(new Date(2018, 11, 1))).toBe(false); 9 | }); 10 | 11 | 12 | test('valid date', () => { 13 | const numberRule = new Rule({ 14 | type: 'string-date', 15 | }, null); 16 | 17 | expect(numberRule.test('1995-12-17T03:24:00')).toBe(true); 18 | }); 19 | 20 | 21 | test('date is after', () => { 22 | const numberRule = new Rule({ 23 | type: 'string-date', 24 | after: new Date(2018, 11, 1), 25 | }, null); 26 | 27 | expect(numberRule.test('2019-12-17')).toBe(true); 28 | }); 29 | 30 | test('date is before', () => { 31 | const numberRule = new Rule({ 32 | type: 'string-date', 33 | before: new Date(2018, 11, 1), 34 | }); 35 | 36 | expect(numberRule.test('2019-12-17')).toBe(false); 37 | }); 38 | 39 | 40 | test('date is equal', () => { 41 | const numberRule = new Rule({ 42 | type: 'string-date', 43 | equal: new Date(2018, 10, 1), // javascript moth start from 0 44 | }); 45 | expect(numberRule.test('2018-11-01T00:00')).toBe(true); // You need to specify time otherise default time is set to 01:00 46 | }); 47 | -------------------------------------------------------------------------------- /src/testFunctions/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains all validation functions that are present in all the types 3 | */ 4 | 5 | const { looseEqual } = require('../util'); 6 | const types = require('./types'); 7 | 8 | /** 9 | * The custom validation function 10 | * @param {*} val 11 | * @param {*} f 12 | * @param {*} obj 13 | */ 14 | 15 | const CUSTOM = (val, f, path, obj) => f(val, path, obj); 16 | 17 | /** 18 | * The optional validation function 19 | * @param val the value to be tested 20 | * @param {boolean} state is the value optional 21 | */ 22 | const OPTIONAL = (val, state) => val === undefined && state === true; 23 | 24 | /** 25 | * This function validates that a value is part of an array 26 | * @param val the value 27 | * @param {Array} arr the array of alowed values 28 | */ 29 | const oneOf = (val, arr) => { 30 | for (let i = 0; i < arr.length; i += 1) { 31 | if (looseEqual(arr[i], val)) { 32 | return true; 33 | } 34 | } 35 | return false; 36 | }; 37 | 38 | const TEST_FUNCTIONS = { 39 | ...types, 40 | }; 41 | Object.keys(TEST_FUNCTIONS).forEach((key) => { 42 | TEST_FUNCTIONS[key].custom = CUSTOM; 43 | TEST_FUNCTIONS[key].optional = OPTIONAL; 44 | TEST_FUNCTIONS[key].oneOf = oneOf; 45 | }); 46 | 47 | module.exports = { TEST_FUNCTIONS, OPTIONAL }; 48 | -------------------------------------------------------------------------------- /src/testFunctions/primitives.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These are the basic validation functions that are common to multiple types 3 | */ 4 | 5 | const { 6 | isString, 7 | } = require('../util'); 8 | 9 | /** 10 | * This object combines all validation functions related to numbers 11 | */ 12 | 13 | const NUMBER = { 14 | min: (val, min) => val - min >= 0, 15 | max: (val, max) => val - max <= 0, 16 | equal: (val, equal) => val === equal, 17 | type: val => Number(val) === val, 18 | }; 19 | 20 | /** 21 | * This object combines all validation functions related to strings 22 | */ 23 | const STRING = { 24 | minLength: (val, min) => val.length - min >= 0, 25 | maxLength: (val, max) => val.length - max <= 0, 26 | equal: (val, equal) => val === equal, 27 | match: (val, regex) => regex.test(val), 28 | notEmpty: (val, itShouldNotBeEmpty) => (val.length !== 0) === itShouldNotBeEmpty, 29 | isEmpty: (val, itShouldBeEmpty) => (val.length === 0) === itShouldBeEmpty, 30 | type: isString, 31 | }; 32 | 33 | /** 34 | * This object combines all validation functions related to booleans 35 | */ 36 | 37 | const BOOLEAN = { 38 | type: val => val === true || val === false, 39 | toBe: (val, bool) => val === bool, 40 | }; 41 | 42 | module.exports = { 43 | BOOLEAN, 44 | STRING, 45 | NUMBER, 46 | }; 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .idea 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # Bower dependency directory (https://bower.io/) 28 | bower_components 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (https://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directories 37 | node_modules/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | junit.xml 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # parcel-bundler cache (https://parceljs.org/) 64 | .cache 65 | 66 | # next.js build output 67 | .next 68 | 69 | # nuxt.js build output 70 | .nuxt 71 | 72 | # vuepress build output 73 | .vuepress/dist 74 | 75 | # Serverless directories 76 | .serverless/ 77 | 78 | # FuseBox cache 79 | .fusebox/ 80 | 81 | #DynamoDB Local files 82 | .dynamodb/ 83 | -------------------------------------------------------------------------------- /test/Rule/float.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type float returns true when float', () => { 4 | const floatRule = new Rule({ 5 | type: 'float', 6 | }, null); 7 | expect(floatRule.test(2.001)).toBe(true); 8 | }); 9 | 10 | test('type float returns false when int', () => { 11 | const floatRule = new Rule({ 12 | type: 'float', 13 | }, null); 14 | expect(floatRule.test(2)).toBe(false); 15 | }); 16 | 17 | test('type float returns true when bigger than 100', () => { 18 | const floatRule = new Rule({ 19 | type: 'float', 20 | min: 100, 21 | }, null); 22 | expect(floatRule.test(100.3)).toBe(true); 23 | }); 24 | 25 | test('type float returns true when smaller than 100', () => { 26 | const floatRule = new Rule({ 27 | type: 'float', 28 | max: 100, 29 | }, null); 30 | expect(floatRule.test(99.3)).toBe(true); 31 | }); 32 | 33 | test('type float returns true when two floats are equal', () => { 34 | const floatRule = new Rule({ 35 | type: 'float', 36 | equal: 100.1, 37 | }, null); 38 | expect(floatRule.test(100.1)).toBe(true); 39 | }); 40 | 41 | test('type float returns true when is one of array', () => { 42 | const floatRule = new Rule({ 43 | type: 'float', 44 | oneOf: [3.5, 100.1, 7.2, 0.1], 45 | }, null); 46 | expect(floatRule.test(100.1)).toBe(true); 47 | }); 48 | 49 | test('type float returns false when is not one of array', () => { 50 | const floatRule = new Rule({ 51 | type: 'float', 52 | oneOf: [100.01, 7.2, 0.1], 53 | }, null); 54 | expect(floatRule.test(100.1)).toBe(false); 55 | }); 56 | -------------------------------------------------------------------------------- /src/Validator/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | traverse, getValFromPath, 3 | } = require('./util'); 4 | 5 | const unexpectedFiled = filed => `${filed} is unexpcted`; 6 | const { 7 | flattenObject, arrayContainsAll, 8 | } = require('../util'); 9 | 10 | class Validator { 11 | constructor(o) { 12 | this.rules = o; 13 | } 14 | 15 | test(o) { 16 | let ret = true; 17 | const keysOfRules = Object.keys(flattenObject(this.rules)); 18 | const keysOfObject = Object.keys(flattenObject(o)); 19 | 20 | if (!arrayContainsAll(keysOfObject, keysOfRules)) { 21 | return false; 22 | } 23 | 24 | traverse(this.rules, (rule, path) => { 25 | if (rule.test(getValFromPath(path, o), path, o) === false) { 26 | ret = false; 27 | } 28 | }); 29 | return ret; 30 | } 31 | 32 | testAll(arr) { 33 | for (let i = 0; i < arr.length; i += 1) { 34 | if (this.test(arr[i]) === false) { 35 | return i; 36 | } 37 | } 38 | return -1; 39 | } 40 | 41 | getErrors(o) { 42 | let errors = []; 43 | const keysOfRules = Object.keys(flattenObject(this.rules)); 44 | const keysOfObject = Object.keys(flattenObject(o)); 45 | 46 | if (!arrayContainsAll(keysOfObject, keysOfRules)) { 47 | let undeclaredFiledes = keysOfObject.filter(i => keysOfRules.indexOf(i) < 0); 48 | undeclaredFiledes = undeclaredFiledes.map(unexpectedFiled); 49 | errors = [...errors, ...undeclaredFiledes]; 50 | } 51 | 52 | traverse(this.rules, (rule, path) => { 53 | if (rule.test(getValFromPath(path, o), o, path) === false) { 54 | errors = [ 55 | ...errors, 56 | ...rule.getError(path, getValFromPath(path, o)), 57 | ]; 58 | } 59 | }); 60 | return errors; 61 | } 62 | } 63 | 64 | module.exports = Validator; 65 | -------------------------------------------------------------------------------- /test/Rule/string-int.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type test not number is false', () => { 4 | const numberRule = new Rule({ 5 | type: 'string-int', 6 | }, null); 7 | 8 | expect(numberRule.test([])).toBe(false); 9 | }); 10 | 11 | test('type test int number is false', () => { 12 | const numberRule = new Rule({ 13 | type: 'string-int', 14 | }, null); 15 | 16 | expect(numberRule.test(2)).toBe(false); 17 | }); 18 | 19 | test('type test int number is true', () => { 20 | const numberRule = new Rule({ 21 | type: 'string-int', 22 | }, null); 23 | 24 | expect(numberRule.test('2')).toBe(true); 25 | }); 26 | 27 | 28 | test('type test max string-number is true', () => { 29 | const numberRule = new Rule({ 30 | type: 'string-int', 31 | max: 5, 32 | }, null); 33 | 34 | expect(numberRule.test('2')).toBe(true); 35 | }); 36 | 37 | test('type test max string-number is false', () => { 38 | const numberRule = new Rule({ 39 | type: 'string-int', 40 | max: 5, 41 | }, null); 42 | 43 | expect(numberRule.test('7')).toBe(false); 44 | }); 45 | 46 | 47 | test('type test min string-number is false', () => { 48 | const numberRule = new Rule({ 49 | type: 'string-int', 50 | min: 5, 51 | }, null); 52 | 53 | expect(numberRule.test('2')).toBe(false); 54 | }); 55 | 56 | test('type test min string-number is true', () => { 57 | const numberRule = new Rule({ 58 | type: 'string-int', 59 | min: 5, 60 | }, null); 61 | 62 | expect(numberRule.test('7')).toBe(true); 63 | }); 64 | 65 | 66 | test('type test string-number is equal to 5', () => { 67 | const numberRule = new Rule({ 68 | type: 'string-int', 69 | equal: 5, 70 | }, null); 71 | 72 | expect(numberRule.test('5')).toBe(true); 73 | }); 74 | 75 | 76 | test('type test string-number is not equal to 5', () => { 77 | const numberRule = new Rule({ 78 | type: 'string-int', 79 | equal: 5, 80 | }, null); 81 | 82 | expect(numberRule.test('7')).toBe(false); 83 | }); 84 | -------------------------------------------------------------------------------- /test/Rule/date.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('throws error when test doesn\'t exist', () => { 4 | expect(() => { 5 | const rule = new Rule({ 6 | type: 'date', 7 | dummyTest: /^The/g, 8 | }, null); 9 | rule.test(); 10 | }).toThrow(); 11 | }); 12 | 13 | 14 | test('returns false when not date', () => { 15 | const dateRule = new Rule({ 16 | type: 'date', 17 | }, null); 18 | expect(dateRule.test('the quick brown fox')).toBe(false); 19 | }); 20 | 21 | test('returns true when date is after a specific date', () => { 22 | const date = new Date(2018, 11, 1); 23 | const test = new Date(2018, 11, 2); 24 | const dateRule = new Rule({ 25 | type: 'date', 26 | after: date, 27 | }, null); 28 | 29 | expect(dateRule.test(test)).toBe(true); 30 | }); 31 | 32 | 33 | test('returns true when date is before a specific date', () => { 34 | const date = new Date(2018, 11, 3); 35 | const test = new Date(2018, 11, 2); 36 | const dateRule = new Rule({ 37 | type: 'date', 38 | before: date, 39 | }, null); 40 | 41 | expect(dateRule.test(test)).toBe(true); 42 | }); 43 | 44 | test('returns true when date is between two dates', () => { 45 | const date1 = new Date(2018, 11, 3); 46 | const date2 = new Date(2018, 11, 7); 47 | const test = new Date(2018, 11, 5); 48 | const dateRule = new Rule({ 49 | type: 'date', 50 | between: [date1, date2], 51 | }, null); 52 | 53 | expect(dateRule.test(test)).toBe(true); 54 | }); 55 | 56 | 57 | test('returns true when two dates are equal', () => { 58 | const date1 = new Date(2018, 11, 3); 59 | const dateRule = new Rule({ 60 | type: 'date', 61 | equal: date1, 62 | }, null); 63 | 64 | expect(dateRule.test(date1)).toBe(true); 65 | }); 66 | 67 | test('returns true when date is one of array', () => { 68 | const date1 = new Date(2018, 11, 3); 69 | const dateRule = new Rule({ 70 | type: 'date', 71 | oneOf: [new Date(2018, 11, 3), new Date()], 72 | }, null); 73 | 74 | expect(dateRule.test(date1)).toBe(true); 75 | }); 76 | -------------------------------------------------------------------------------- /test/Rule/string-float.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type test not number is false', () => { 4 | const numberRule = new Rule({ 5 | type: 'string-float', 6 | }, null); 7 | 8 | expect(numberRule.test([])).toBe(false); 9 | }); 10 | 11 | test('type test float number is false', () => { 12 | const numberRule = new Rule({ 13 | type: 'string-float', 14 | }, null); 15 | 16 | expect(numberRule.test(2.01)).toBe(false); 17 | }); 18 | 19 | test('type test float number is true', () => { 20 | const numberRule = new Rule({ 21 | type: 'string-float', 22 | }, null); 23 | 24 | expect(numberRule.test('2.01')).toBe(true); 25 | }); 26 | 27 | 28 | test('type test max string-number is true', () => { 29 | const numberRule = new Rule({ 30 | type: 'string-float', 31 | max: 5, 32 | }, null); 33 | 34 | expect(numberRule.test('2.01')).toBe(true); 35 | }); 36 | 37 | test('type test max string-number is false', () => { 38 | const numberRule = new Rule({ 39 | type: 'string-float', 40 | max: 5, 41 | }, null); 42 | 43 | expect(numberRule.test('7.1')).toBe(false); 44 | }); 45 | 46 | 47 | test('type test min string-number is false', () => { 48 | const numberRule = new Rule({ 49 | type: 'string-float', 50 | min: 5, 51 | }, null); 52 | 53 | expect(numberRule.test('2.1')).toBe(false); 54 | }); 55 | 56 | test('type test min string-number is true', () => { 57 | const numberRule = new Rule({ 58 | type: 'string-float', 59 | min: 5, 60 | }, null); 61 | 62 | expect(numberRule.test('7.1')).toBe(true); 63 | }); 64 | 65 | 66 | test('type test string-number is equal to 5', () => { 67 | const numberRule = new Rule({ 68 | type: 'string-float', 69 | equal: 5.1, 70 | }, null); 71 | 72 | expect(numberRule.test('5.1')).toBe(true); 73 | }); 74 | 75 | 76 | test('type test string-number is not equal to 5', () => { 77 | const numberRule = new Rule({ 78 | type: 'string-float', 79 | equal: 5, 80 | }, null); 81 | 82 | expect(numberRule.test('7.1')).toBe(false); 83 | }); 84 | -------------------------------------------------------------------------------- /test/Rule/array.test.js: -------------------------------------------------------------------------------- 1 | const { Rule, Validator } = require('../../src'); 2 | 3 | test('returns true when its an array', () => { 4 | const arrayRule = new Rule({ 5 | type: 'array', 6 | }, null); 7 | expect(arrayRule.test([])).toBe(true); 8 | 9 | // eslint-disable-next-line 10 | expect(arrayRule.test(new Array())).toBe(true); 11 | }); 12 | 13 | test('returns true when aray is not empty', () => { 14 | const elemntsRule = new Rule({ 15 | type: 'int', 16 | }); 17 | 18 | const arrayRule = new Rule({ 19 | type: 'array', 20 | of: elemntsRule, 21 | notEmpty: true, 22 | }, null); 23 | expect(arrayRule.test([])).toBe(false); 24 | }); 25 | 26 | test('returns true when all elems verify the rule', () => { 27 | const elemntsRule = new Rule({ 28 | type: 'string', 29 | maxLength: 2, 30 | }); 31 | 32 | const arrayRule = new Rule({ 33 | type: 'array', 34 | of: elemntsRule, 35 | }, null); 36 | expect(arrayRule.test(['1', '2', '1'])).toBe(true); 37 | }); 38 | 39 | 40 | test('returns true when array contains 3 elems', () => { 41 | const arrayRule = new Rule({ 42 | type: 'array', 43 | length: 3, 44 | }, null); 45 | expect(arrayRule.test(['1', '2', '1'])).toBe(true); 46 | }); 47 | 48 | test('returns false when array contains elements that dont match', () => { 49 | const arrayRule = new Rule({ 50 | type: 'array', 51 | of: new Rule('int'), 52 | }, null); 53 | expect(arrayRule.test(['1', '2', '1'])).toBe(false); 54 | }); 55 | 56 | test('returns true when elements verify the validator', () => { 57 | const users = new Validator({ 58 | name: new Rule('string'), 59 | age: new Rule('int'), 60 | }); 61 | 62 | const arrayRule = new Rule({ 63 | type: 'array', 64 | of: users, 65 | }); 66 | expect(arrayRule.test([{ name: 'Meee', age: 23 }])).toBe(true); 67 | }); 68 | 69 | test('returns true when array is one of validator', () => { 70 | const arrayRule = new Rule({ 71 | type: 'array', 72 | oneOf: [[{ name: 'Meee', age: 23 }], [{ name: 'haaa', age: 27 }]], 73 | }); 74 | expect(arrayRule.test([{ name: 'Meee', age: 23 }])).toBe(true); 75 | }); 76 | -------------------------------------------------------------------------------- /test/Rule/password.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type test not password is false', () => { 4 | const passwordRule = new Rule({ 5 | type: 'password', 6 | }, null); 7 | 8 | expect(passwordRule.test([])).toBe(false); 9 | }); 10 | 11 | 12 | test('number', () => { 13 | const passwordRule = new Rule({ 14 | type: 'password', 15 | numbers: 5, 16 | }, null); 17 | 18 | expect(passwordRule.test('Aa6z6666')).toBe(true); 19 | }); 20 | 21 | test('uppercase', () => { 22 | const passwordRule = new Rule({ 23 | type: 'password', 24 | uppercase: 5, 25 | }, null); 26 | 27 | expect(passwordRule.test('@_6b6ddcdA')).toBe(false); 28 | }); 29 | 30 | 31 | test('matcesOneOf', () => { 32 | const passwordRule = new Rule({ 33 | type: 'password', 34 | matchesOneOf: ['@', '_', '-'], 35 | }, null); 36 | 37 | expect(passwordRule.test('AAbd_AdcdAA')).toBe(true); 38 | }); 39 | 40 | test('matcesOneOf without mach', () => { 41 | const passwordRule = new Rule({ 42 | type: 'password', 43 | matchesOneOf: ['@', '_', '-'], 44 | }, null); 45 | 46 | expect(passwordRule.test('AAbdAdcdAA')).toBe(false); 47 | }); 48 | 49 | test('matchesAllOf', () => { 50 | const passwordRule = new Rule({ 51 | type: 'password', 52 | matchesAllOf: ['@', '_', '-'], 53 | }, null); 54 | 55 | expect(passwordRule.test('A@_-AbdAdcdAA')).toBe(true); 56 | }); 57 | 58 | test('matchesAllOf false', () => { 59 | const passwordRule = new Rule({ 60 | type: 'password', 61 | matchesAllOf: ['@', '_', '-'], 62 | }, null); 63 | 64 | expect(passwordRule.test('A@-AbdAdcdAA')).toBe(false); 65 | }); 66 | 67 | test('specialChars false', () => { 68 | const passwordRule = new Rule({ 69 | type: 'password', 70 | specialChars: 2, 71 | }, null); 72 | 73 | expect(passwordRule.test('A@AbdAdcdAA')).toBe(false); 74 | }); 75 | 76 | test('good password', () => { 77 | const passwordRule = new Rule({ 78 | type: 'password', 79 | minLength: 8, 80 | uppercase: 1, 81 | numbers: 1, 82 | matchesOneOf: ['@', '_', '-', '.', '!'], 83 | }, null); 84 | 85 | expect(passwordRule.test('@_-bddcd6A')).toBe(true); 86 | }); 87 | -------------------------------------------------------------------------------- /test/Rule/string.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type string returns true when a string', () => { 4 | const strRule = new Rule({ 5 | type: 'string', 6 | }, null); 7 | 8 | expect(strRule.test('dqsdqsd')).toBe(true); 9 | expect(strRule.test(String('dqsdqsd'))).toBe(true); 10 | }); 11 | 12 | 13 | test('type string returns false when not string', () => { 14 | const strRule = new Rule({ 15 | type: 'string', 16 | }, null); 17 | expect(strRule.test({})).toBe(false); 18 | }); 19 | 20 | test('type string returns true when matches a REGEX', () => { 21 | const strRule = new Rule({ 22 | type: 'string', 23 | match: /^The/g, 24 | }, null); 25 | expect(strRule.test('The quick brown fox')).toBe(true); 26 | }); 27 | 28 | test('type string returns false when doesen\'t matches a REGEX', () => { 29 | const strRule = new Rule({ 30 | type: 'string', 31 | match: /^The/g, 32 | }, null); 33 | expect(strRule.test('the quick brown fox')).toBe(false); 34 | }); 35 | 36 | test('throws error when test doesn\'t exist', () => { 37 | expect(() => { 38 | const strRule = new Rule({ 39 | type: 'string', 40 | dummyTest: /^The/g, 41 | }, null); 42 | strRule.test('the quick brown fox'); 43 | }).toThrow(); 44 | }); 45 | 46 | test('type string returns true when string not empty', () => { 47 | const strRule = new Rule({ 48 | type: 'string', 49 | notEmpty: true, 50 | }, null); 51 | expect(strRule.test('the quick brown fox')).toBe(true); 52 | }); 53 | 54 | test('type string returns false when string empty', () => { 55 | const strRule = new Rule({ 56 | type: 'string', 57 | notEmpty: true, 58 | }, null); 59 | expect(strRule.test('')).toBe(false); 60 | }); 61 | 62 | test('type string returns true when two strings are equal', () => { 63 | const strRule = new Rule({ 64 | type: 'string', 65 | notEmpty: true, 66 | equal: 'hello', 67 | }, null); 68 | expect(strRule.test('hello')).toBe(true); 69 | }); 70 | 71 | test('type string returns true when string minLength', () => { 72 | const strRule = new Rule({ 73 | type: 'string', 74 | notEmpty: true, 75 | minLength: 2, 76 | }, null); 77 | expect(strRule.test('hello')).toBe(true); 78 | }); 79 | -------------------------------------------------------------------------------- /test/Rule/int.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type test not int is false', () => { 4 | const intRule = new Rule({ 5 | type: 'int', 6 | }, null); 7 | 8 | expect(intRule.test([])).toBe(false); 9 | }); 10 | 11 | test('type test', () => { 12 | const intRule = new Rule({ 13 | type: 'int', 14 | }, null); 15 | 16 | expect(intRule.test(2)).toBe(true); 17 | }); 18 | 19 | test('max is true if < 100', () => { 20 | const intRule = new Rule({ 21 | type: 'int', 22 | max: 100, 23 | }, null); 24 | 25 | expect(intRule.test(99)).toBe(true); 26 | }); 27 | 28 | test('max is false if > 100', () => { 29 | const intRule = new Rule({ 30 | type: 'int', 31 | max: 100, 32 | }, null); 33 | 34 | expect(intRule.test(101)).toBe(false); 35 | }); 36 | 37 | test('equal returns true if 100', () => { 38 | const intRule = new Rule({ 39 | type: 'int', 40 | equal: 100, 41 | }, null); 42 | 43 | expect(intRule.test(100)).toBe(true); 44 | }); 45 | 46 | 47 | test('custom rulle should return true', () => { 48 | const intRule = new Rule({ 49 | type: 'int', 50 | custom: val => val % 2 === 0, 51 | }, null); 52 | 53 | expect(intRule.test(4)).toBe(true); 54 | }); 55 | 56 | test('custom rulle should return false', () => { 57 | const intRule = new Rule({ 58 | type: 'int', 59 | custom: val => val % 2 === 0, 60 | }, null); 61 | 62 | expect(intRule.test(3)).toBe(false); 63 | }); 64 | 65 | test('mixng rulles returns true', () => { 66 | const intRule = new Rule({ 67 | type: 'int', 68 | max: 50, 69 | min: 5, 70 | custom: val => val % 2 === 0, 71 | }, null); 72 | 73 | expect(intRule.test(6)).toBe(true); 74 | }); 75 | 76 | test('mixng rulles returns false', () => { 77 | const intRule = new Rule({ 78 | type: 'int', 79 | max: 50, 80 | min: 5, 81 | custom: val => val % 2 === 0, 82 | }, null); 83 | 84 | expect(intRule.test(1)).toBe(false); 85 | }); 86 | 87 | test('throws error when test doesn\'t exist', () => { 88 | expect(() => { 89 | const intRule = new Rule({ 90 | type: 'int', 91 | test: false, 92 | }, null); 93 | intRule.test(3); 94 | }).toThrow(); 95 | }); 96 | 97 | test('returns true if optional set to true and patram undefined', () => { 98 | const intRule = new Rule({ 99 | type: 'int', 100 | optional: true, 101 | }, null); 102 | expect(intRule.test()).toBe(true); 103 | }); 104 | 105 | test('returns false if optional set to true and patram dont match', () => { 106 | const intRule = new Rule({ 107 | type: 'int', 108 | optional: true, 109 | }, null); 110 | expect(intRule.test('hello')).toBe(false); 111 | }); 112 | 113 | test('returns true if optional set to true and patram match', () => { 114 | const intRule = new Rule({ 115 | type: 'int', 116 | optional: true, 117 | }, null); 118 | expect(intRule.test(150)).toBe(true); 119 | }); 120 | 121 | test('returns true if optional set to false and patram match', () => { 122 | const intRule = new Rule({ 123 | type: 'int', 124 | optional: false, 125 | }, null); 126 | expect(intRule.test(150)).toBe(true); 127 | }); 128 | 129 | test('returns false if optional set to false and patram dont match', () => { 130 | const intRule = new Rule({ 131 | type: 'int', 132 | optional: false, 133 | }, null); 134 | expect(intRule.test('150')).toBe(false); 135 | }); 136 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const isArray = arr => Array.isArray(arr); 2 | 3 | const isString = str => typeof str === 'string' || str instanceof String; 4 | 5 | const isInt = val => Number.isInteger(val); 6 | 7 | const isFunction = func => func !== null && typeof func === 'function'; 8 | 9 | /** 10 | * Regex that validates if a string is a valid url 11 | */ 12 | const URL_REGEX = /^\(?(?:(http|https|ftp):\/\/)?(?:((?:[^\W\s]|\.|-|[:]{1})+)@{1})?((?:www.)?(?:[^\W\s]|\.|-)+[\.][^\W\s]{2,4}|localhost(?=\/)|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?::(\d*))?([\/]?[^\s\?]*[\/]{1})*(?:\/?([^\s\n\?\[\]\{\}\#]*(?:(?=\.)){1}|[^\s\n\?\[\]\{\}\.\#]*)?([\.]{1}[^\s\?\#]*)?)?(?:\?{1}([^\s\n\#\[\]]*))?([\#][^\s\n]*)?\)?/; // eslint-disable-line 13 | 14 | const isObject = obj => obj !== null && typeof obj === 'object'; 15 | 16 | const AND = (v1, v2) => v1 && v2; 17 | const OR = (v1, v2) => v1 || v2; 18 | 19 | /** 20 | * Checks if two bojects are loosly equal 21 | * @param {any} a first object 22 | * @param {any} b second object 23 | * 24 | * @return {boolean} 25 | */ 26 | const looseEqual = (a, b) => { 27 | if (a === b) return true; 28 | 29 | const isObjectA = isObject(a); 30 | const isObjectB = isObject(b); 31 | const isArrayA = isArray(a); 32 | const isArrayB = isArray(b); 33 | let ret = false; 34 | 35 | if (isArrayA && isArrayB) { 36 | ret = a.length === b.length && a.every((e, i) => looseEqual(e, b[i])); 37 | } 38 | 39 | if (a instanceof Date && b instanceof Date) { 40 | ret = a.getTime() === b.getTime(); 41 | } 42 | 43 | if (isObjectA && isObjectB) { 44 | const keysA = Object.keys(a); 45 | const keysB = Object.keys(b); 46 | ret = keysA.length === keysB.length && keysA.every(key => looseEqual(a[key], b[key])); 47 | } 48 | 49 | return ret; 50 | }; 51 | 52 | 53 | function mapFirstArgument(f, map) { 54 | return (...args) => { 55 | const arg = args; 56 | arg[0] = map(arg[0]); 57 | return f(...arg); 58 | }; 59 | } 60 | 61 | function mergeRule(rule1, rule2, mapFunction) { 62 | const keys = Object.keys(rule2); 63 | const mappedCopy = {}; 64 | keys.forEach((key) => { 65 | mappedCopy[key] = mapFirstArgument(rule2[key], mapFunction); 66 | }); 67 | return { 68 | ...rule1, 69 | ...mappedCopy, 70 | type: val => rule1.type(val) && rule2.type(mapFunction(val)), 71 | }; 72 | } 73 | 74 | function flattenObject(ob) { 75 | const toReturn = {}; 76 | /* eslint-disable */ 77 | for (const i in ob) { 78 | if (ob[i] && ob[i].constructor === Object) { 79 | const flatObject = flattenObject(ob[i]); 80 | for (const x in flatObject) { 81 | toReturn[`${i}.${x}`] = flatObject[x]; 82 | } 83 | } else { 84 | toReturn[i] = ob[i]; 85 | } 86 | } 87 | /* eslint-enable */ 88 | return toReturn; 89 | } 90 | 91 | function arrayContainsAll(a, b) { 92 | for (let i = 0; i < a.length; i += 1) { 93 | let contains = false; 94 | for (let j = 0; j < b.length; j += 1) { 95 | if (a[i] === b[j]) { 96 | contains = true; 97 | } 98 | } 99 | if (contains === false) { 100 | return false; 101 | } 102 | } 103 | return true; 104 | } 105 | 106 | module.exports = { 107 | isArray, 108 | isString, 109 | isFunction, 110 | isObject, 111 | URL_REGEX, 112 | looseEqual, 113 | AND, 114 | OR, 115 | isInt, 116 | mapFirstArgument, 117 | mergeRule, 118 | flattenObject, 119 | arrayContainsAll, 120 | }; 121 | -------------------------------------------------------------------------------- /test/Rule/number.test.js: -------------------------------------------------------------------------------- 1 | const { Rule } = require('../../src'); 2 | 3 | test('type test not number is false', () => { 4 | const numberRule = new Rule({ 5 | type: 'number', 6 | }, null); 7 | 8 | expect(numberRule.test([])).toBe(false); 9 | }); 10 | 11 | test('type test', () => { 12 | const numberRule = new Rule({ 13 | type: 'number', 14 | }, null); 15 | 16 | expect(numberRule.test(2)).toBe(true); 17 | }); 18 | 19 | test('max is true if < 100', () => { 20 | const numberRule = new Rule({ 21 | type: 'number', 22 | max: 100, 23 | }, null); 24 | 25 | expect(numberRule.test(99)).toBe(true); 26 | }); 27 | 28 | test('max is false if > 100', () => { 29 | const numberRule = new Rule({ 30 | type: 'number', 31 | max: 100, 32 | }, null); 33 | 34 | expect(numberRule.test(101)).toBe(false); 35 | }); 36 | 37 | test('equal returns true if 100', () => { 38 | const numberRule = new Rule({ 39 | type: 'number', 40 | equal: 100, 41 | }, null); 42 | 43 | expect(numberRule.test(100)).toBe(true); 44 | }); 45 | 46 | 47 | test('custom rulle should return true', () => { 48 | const numberRule = new Rule({ 49 | type: 'number', 50 | custom: val => val % 2 === 0, 51 | }, null); 52 | 53 | expect(numberRule.test(4)).toBe(true); 54 | }); 55 | 56 | test('custom rulle should return false', () => { 57 | const numberRule = new Rule({ 58 | type: 'number', 59 | custom: val => val % 2 === 0, 60 | }, null); 61 | 62 | expect(numberRule.test(3)).toBe(false); 63 | }); 64 | 65 | test('mixng rulles returns true', () => { 66 | const numberRule = new Rule({ 67 | type: 'number', 68 | max: 50, 69 | min: 5, 70 | custom: val => val % 2 === 0, 71 | }, null); 72 | 73 | expect(numberRule.test(6)).toBe(true); 74 | }); 75 | 76 | test('mixng rulles returns false', () => { 77 | const numberRule = new Rule({ 78 | type: 'number', 79 | max: 50, 80 | min: 5, 81 | custom: val => val % 2 === 0, 82 | }, null); 83 | 84 | expect(numberRule.test(1)).toBe(false); 85 | }); 86 | 87 | test('throws error when test doesn\'t exist', () => { 88 | expect(() => { 89 | const numberRule = new Rule({ 90 | type: 'number', 91 | test: false, 92 | }, null); 93 | numberRule.test(3); 94 | }).toThrow(); 95 | }); 96 | 97 | test('returns true if optional set to true and patram undefined', () => { 98 | const numberRule = new Rule({ 99 | type: 'number', 100 | optional: true, 101 | }, null); 102 | expect(numberRule.test()).toBe(true); 103 | }); 104 | 105 | test('returns false if optional set to true and patram dont match', () => { 106 | const numberRule = new Rule({ 107 | type: 'number', 108 | optional: true, 109 | }, null); 110 | expect(numberRule.test('hello')).toBe(false); 111 | }); 112 | 113 | test('returns true if optional set to true and patram match', () => { 114 | const numberRule = new Rule({ 115 | type: 'number', 116 | optional: true, 117 | }, null); 118 | expect(numberRule.test(150)).toBe(true); 119 | }); 120 | 121 | test('returns true if optional set to false and patram match', () => { 122 | const numberRule = new Rule({ 123 | type: 'number', 124 | optional: false, 125 | }, null); 126 | expect(numberRule.test(150)).toBe(true); 127 | }); 128 | 129 | test('returns false if optional set to false and patram dont match', () => { 130 | const numberRule = new Rule({ 131 | type: 'number', 132 | optional: false, 133 | }, null); 134 | expect(numberRule.test('150')).toBe(false); 135 | }); 136 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // abstract interfaces for extending 2 | interface basicRule { 3 | type: string, 4 | optional?: boolean, 5 | oneOf?: Array, 6 | custom?: (arg: any, object: this) => boolean, 7 | } 8 | 9 | interface basicNumberRule extends basicRule { 10 | min?: number, 11 | max?: number, 12 | equal?: number, 13 | } 14 | 15 | interface basicStringRule extends basicRule { 16 | minLength?: number, 17 | maxLength?: number, 18 | match?: RegExp, 19 | notEmpty?: boolean, 20 | } 21 | 22 | interface simpleStringRule extends basicStringRule { 23 | equal?: string, 24 | } 25 | 26 | interface complexStringRule extends basicStringRule { 27 | equal?: T, 28 | } 29 | 30 | 31 | // interfaces for implementation 32 | interface intRule extends basicNumberRule { 33 | type: 'int', 34 | } 35 | 36 | interface booleanRule extends basicRule { 37 | type: 'boolean', 38 | toBe?: boolean, 39 | } 40 | 41 | interface stringRule extends simpleStringRule { 42 | type: 'string', 43 | } 44 | 45 | interface emailRule extends simpleStringRule { 46 | type: 'email', 47 | user?: (user: string) => boolean, 48 | domain?: (domain: string) => boolean, 49 | } 50 | 51 | interface passwordRule extends simpleStringRule { 52 | type: 'password', 53 | uppercase?: number, 54 | specialChars?: number, 55 | numbers?: number, 56 | matchesOneOf?: Array, 57 | matchesAllOf?: Array, 58 | } 59 | 60 | interface urlRule extends simpleStringRule { 61 | type: 'url', 62 | protocol?: (protocol: string) => boolean, 63 | domain?: (domain: string) => boolean, 64 | } 65 | 66 | interface dateRule extends basicRule { 67 | type: 'date', 68 | after?: Date, 69 | before?: Date, 70 | between?: [Date, Date], 71 | equal?: Date, 72 | } 73 | 74 | interface floatRule extends basicNumberRule { 75 | type: 'float', 76 | } 77 | 78 | interface arrayRule extends basicRule { 79 | type: 'array', 80 | of?: Rule, 81 | notEmpty?: boolean, 82 | length?: number, 83 | } 84 | 85 | interface functionRule extends basicRule { 86 | type: 'function', 87 | result?: { 88 | of: any, 89 | toBe: Rule, 90 | }, 91 | } 92 | 93 | interface stringIntRule extends complexStringRule { 94 | type: 'string-int', 95 | min?: number, 96 | max?: number, 97 | } 98 | 99 | interface stringFloatRule extends complexStringRule { 100 | type: 'string-float', 101 | min?: number, 102 | max?: number, 103 | } 104 | 105 | interface stringDateRule extends complexStringRule { 106 | type: 'string-date', 107 | after?: Date, 108 | before?: Date, 109 | between?: [Date, Date], 110 | } 111 | 112 | interface stringBooleanRule extends complexStringRule { 113 | type: 'string-boolean', 114 | toBe?: boolean, 115 | } 116 | 117 | // There is no regex-validated string type, so make multipleRule is not easy 118 | // interface multipleRule {} 119 | 120 | type ruleType = intRule 121 | |booleanRule 122 | |stringRule 123 | |emailRule 124 | |passwordRule 125 | |urlRule 126 | |dateRule 127 | |floatRule 128 | |arrayRule 129 | |functionRule 130 | |stringIntRule 131 | |stringFloatRule 132 | |stringDateRule 133 | |stringBooleanRule; 134 | 135 | export declare class Rule { 136 | public constructor (rule: ruleType|string, error?: string|null|undefined); 137 | 138 | public static addCustom(name: string, rules: {[key: string]: (val: any, arg?: any) => boolean}): void; 139 | 140 | public test(value: any): boolean; 141 | } 142 | 143 | export declare class Validator { 144 | public constructor (fields: {[key: string]: Rule}); 145 | 146 | public test(object: {[key: string]: any}): boolean; 147 | public testAll(objects: Array< {[key: string]: any} >): number; 148 | public getErrors(object: {[key: string]: any}): Array; 149 | } 150 | -------------------------------------------------------------------------------- /test/Validator/Validator.test.js: -------------------------------------------------------------------------------- 1 | const { Rule, Validator } = require('../../src'); 2 | 3 | test('test the hole object to be true', () => { 4 | const vComplexe = new Validator({ 5 | age: new Rule({ type: 'int', min: 18, max: 99 }), 6 | dateOfBirth: new Rule({ type: 'date' }), 7 | array: new Rule({ type: 'array', of: new Rule({ type: 'string' }) }), 8 | }); 9 | 10 | expect(vComplexe.test({ 11 | age: 26, 12 | dateOfBirth: new Date(1995, 10, 3), 13 | array: ['1'], 14 | })).toBe(true); 15 | }); 16 | 17 | test('test the hole object to be false extra values', () => { 18 | const vComplexe = new Validator({ 19 | age: new Rule({ type: 'int', min: 18, max: 99 }), 20 | dateOfBirth: new Rule({ type: 'date' }), 21 | array: new Rule({ type: 'array', of: new Rule({ type: 'string' }) }), 22 | }); 23 | 24 | expect(vComplexe.test({ 25 | age: 26, 26 | dateOfBirth: new Date(1995, 10, 3), 27 | array: ['1'], 28 | extraValue: '', 29 | })).toBe(false); 30 | }); 31 | 32 | test('test the hole object to be false', () => { 33 | const vComplexe = new Validator({ 34 | age: new Rule({ type: 'int', min: 18, max: 99 }), 35 | dateOfBirth: new Rule({ type: 'date' }), 36 | array: new Rule({ type: 'array', of: new Rule({ type: 'string' }) }), 37 | }); 38 | 39 | expect(vComplexe.test({ 40 | age: 26, 41 | dateOfBirth: new Date(1995, 10, 3), 42 | array: [1], 43 | })).toBe(false); 44 | }); 45 | 46 | test('test custom', () => { 47 | function f(age, _, object) { 48 | const expectedAge = Math.floor((new Date() - object.dateOfBirth) / 3.154e+10); 49 | if (age === expectedAge) { 50 | return true; 51 | } 52 | return false; 53 | } 54 | const vComplexe = new Validator({ 55 | age: new Rule({ 56 | type: 'int', min: 18, max: 99, custom: f, 57 | }), 58 | dateOfBirth: new Rule({ type: 'date' }), 59 | }); 60 | 61 | const age = Math.floor((new Date() - new Date(1995, 10, 3)) / 3.154e+10); 62 | 63 | expect(vComplexe.test({ 64 | age, 65 | dateOfBirth: new Date(1995, 10, 3), 66 | })).toBe(true); 67 | }); 68 | 69 | 70 | test('test getErrors must return an array of errors', () => { 71 | const vComplexe = new Validator({ 72 | age: new Rule({ 73 | type: 'int', min: 18, max: 99, 74 | }, 'age must be integer and between 18 and 99'), 75 | dateOfBirth: new Rule({ type: 'date' }, 'date must be a date'), 76 | name: new Rule({ type: 'string' }, (key, value) => `${key} must be a string, ${value} is not a string`), 77 | }); 78 | 79 | expect(vComplexe.getErrors({ 80 | age: 16, 81 | dateOfBirth: new Date(), 82 | name: 1, 83 | })).toEqual(['age must be integer and between 18 and 99', 'name must be a string, 1 is not a string']); 84 | }); 85 | 86 | test('test getErrors return array of errors even with object as rule error', () => { 87 | const vComplexe = new Validator({ 88 | age: new Rule({ 89 | type: 'int', min: 18, max: 99, 90 | }, { 91 | type: 'age must be an integer.', 92 | min: (key, value) => `age must be greater then 18, ${value} is not enough.`, 93 | }), 94 | dataOfBirth: new Rule({ type: 'date' }, 'date must be a date'), 95 | }); 96 | 97 | expect(vComplexe.getErrors({ 98 | age: 16, 99 | dateOfBirth: 1, 100 | })).toEqual([ 101 | 'dateOfBirth is unexpcted', 102 | 'age must be greater then 18, 16 is not enough.', 103 | 'date must be a date', 104 | ]); 105 | 106 | expect(vComplexe.getErrors({ 107 | age: '16', 108 | dateOfBirth: 1, 109 | })).toEqual([ 110 | 'dateOfBirth is unexpcted', 111 | 'age must be an integer.', 112 | 'date must be a date', 113 | ]); 114 | 115 | expect(vComplexe.getErrors({ 116 | age: 102, // there is no error definition for this key 117 | dateOfBirth: 1, 118 | })).toEqual([ 119 | 'dateOfBirth is unexpcted', 120 | "age doesn't satisfy the max rule", 121 | 'date must be a date', 122 | ]); 123 | }); 124 | 125 | test('test test over an array of values', () => { 126 | const vComplexe = new Validator({ 127 | age: new Rule({ 128 | type: 'int', min: 18, max: 99, 129 | }), 130 | }); 131 | 132 | expect(vComplexe.testAll([{ 133 | age: 19, 134 | }, { 135 | age: 16, 136 | }])).toBe(1); 137 | }); 138 | 139 | test('test over multiple values and return -1', () => { 140 | const vComplexe = new Validator({ 141 | age: new Rule({ 142 | type: 'int', min: 18, max: 99, 143 | }), 144 | }); 145 | 146 | expect(vComplexe.testAll([{ 147 | age: 19, 148 | }, { 149 | age: 20, 150 | }])).toBe(-1); 151 | }); 152 | -------------------------------------------------------------------------------- /src/Rule/index.js: -------------------------------------------------------------------------------- 1 | const { getErrorFromObject, getErrorFromFunctionOrString } = require('./util'); 2 | const { TEST_FUNCTIONS, OPTIONAL } = require('../testFunctions'); 3 | const { AND, OR, isObject } = require('./../util'); 4 | 5 | const OPERATORS = { 6 | '&': AND, 7 | '|': OR, 8 | }; 9 | 10 | /** 11 | * The Rule class validates only one value 12 | * once a rule is created it can be used multiple times 13 | */ 14 | class Rule { 15 | /** 16 | * 17 | * @param {String|Object} obj the rule object describes the tests that are ran by the Rule 18 | * @param {String} error the error returned when the tested input is not correct 19 | */ 20 | constructor(obj, error) { 21 | if (typeof obj === 'string') { 22 | this.rule = { type: obj }; 23 | } else { 24 | this.rule = obj; 25 | } 26 | this.error = error; 27 | this.testEntryObject(); 28 | } 29 | 30 | /** 31 | * 32 | * @param {any} val the value to be tested 33 | * @param {Object|String} obj the error object or string thats showed on error 34 | * @param {String} path the path to the tested value this is used when 35 | * using validator to keep track of the prop value ex: obj.min 36 | * 37 | * @return {boolean} 38 | */ 39 | 40 | test(val, path, obj) { 41 | const types = this.getTypes(); 42 | const operators = this.getRuleOperators(); 43 | let ret = this.testOneRule(val, types[0], path, obj); 44 | 45 | for (let i = 1; i < types.length; i += 1) { 46 | const operator = operators[i] || operators[i - 1]; 47 | ret = operator(ret, this.testOneRule(val, types[i], path, obj)); 48 | } 49 | return ret; 50 | } 51 | 52 | /** 53 | * converts array from string if multiple types given in type 54 | * its the case for exemple int|float 55 | * @private 56 | * @return {[String]} 57 | */ 58 | 59 | getTypes() { 60 | return this.rule.type.split(/[&|]/); 61 | } 62 | 63 | /** 64 | * Returns a list of the operators when multiple types given 65 | * its the case for example int|float 66 | * @private 67 | * @returns {[String]} 68 | */ 69 | getRuleOperators() { 70 | const ret = []; 71 | const operators = this.rule.type.match(/[&|]/g) || '&'; 72 | for (let i = 0; i < operators.length; i += 1) { 73 | ret.push(OPERATORS[operators[i]]); 74 | } 75 | return ret; 76 | } 77 | 78 | /** 79 | * @private 80 | * @param val value to be tested 81 | * @param {String} type the type from getTypes() 82 | * @param {String} path the path to the value if Validator is used 83 | * @param {any} obj full object beeing tested 84 | * 85 | * @returns {boolean} 86 | */ 87 | testOneRule(val, type, path, obj) { 88 | if (Rule.TEST_FUNCTIONS[type].optional(val, this.rule.optional) === true) { 89 | return true; 90 | } 91 | 92 | const keys = Object.keys(this.rule).sort((key) => { 93 | if (key === 'type') return -1; 94 | return 0; 95 | }); 96 | 97 | for (let i = 0; i < keys.length; i += 1) { 98 | const key = keys[i]; 99 | const testFunction = Rule.TEST_FUNCTIONS[type][key]; 100 | 101 | if (testFunction(val, this.rule[key], path, obj) === false && testFunction !== OPTIONAL) { 102 | return false; 103 | } 104 | } 105 | return true; 106 | } 107 | 108 | 109 | getFailingRules(val) { 110 | const keys = Object.keys(this.rule).sort((key) => { 111 | if (key === 'type') return -1; 112 | return 0; 113 | }); 114 | return this.getTypes().reduce((acc, type) => { 115 | if (Rule.TEST_FUNCTIONS[type].optional(val, this.rule.optional) === true) { 116 | return acc; 117 | } 118 | 119 | for (let i = 0; i < keys.length; i += 1) { 120 | const key = keys[i]; 121 | const testFunction = Rule.TEST_FUNCTIONS[type][key]; 122 | 123 | if (testFunction(val, this.rule[key]) === false && testFunction !== OPTIONAL) { 124 | acc.push(key); 125 | break; 126 | } 127 | } 128 | return acc; 129 | }, []); 130 | } 131 | 132 | /** 133 | * Tests the validity of the constructor object 134 | * thows an error if the object is invalid 135 | */ 136 | 137 | testEntryObject() { 138 | if (!this.rule.type) { 139 | throw Error('`type` is required'); 140 | } 141 | const types = this.getTypes(); 142 | types.forEach((type) => { 143 | this.testEntryObjectOneType(type); 144 | }); 145 | } 146 | 147 | /** 148 | * Tests the validity of the constructor object 149 | * thows an error if the object is invalid 150 | * tests if all the keys are valid 151 | */ 152 | 153 | testEntryObjectOneType(type) { 154 | const keys = Object.keys(this.rule); 155 | for (let i = 0; i < keys.length; i += 1) { 156 | const key = keys[i]; 157 | if (!Rule.TEST_FUNCTIONS[type]) { 158 | throw Error(`The \`${type}\` type doesn't exist`); 159 | } 160 | if (!Rule.TEST_FUNCTIONS[type][key]) { 161 | throw new Error(`\`${type}\` doesn't have "${key}" test!`); 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * returns a list of errors if they are present 168 | * @return {[String]} 169 | */ 170 | 171 | getError(path, value) { 172 | if (isObject(this.error)) { 173 | return this.getFailingRules(value) 174 | .map(key => getErrorFromObject(this.error, path, value, key)); 175 | } 176 | return [getErrorFromFunctionOrString(this.error, path, value)]; 177 | } 178 | 179 | /** 180 | * Add custom rule to the Rule class 181 | * @param {String} name the name of the rule 182 | * @param {Function} rule the validation function 183 | */ 184 | static addCustom(name, rule) { 185 | Rule.TEST_FUNCTIONS[name] = rule; 186 | Rule.TEST_FUNCTIONS[name].optional = OPTIONAL; 187 | } 188 | } 189 | 190 | Rule.TEST_FUNCTIONS = TEST_FUNCTIONS; 191 | 192 | module.exports = Rule; 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![forgJs logo](./media/logo.png?raw=true) 2 | 3 | ForgJs is a JavaScript lightweight object validator. Go check the Quick start section and start coding with love :heart: 4 | 5 | ![email](./media/email.gif?raw=true) 6 | 7 | ![password](./media/password.gif?raw=true) 8 | 9 | ![number](./media/number.gif?raw=true) 10 | 11 | ![url](./media/url.gif?raw=true) 12 | 13 | See more live examples [here](https://oussamahamdaoui.github.io/forgjs-examples/) 14 | 15 | # Quick start 16 | 17 | Install it via npm by running `npm i @cesium133/forgjs` 18 | 19 | ## Your first validator 20 | 21 | ```javascript 22 | const { Validator, Rule } = require('@cesium133/forgjs'); 23 | 24 | const emailRule = new Rule({ 25 | type: 'email', 26 | user: user => user === 'dedede', 27 | domain: domain => ['outlook', 'gmail', 'yahoo'].indexOf(domain) !== -1, 28 | }, null); 29 | 30 | const passwordRule = new Rule({ 31 | type: 'password', 32 | minLength: 8, 33 | uppercase: 1, 34 | numbers: 1, 35 | matchesOneOf: ['@', '_', '-', '.', '!'], 36 | }, null); 37 | 38 | const vComplex = new Validator({ 39 | age: new Rule({ type: 'int', min: 18, max: 99 }), 40 | dateOfBirth: new Rule({ type: 'date' }), 41 | array: new Rule({ type: 'array', of: new Rule({ type: 'string' }) }), 42 | email: emailRule, 43 | password: passwordRule 44 | }); 45 | 46 | vComplex.test({ 47 | age: 26, 48 | dateOfBirth: new Date(1995, 10, 3), 49 | array: ['1'], 50 | email: 'dedede@yahoo.fr;', 51 | password: 'ad1_A@@Axs', 52 | }); /// returns true 53 | ``` 54 | 55 | ## Error handling 56 | 57 | You can get custom error messages by doing this: 58 | 59 | ```javascript 60 | const vComplexe = new Validator({ 61 | age: new Rule({ 62 | type: 'int', min: 18, max: 99, 63 | }, 'age must be integer and between 18 and 99'), 64 | dateOfBirth: new Rule({ type: 'date' }, 'date must be a date'), 65 | }); 66 | 67 | vComplexe.getErrors({ 68 | age: 16, 69 | dateOfBirth: 123, 70 | }); // ['age must be integer and between 18 and 99', 'date must be a date'] 71 | 72 | ``` 73 | ## Test over an array 74 | 75 | You can test over multiple arrays using this method: 76 | 77 | ```javascript 78 | const vComplexe = new Validator({ 79 | age: new Rule({ 80 | type: 'int', min: 18, max: 99, 81 | }), 82 | }); 83 | 84 | vComplexe.testAll([{ 85 | age: 19, 86 | }, { 87 | age: 16, 88 | }]); // returns 1 89 | ``` 90 | 91 | # Rules 92 | 93 | A `Rule` object validates a single value, it can be used like this: 94 | 95 | ```javascript 96 | const { Validator, Rule } = require('@cesium133/forgjs'); 97 | const floatRule = new Rule({ 98 | type: 'float', 99 | min: 100, 100 | }, null); 101 | 102 | floatRule.test(2.001); /// returns true; 103 | ``` 104 | 105 | **The only required value is `type`!** 106 | 107 | > You can make a rule by simply passing a string if you only need to check the type : `new Rule('int');` 108 | 109 | ## int 110 | 111 | * min (int) 112 | * max (int) 113 | * equal (int) 114 | 115 | ## boolean 116 | 117 | * toBe (boolean) 118 | 119 | ## string 120 | 121 | * minLength (int) 122 | * maxLength (int) 123 | * equal (string) 124 | * match: (regex) 125 | * notEmpty (bool) 126 | 127 | ## email 128 | 129 | * minLength (int) 130 | * maxLength (int) 131 | * equal (string) 132 | * match: (regex) 133 | * notEmpty (bool) 134 | * user (`function(user)`) 135 | * domain (`function(domain)`) 136 | 137 | ```javascript 138 | const emailRule = new Rule({ 139 | type: 'email', 140 | user: user => user === 'dedede', 141 | domain: domain => ['outlook', 'gmail', 'yahoo'].indexOf(domain) !== -1, 142 | }, null); 143 | 144 | emailRule.test('dedede@gmail.fr'); // returns true 145 | ``` 146 | 147 | ## password 148 | 149 | * minLength (int) 150 | * maxLength (int) 151 | * equal (string) 152 | * match: (regex) 153 | * notEmpty (bool) 154 | * uppercase (int) 155 | * number (int) 156 | * mathesOneOf (Array) 157 | * matchesAllOf (Array) 158 | 159 | ```javascript 160 | const passwordRule = new Rule({ 161 | type: 'password', 162 | minLength: 8, 163 | uppercase: 1, 164 | numbers: 1, 165 | matchesOneOf: ['@', '_', '-', '.', '!'], 166 | }, null); 167 | 168 | passwordRule.test('@_-bddcd6A'); // returns true 169 | ``` 170 | 171 | ## url 172 | 173 | * minLength (int) 174 | * maxLength (int) 175 | * equal (string) 176 | * match: (regex) 177 | * notEmpty (bool) 178 | * protocol (`function(protocol)`) 179 | * domain (`function(domain)`) 180 | 181 | ```javascript 182 | const urlRule = new Rule({ 183 | type: 'url', 184 | protocol: prot => prot === 'https', 185 | domain: domain => domain === 'google.fr', 186 | }, null); 187 | 188 | urlRule.test('https://google.fr'); // returns true 189 | ``` 190 | 191 | ## date 192 | 193 | * after (date) 194 | * before (date) 195 | * between (Array of dates like this [date, date]) 196 | * equal (date) 197 | 198 | ## float 199 | 200 | * min (Number) 201 | * max (Number) 202 | * equal (float) 203 | 204 | ## array 205 | 206 | * of (Rule or Validator object) 207 | * notEmpty (bool) 208 | * length (int) 209 | 210 | The `of` rule checks every element of the array against the rule. 211 | 212 | ## function 213 | 214 | * result 215 | 216 | To explain result, what's better than an example: 217 | 218 | ```javascript 219 | const { Validator, Rule } = require('@cesium133/forgjs'); 220 | 221 | function someFunctionThatReturnsAnInt(int) { 222 | return int * 5; 223 | } 224 | 225 | const functionTest = new Rule({ 226 | type: 'function', 227 | result: { 228 | of: 5, 229 | toBe: new Rule('int'), 230 | }, 231 | }, null); 232 | 233 | functionTest.test(someFunctionThatReturnsAnInt); /// returns true; 234 | 235 | ``` 236 | ## string-int, string-float, string-date, string-boolean 237 | 238 | These types 'inherit' from string, they have both the properties, here are some examples: 239 | 240 | ### string-int 241 | 242 | ```javascript 243 | const stringInt = new Rule({ 244 | type: 'string-int', 245 | minLength: 2, 246 | min: 5, 247 | }, null); 248 | 249 | stringInt.test(2) // returns false 2 is not a string 250 | stringInt.test('2a') // returns false '2a' is not a int 251 | stringInt.test('2.1') // returns false '2.1' is not a int 252 | stringInt.test('5') // returns false length of '5' is smaller than 2 253 | stringInt.test('50') // returns true 254 | ``` 255 | ### string-boolean 256 | 257 | ```javascript 258 | const stringBoolean = new Rule({ 259 | type: 'string-boolean', 260 | toBe: true 261 | }, null); 262 | 263 | stringBoolean.test(true) // returns false true is not a boolean 264 | stringBoolean.test('false') // returns false 'false' is not true 265 | stringBoolean.test('true') // returns true 266 | 267 | ``` 268 | 269 | ```javascript 270 | const stringDate = new Rule({ 271 | type: 'string-date', 272 | after: new Date(2019, 11, 1), 273 | }, null); 274 | 275 | stringDate.test(new Date(2018, 11, 1)) // returns false new Date(2018, 11, 1) is not a string 276 | stringDate.test('some string') // returns false 'some string' is not a valid date 277 | stringDate.test('2018-12-17') // returns false '2018-12-17' is not after new Date(2019, 11, 1) 278 | stringDate.test('2020-01-01') // returns true 279 | ``` 280 | 281 | **Forgjs tries to cast the value to the right type before passing it to the validation function please be careful!** 282 | 283 | Here is an example where Javascript behaviour makes the test wrong: 284 | 285 | ```javascript 286 | const stringDate = new Rule({ 287 | type: 'string-date', 288 | equal: new Date(2019, 10, 1), // month in js strart at 0 289 | }, null); 290 | 291 | stringDate.test('2019-11-01') // returns false 292 | stringDate.test('2019-11-01T00:00') // returns true 293 | 294 | 295 | // this is because: 296 | 297 | new Date(2019, 10, 1) - new Date('2019-11-01') // equals 3600000 thats exactly 1 hour 298 | 299 | new Date(2019, 10, 1) - new Date('2019-11-01T00:00') // equals 0 300 | 301 | ``` 302 | 303 | ## Multiple types 304 | 305 | You can check for multiple types with `OR` or `AND` operators like this: 306 | 307 | ```javascript 308 | const intRule = new Rule({ 309 | type: 'int|float|number', 310 | }, null); 311 | 312 | intRule.test(2) // returns true 313 | ``` 314 | 315 | This means the test should verify the `int`, `float` or `number` rule 316 | 317 | ```javascript 318 | const intRule = new Rule({ 319 | type: 'int&number', 320 | }, null); 321 | intRule.test(2.1); // returns false 322 | ``` 323 | 324 | The result doesn't match the `int` rule 325 | 326 | ## Common properties 327 | 328 | Every type has these properties: 329 | 330 | * optional 331 | * custom 332 | * oneOf 333 | 334 | ### optional 335 | 336 | If optional is set to `true` the element is optional and an `undefined` value is considered correct. 337 | Example: 338 | 339 | ```javascript 340 | const { Validator, Rule } = require('@cesium133/forgjs'); 341 | 342 | const intRule = new Rule({ 343 | type: 'int', 344 | optional: true, 345 | }, null); 346 | intRule.test(); // returns true 347 | ``` 348 | 349 | ### custom 350 | 351 | Custom allows you to write your own rule, an example is better than a long explanation: 352 | 353 | ```javascript 354 | const { Validator, Rule } = require('@cesium133/forgjs'); 355 | 356 | function isCorrectAge(age, object) { 357 | if (age === Math.floor((new Date() - object.dateOfBirth) / 1000 / 60 / 60 / 24 / 30 / 12)) { 358 | return true; 359 | } 360 | return false; 361 | } 362 | const vComplexe = new Validator({ 363 | age: new Rule({ 364 | type: 'int', min: 18, max: 99, custom: isCorrectAge, 365 | }), 366 | dateOfBirth: new Rule({ type: 'date' }), 367 | }); 368 | 369 | vComplexe.test({ 370 | age: 23, 371 | dateOfBirth: new Date(1995, 10, 3), 372 | array: ['1'], 373 | }); // returns true 374 | 375 | ``` 376 | ### oneOf 377 | 378 | One of checks if the element is in a array 379 | ```javascript 380 | const floatRule = new Rule({ 381 | type: 'float', 382 | oneOf: [3.5, 100.1, 7.2, 0.1], 383 | }, null); 384 | floatRule.test(100.1); // returns true 385 | ``` 386 | 387 | # Make a new type 388 | 389 | Creating a new type is done using the Rule class like this: 390 | 391 | ```javascript 392 | const { Validator, Rule } = require('@cesium133/forgjs'); 393 | 394 | Rule.addCustom('customInteger', { 395 | min: (val, min) => val - min > 0, 396 | max: (val, max) => val - max < 0, 397 | equal: (val, equal) => val === equal, 398 | type: val => Number.isInteger(val) && val > 0 && val < 100, 399 | }); 400 | 401 | const customInteger = new Rule({ 402 | type: 'customInteger', 403 | min: 10, 404 | }, null); 405 | 406 | customInteger.test(11) // returns true 407 | 408 | customInteger.test(200) // returns false 409 | 410 | ``` 411 | 412 | # How to contribute 413 | 414 | Thank you everyone for contributing to make this code better, if you have suggestions or ideas to improve the code please feel free to leave a comment here #29. 415 | Rules: 416 | 417 | ### 1 Please use this template which will help developers to test and better understand your request 418 | 419 | ```javascript 420 | const someRule= new Rule({ 421 | type: 'yourType', 422 | prop1: val1, 423 | prop2: val2, ... 424 | }, null); 425 | 426 | someRule.test(validValue) // returns true 427 | someRule.test(invalidValue) // returns false 428 | ``` 429 | 430 | ## 2 Please if you think a comment is a good feature to be added like the comment instead of creating a new one. 431 | 432 | ## 3 Before submitting a new comment check if the same comment is not already present 433 | 434 | ## 4 If you submit a PR (pull request) and you only change the Readme please add `[ci skip]` to your commit message 435 | 436 | ## 5 If you have any questions ask them in the FAQ 437 | 438 | ## 6 Please have fun, and if you feel like not following the rules then don't follow them 439 | 440 | code with love ❤️ 441 | 442 | # Left TO DO for next release 443 | 444 | # Contact 445 | 446 | Follow me on twitter at [@forg_js](https://twitter.com/forg_js "@forg_js") 447 | --------------------------------------------------------------------------------