├── .npmignore ├── .gitignore ├── .babelrc ├── tests ├── date.rules.test.js ├── console.test.js ├── credit_card_cvv.rules.test.js ├── required.rules.test.js ├── email.rules.test.js ├── required_if.rules.test.js ├── integer.rules.test.js ├── max.rules.test.js ├── min.rules.test.js ├── string.rules.test.js ├── min_chars.rules.test.js ├── max_chars.rules.test.js ├── after_or_equal.rules.test.js ├── before_or_equal.rules.test.js ├── helper.test.js ├── credit_card_number.rules.test.js └── eloquent.test.js ├── src ├── utils │ ├── Console.js │ ├── Helper.js │ ├── Eloquent.js │ └── Rules.js ├── index.js └── Classes │ ├── CreditCard.js │ └── Validator.js ├── .github └── workflows │ └── nodejs.yml ├── LICENSE ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | coverage/ -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-proposal-class-properties" 7 | ] 8 | } -------------------------------------------------------------------------------- /tests/date.rules.test.js: -------------------------------------------------------------------------------- 1 | import {date} from "../src/utils/Rules"; 2 | 3 | let d = '10/31/2020'; 4 | 5 | test('date rule passes with a valid date', () => { 6 | expect(date(d)).toBe(true); 7 | }); 8 | 9 | let invalidDate = '15/15/15' 10 | test('date rule fails with a invalid date', () => { 11 | expect(date(invalidDate)).toBe(false); 12 | }); -------------------------------------------------------------------------------- /tests/console.test.js: -------------------------------------------------------------------------------- 1 | import {warn, warnIf} from "../src/utils/Console"; 2 | 3 | test('it displays a warning', () => { 4 | expect(warn('custom msg')).toBe(true); 5 | }); 6 | 7 | test('it displays a warning based on condition', () => { 8 | expect(warnIf(true, 'custom msg')).toBe(true); 9 | }); 10 | 11 | test('it does not display a warning based on false condition', () => { 12 | expect(warnIf(false, 'custom msg')).toBe(false); 13 | }); -------------------------------------------------------------------------------- /tests/credit_card_cvv.rules.test.js: -------------------------------------------------------------------------------- 1 | import {credit_card_cvv, credit_card_number} from "../src/utils/Rules"; 2 | 3 | let cvv = 852; 4 | 5 | test('credit_card_cvv rule passes with valid number', () => { 6 | expect(credit_card_cvv(cvv)).toBe(true); 7 | }); 8 | 9 | test('credit_card_cvv rule fails with null', () => { 10 | expect(credit_card_cvv(null)).toBe(false); 11 | }); 12 | 13 | test('credit_card_cvv rule fails invalid number', () => { 14 | expect(credit_card_cvv(12345)).toBe(false); 15 | }); -------------------------------------------------------------------------------- /src/utils/Console.js: -------------------------------------------------------------------------------- 1 | const warnPrefix = 'PsValidation debugger: '; 2 | 3 | export function warn(message) { 4 | if(!process.env.JEST_WORKER_ID) { 5 | window.console.warn(warnPrefix + message); 6 | } 7 | 8 | return true; 9 | } 10 | 11 | export function warnIf(condition, message) { 12 | if(condition) { 13 | if(!process.env.JEST_WORKER_ID) { 14 | window.console.warn(warnPrefix + message); 15 | } 16 | return true; 17 | } 18 | return false; 19 | } -------------------------------------------------------------------------------- /tests/required.rules.test.js: -------------------------------------------------------------------------------- 1 | import {required} from "./../src/utils/Rules"; 2 | 3 | test('required rule fails with null value', () => { 4 | expect(required(null)).toBe(false); 5 | }); 6 | 7 | test('required rule fails with empty string', () => { 8 | expect(required('')).toBe(false); 9 | }); 10 | 11 | test('required rule passes with valid string', () => { 12 | expect(required('Jest')).toBe(true); 13 | }); 14 | 15 | test('required rule passes with valid integer', () => { 16 | expect(required(100)).toBe(true); 17 | }); -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: NodeCI 2 | on: [push] 3 | 4 | jobs: 5 | build: 6 | 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [8.x, 10.x, 12.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: npm install, build, and test 20 | run: | 21 | npm ci 22 | npm run build --if-present 23 | npm test 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /tests/email.rules.test.js: -------------------------------------------------------------------------------- 1 | import {email} from "../src/utils/Rules"; 2 | 3 | let validEmail = 'elie@primitivesocial.com'; 4 | test('email rule passes with valid email', () => { 5 | expect(email(validEmail)).toBe(true); 6 | }); 7 | 8 | let invalidEmail = 'elie at primitivesocial dot com'; 9 | test('email rule fails with invalid email', () => { 10 | expect(email(invalidEmail)).toBe(false); 11 | }); 12 | 13 | let emptyStr = ''; 14 | test('email rule fails with empty string', () => { 15 | expect(email(emptyStr)).toBe(false); 16 | }); 17 | 18 | let number = 100; 19 | test('email rule fails with numbers', () => { 20 | expect(email(number)).toBe(false); 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /tests/required_if.rules.test.js: -------------------------------------------------------------------------------- 1 | import {required_if} from "../src/utils/Rules"; 2 | 3 | let validCondition = true; 4 | 5 | test('required_if passes if the condition is not boolean', () => { 6 | expect(required_if('Foo', 'Jest')).toBe(true); 7 | }); 8 | 9 | test('required_if rule fails with empty value and valid condition', () => { 10 | expect(required_if(null, validCondition)).toBe(false); 11 | }); 12 | 13 | test('required_if rule passes with valid value and valid condition', () => { 14 | expect(required_if('Jest', validCondition)).toBe(true); 15 | }); 16 | 17 | test('required_if rule passes with empty value and false condition', () => { 18 | expect(required_if('', false)).toBe(true); 19 | }); -------------------------------------------------------------------------------- /tests/integer.rules.test.js: -------------------------------------------------------------------------------- 1 | import {integer} from "./../src/utils/Rules"; 2 | 3 | test('integer rule fails with null value', () => { 4 | expect(integer(null)).toBe(false); 5 | }); 6 | 7 | test('integer rule fails with valid string', () => { 8 | expect(integer('Jest')).toBe(false); 9 | }); 10 | 11 | test('integer rule fails with valid array', () => { 12 | expect(integer([])).toBe(false); 13 | }); 14 | 15 | test('integer rule fails with valid object', () => { 16 | expect(integer({})).toBe(false); 17 | }); 18 | 19 | test('integer rule passes with valid integer', () => { 20 | expect(integer(10)).toBe(true); 21 | }); 22 | 23 | test('integer rule passes with valid decimal', () => { 24 | expect(integer(10.5)).toBe(true); 25 | }); -------------------------------------------------------------------------------- /tests/max.rules.test.js: -------------------------------------------------------------------------------- 1 | import {max} from "../src/utils/Rules"; 2 | 3 | let arg = 10; 4 | 5 | test('max rule passes with number less than the max', () => { 6 | expect(max(5, arg)).toBe(true); 7 | }); 8 | 9 | test('max rule passes with decimals less than the max', () => { 10 | expect(max(5.9, arg)).toBe(true); 11 | }); 12 | 13 | test('max rule fails with number greater than the max', () => { 14 | expect(max(18, arg)).toBe(false); 15 | }); 16 | 17 | test('max rule passes with string as number because of parseInt', () => { 18 | expect(max('7', arg)).toBe(true); 19 | }); 20 | 21 | test('max rule fails with array as number', () => { 22 | expect(max([1], arg)).toBe(false); 23 | }); 24 | 25 | test('max rule fails with object as number', () => { 26 | expect(max({}, arg)).toBe(false); 27 | }); -------------------------------------------------------------------------------- /tests/min.rules.test.js: -------------------------------------------------------------------------------- 1 | import {min} from "../src/utils/Rules"; 2 | 3 | let arg = 10; 4 | 5 | test('min rule passes with number greater than the min', () => { 6 | expect(min(15, arg)).toBe(true); 7 | }); 8 | 9 | test('min rule passes with decimals greater than the min', () => { 10 | expect(min(15.9, arg)).toBe(true); 11 | }); 12 | 13 | test('min rule fails with number less than the min', () => { 14 | expect(min(8, arg)).toBe(false); 15 | }); 16 | 17 | test('min rule passes with string as number because of parseInt', () => { 18 | expect(min('17', arg)).toBe(true); 19 | }); 20 | 21 | test('min rule fails with array as number', () => { 22 | expect(min([17], arg)).toBe(false); 23 | }); 24 | 25 | test('min rule fails with object as number', () => { 26 | expect(min({}, arg)).toBe(false); 27 | }); -------------------------------------------------------------------------------- /tests/string.rules.test.js: -------------------------------------------------------------------------------- 1 | import {string} from "../src/utils/Rules"; 2 | 3 | let validStr = 'Jest'; 4 | test('string rule passes with valid value', () => { 5 | expect(string(validStr)).toBe(true); 6 | }); 7 | 8 | let nullStr = null; 9 | test('string rule fails with null value', () => { 10 | expect(string(nullStr)).toBe(false); 11 | }); 12 | 13 | let arr = []; 14 | test('string rule fails with array', () => { 15 | expect(string(arr)).toBe(false); 16 | }); 17 | 18 | let obj = {}; 19 | test('string rule fails with object', () => { 20 | expect(string(obj)).toBe(false); 21 | }); 22 | 23 | let number = 12; 24 | test('string rule fails with number', () => { 25 | expect(string(number)).toBe(false); 26 | }); 27 | 28 | let emptyStr = ''; 29 | test('string rule passes with empty string', () => { 30 | expect(string(emptyStr)).toBe(true); 31 | }); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {Validator} from "./Classes/Validator"; 2 | 3 | const PsValidation = { 4 | install: function(Vue, options = {}) { 5 | Vue.mixin({ 6 | data() { 7 | return { 8 | validator: null 9 | } 10 | }, 11 | methods: { 12 | // initialize the validator 13 | $initValidator: function() { 14 | let sfc = this; 15 | this.validator = new Validator(sfc); 16 | }, 17 | // renders the validator error (by model). 18 | $error: function(model) { 19 | return (this.validator) ? this.validator.renderError(model) : null; 20 | } 21 | }, 22 | //mounted() { 23 | //this.$initValidator(); 24 | //} 25 | }); 26 | } 27 | }; 28 | 29 | export default PsValidation; 30 | -------------------------------------------------------------------------------- /tests/min_chars.rules.test.js: -------------------------------------------------------------------------------- 1 | import {min_chars} from "../src/utils/Rules"; 2 | 3 | let arg = 5; 4 | 5 | test('min_chars rule passes with string length greater than the min', () => { 6 | expect(min_chars('hello world', arg)).toBe(true); 7 | }); 8 | 9 | test('min_chars rule fails with string length lesser than the min', () => { 10 | expect(min_chars('test', arg)).toBe(false); 11 | }); 12 | 13 | test('min_chars rule passes with number as string', () => { 14 | expect(min_chars('123456', arg)).toBe(true); 15 | }); 16 | 17 | test('min_chars rule fails with number as string', () => { 18 | expect(min_chars('1234', arg)).toBe(false); 19 | }); 20 | 21 | test('min_chars rule fails with array as input', () => { 22 | expect(min_chars([17], arg)).toBe(false); 23 | }); 24 | 25 | test('min_chars rule fails with number as input', () => { 26 | expect(min_chars(17, arg)).toBe(false); 27 | }); 28 | 29 | test('min_chars rule fails with object as input', () => { 30 | expect(min_chars({}, arg)).toBe(false); 31 | }); -------------------------------------------------------------------------------- /tests/max_chars.rules.test.js: -------------------------------------------------------------------------------- 1 | import {max_chars} from "../src/utils/Rules"; 2 | 3 | let arg = 5; 4 | 5 | test('max_chars rule passes with string length lesser than the max', () => { 6 | expect(max_chars('hello', arg)).toBe(true); 7 | }); 8 | 9 | test('max_chars rule fails with string length greater than the max', () => { 10 | expect(max_chars('hello world!', arg)).toBe(false); 11 | }); 12 | 13 | test('max_chars rule passes with number as string', () => { 14 | expect(max_chars('12345', arg)).toBe(true); 15 | }); 16 | 17 | test('max_chars rule fails with number as string', () => { 18 | expect(max_chars('123456', arg)).toBe(false); 19 | }); 20 | 21 | test('max_chars rule fails with array as input', () => { 22 | expect(max_chars([17], arg)).toBe(false); 23 | }); 24 | 25 | test('max_chars rule fails with number as input', () => { 26 | expect(max_chars(17, arg)).toBe(false); 27 | }); 28 | 29 | test('max_chars rule fails with object as input', () => { 30 | expect(max_chars({}, arg)).toBe(false); 31 | }); -------------------------------------------------------------------------------- /tests/after_or_equal.rules.test.js: -------------------------------------------------------------------------------- 1 | import {after_or_equal} from "../src/utils/Rules"; 2 | 3 | let comparisonDate = '10/31/2019'; 4 | let currentDate = '11/20/2019'; 5 | 6 | test('after_or_equal rule passes with a valid condition', () => { 7 | expect(after_or_equal(currentDate, comparisonDate)).toBe(true); 8 | }); 9 | 10 | let pastDate = '11/20/2018'; 11 | test('after_or_equal rule fails with an invalid condition', () => { 12 | expect(after_or_equal(pastDate, comparisonDate)).toBe(false); 13 | }); 14 | 15 | test('after_or_equal rule fails with invalid date', () => { 16 | expect(after_or_equal('15/15/15', comparisonDate)).toBe(false); 17 | }); 18 | 19 | test('after_or_equal rule fails with number', () => { 20 | expect(after_or_equal(15, comparisonDate)).toBe(false); 21 | }); 22 | 23 | test('after_or_equal rule fails array', () => { 24 | expect(after_or_equal(['11/20/2019'], comparisonDate)).toBe(false); 25 | }); 26 | 27 | test('after_or_equal rule fails object', () => { 28 | expect(after_or_equal({d: '11/20/2019'}, comparisonDate)).toBe(false); 29 | }); -------------------------------------------------------------------------------- /tests/before_or_equal.rules.test.js: -------------------------------------------------------------------------------- 1 | import {before_or_equal} from "../src/utils/Rules"; 2 | 3 | let comparisonDate = '10/31/2019'; 4 | let currentDate = '11/20/2018'; 5 | 6 | test('before_or_equal rule passes with a valid condition', () => { 7 | expect(before_or_equal(currentDate, comparisonDate)).toBe(true); 8 | }); 9 | 10 | let postDate = '11/20/2020'; 11 | test('before_or_equal rule fails with an invalid condition', () => { 12 | expect(before_or_equal(postDate, comparisonDate)).toBe(false); 13 | }); 14 | 15 | test('before_or_equal rule fails with invalid date', () => { 16 | expect(before_or_equal('15/15/15', comparisonDate)).toBe(false); 17 | }); 18 | 19 | test('before_or_equal rule fails with number', () => { 20 | expect(before_or_equal(15, comparisonDate)).toBe(false); 21 | }); 22 | 23 | test('before_or_equal rule fails array', () => { 24 | expect(before_or_equal(['11/20/2019'], comparisonDate)).toBe(false); 25 | }); 26 | 27 | test('before_or_equal rule fails object', () => { 28 | expect(before_or_equal({d: '11/20/2019'}, comparisonDate)).toBe(false); 29 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Primitive Social 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 | -------------------------------------------------------------------------------- /tests/helper.test.js: -------------------------------------------------------------------------------- 1 | import {isObject, hasArg, Str} from "./../src/utils/Helper"; 2 | 3 | let validStr = 'required_if:hasEvent'; 4 | test('it returns true with a valid syntax', () => { 5 | expect(hasArg(validStr)).toBe(true); 6 | }); 7 | 8 | let invalidStr = 'required_if|hasEvent'; 9 | test('it returns false with an invalid syntax', () => { 10 | expect(hasArg(invalidStr)).toBe(false); 11 | }); 12 | 13 | let validObj = { foo: 'bar'}; 14 | test('it returns true with a valid object', () => { 15 | expect(isObject(validObj)).toBe(true); 16 | }); 17 | 18 | let nullObj = null; 19 | test('it returns false with null', () => { 20 | expect(isObject(nullObj)).toBe(false); 21 | }); 22 | 23 | let invalidObj = "foo:bar"; 24 | test('it returns false with an invalid object', () => { 25 | expect(isObject(invalidObj)).toBe(false); 26 | }); 27 | 28 | let validStartsWith = "validation"; 29 | test('Str starts_with is correct', () => { 30 | expect(Str(validStartsWith).starts_with('va')).toBe(true); 31 | }); 32 | 33 | let invalidStartsWith = "validation"; 34 | test('Str starts_with is incorrect', () => { 35 | expect(Str(invalidStartsWith).starts_with('inva')).toBe(false); 36 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@primitivesocial/ps-validation", 3 | "version": "1.0.11", 4 | "description": "Vue data validation rules, very much inspired from Laravel validation", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "babel src -d dist", 8 | "test": "jest --coverage" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/PrimitiveSocial/ps-validation.git" 13 | }, 14 | "keywords": [ 15 | "vue", 16 | "validation", 17 | "rules", 18 | "laravel-inspired", 19 | "required", 20 | "required_if", 21 | "fails", 22 | "passes", 23 | "extend", 24 | "after_or_equal", 25 | "credit_card_number" 26 | ], 27 | "author": "Elie Andraos ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/PrimitiveSocial/ps-validation/issues" 31 | }, 32 | "homepage": "https://github.com/PrimitiveSocial/ps-validation#readme", 33 | "devDependencies": { 34 | "@babel/cli": "^7.2.3", 35 | "@babel/core": "^7.7.2", 36 | "@babel/plugin-proposal-class-properties": "^7.7.0", 37 | "@babel/preset-env": "^7.3.1", 38 | "jest": "^24.9.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/credit_card_number.rules.test.js: -------------------------------------------------------------------------------- 1 | import {credit_card_number} from "./../src/utils/Rules"; 2 | 3 | test('credit_card_number rule fails with null number', () => { 4 | expect(credit_card_number(null, 'Visa')).toBe(false); 5 | }); 6 | 7 | test('credit_card_number rule fails with a number that is not lunh10', () => { 8 | expect(credit_card_number('1111111111111111', 'Visa')).toBe(false); 9 | }); 10 | 11 | test('credit_card_number rule fails with an invalid card type', () => { 12 | expect(credit_card_number('4242424242424242', 'Foo')).toBe(false); 13 | }); 14 | 15 | test('credit_card_number rule fails with invalid number of digits', () => { 16 | expect(credit_card_number('424242424242', 'Visa')).toBe(false); 17 | }); 18 | 19 | test('credit_card_number rule fails with an invalid prefix for card type', () => { 20 | expect(credit_card_number('5242424242424242', 'Visa')).toBe(false); 21 | }); 22 | 23 | test('credit_card_number of type Visa fails with MasterCard set', () => { 24 | expect(credit_card_number('4242424242424242', 'MasterCard')).toBe(false); 25 | }); 26 | 27 | test('credit_card_number of type Visa is valid', () => { 28 | expect(credit_card_number('4242424242424242', 'Visa')).toBe(true); 29 | }); 30 | 31 | test('credit_card_number of type MasterCard is valid', () => { 32 | expect(credit_card_number('5555555555554444', 'MasterCard')).toBe(true); 33 | }); 34 | 35 | test('credit_card_number supports dashes', () => { 36 | expect(credit_card_number('5555-5555-5555-4444', 'MasterCard')).toBe(true); 37 | }); 38 | 39 | test('credit_card_number supports spaces', () => { 40 | expect(credit_card_number('5555 5555 5555 4444', 'MasterCard')).toBe(true); 41 | }); -------------------------------------------------------------------------------- /src/utils/Helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check if the value is a javascript object. 3 | * 4 | * @param value 5 | * @returns {boolean} 6 | */ 7 | export function isObject(value) { 8 | let type = typeof value; 9 | return value !== null && (type === 'object' || type === 'function'); 10 | } 11 | 12 | /** 13 | * Check if a value is a string with the format: rule:arg 14 | * 15 | * @param value 16 | * @returns {boolean} 17 | */ 18 | export function hasArg(value) { 19 | return ( typeof value === 'string' && value.split(':').length > 1); 20 | } 21 | 22 | /** 23 | * Validate a variety of identification numbers, such as credit card numbers, IMEI numbers, National Provider Identifier numbers 24 | * 25 | * https://en.wikipedia.org/wiki/Luhn_algorithm 26 | * @param identifier 27 | * @returns {boolean} 28 | */ 29 | export function luhn10(identifier) { 30 | if(!identifier || !identifier.length) 31 | return false; 32 | 33 | // Check that the number is numeric 34 | let cardExp = /^[0-9]{13,19}$/; 35 | if (!cardExp.exec(identifier)) { 36 | return false; 37 | } 38 | 39 | let sum = 0; 40 | let alt = false; 41 | let i = identifier.length - 1; 42 | let num; 43 | 44 | while (i >= 0) { 45 | num = parseInt(identifier.charAt(i), 10); 46 | 47 | if (alt) { 48 | num *= 2; 49 | if (num > 9) { 50 | num = (num % 10) + 1; // eslint-disable-line no-extra-parens 51 | } 52 | } 53 | 54 | alt = !alt; 55 | 56 | sum += num; 57 | 58 | i--; 59 | } 60 | 61 | return sum % 10 === 0; 62 | }; 63 | 64 | export const Str = (str) => { 65 | return { 66 | // Determines if a string begins with the given string 67 | starts_with(value) { 68 | return str.indexOf(value) === 0; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Classes/CreditCard.js: -------------------------------------------------------------------------------- 1 | import {luhn10, Str} from "./../utils/Helper"; 2 | 3 | export class CreditCard { 4 | 5 | number; 6 | cardType; 7 | 8 | constructor(number, cardType) { 9 | this.cardType = cardType; 10 | this.number = number; 11 | 12 | if(this.number && this.number.length) { 13 | this.formatCardNumber(); 14 | } 15 | } 16 | 17 | getCardTypes() { 18 | return [ 19 | { 20 | name: 'Visa', 21 | nbDigits: [13,16], 22 | prefixes: [4], 23 | }, 24 | { 25 | name: 'MasterCard', 26 | nbDigits: [16], 27 | prefixes: [51,52,53,54,55], 28 | }, 29 | { 30 | name: 'Amex', 31 | nbDigits: [15], 32 | prefixes: [34,37], 33 | }, 34 | { 35 | name: 'VisaElectron', 36 | nbDigits: [16], 37 | prefixes: [6304,6706,6771,6709], 38 | } 39 | ]; 40 | } 41 | 42 | formatCardNumber() { 43 | // Remove any dashes from the credit card number 44 | this.number = this.number.replace(/-/g, ''); 45 | // Remove any spaces from the credit card number 46 | this.number = this.number.replace (/\s/g, ""); 47 | } 48 | 49 | getCard() { 50 | return this.getCardTypes().filter( card => { 51 | return card.name === this.cardType; 52 | }).shift(); 53 | } 54 | 55 | isValid() { 56 | if(this.isValidCardType() && this.isValidLuhnNumber()) { 57 | if(!this.isValidNbDigits() || !this.isValidPrefix()) { 58 | return false; 59 | } 60 | // validate prefixes 61 | return true; 62 | } 63 | 64 | return false; 65 | } 66 | 67 | isValidCardType() { 68 | return !!(this.getCard()); 69 | } 70 | 71 | isValidLuhnNumber() { 72 | return luhn10(this.number); 73 | } 74 | 75 | isValidNbDigits() { 76 | return this.getCard().nbDigits.includes(this.number.length); 77 | } 78 | 79 | isValidPrefix() { 80 | let valid = false; 81 | this.getCard().prefixes.forEach( prefix => { 82 | if(Str(this.number).starts_with(prefix)) 83 | valid = true; 84 | }); 85 | 86 | return valid; 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /tests/eloquent.test.js: -------------------------------------------------------------------------------- 1 | import {find, has} from "./../src/utils/Eloquent"; 2 | 3 | let data = { 4 | name: 'John Doe', 5 | event: { 6 | start_date: '10/20/2019', 7 | fee: 50, 8 | speaker: [ 9 | 'Taylor', 10 | 'Adam' 11 | ] 12 | }, 13 | a: null, 14 | b: undefined, 15 | c: true, 16 | d: [], 17 | e: {} 18 | }; 19 | 20 | test('it finds name property in data', () => { 21 | expect(find(data, 'name')).toBe('John Doe'); 22 | }); 23 | 24 | test('it finds and returns null values', () => { 25 | expect(find(data, 'a')).toBe(null); 26 | }); 27 | 28 | test('it finds and returns undefined values', () => { 29 | expect(find(data, 'b')).toBe(undefined); 30 | }); 31 | 32 | test('it finds and returns bool values', () => { 33 | expect(find(data, 'c')).toBe(true); 34 | }); 35 | 36 | test('it does not find invalid path', () => { 37 | expect(find(data, 'x.y.z')).toBe('Could not find "x.y.z" in the object.'); 38 | }); 39 | 40 | test('it supports finding properties by dot annotations', () => { 41 | expect(find(data, 'event.start_date')).toBe('10/20/2019'); 42 | }); 43 | 44 | test('it supports finding properties by dot annotations with array indexes', () => { 45 | expect(find(data, 'event.speaker.0')).toBe('Taylor'); 46 | expect(find(data, 'event.speaker.1')).toBe('Adam'); 47 | }); 48 | 49 | test('it returns default message when not found', () => { 50 | expect(find(data, 'foo')).toBe('Could not find "foo" in the object.'); 51 | }); 52 | 53 | test('it returns default message when not finding a nested property', () => { 54 | expect(find(data, 'event.test')).toBe('Could not find "event.test" in the object.'); 55 | }); 56 | 57 | test('it returns default message with undefined', () => { 58 | expect(find(undefined, 'foo')).toBe('Could not find "foo" in the object.'); 59 | }); 60 | 61 | test('it returns default message with array', () => { 62 | expect(find([], 'foo')).toBe('Could not find "foo" in the object.'); 63 | }); 64 | 65 | test('it returns default message with string', () => { 66 | expect(find('foo', 'foo')).toBe('Could not find "foo" in the object.'); 67 | }); 68 | 69 | test('it checks if a property exists in data', () => { 70 | expect(has(data, 'name')).toBe(true); 71 | }); 72 | 73 | test('it checks if a property does not exist in data', () => { 74 | expect(has(data, 'foo')).toBe(false); 75 | }); 76 | 77 | test('it checks if a nested property does not exist in data', () => { 78 | expect(has(data, 'event.test')).toBe(false); 79 | }); 80 | 81 | test('it supports checking property names by dot annotations', () => { 82 | expect(has(data, 'event.fee')).toBe(true); 83 | }); 84 | 85 | test('it returns false with undefined', () => { 86 | expect(has(undefined, 'foo')).toBe(false); 87 | }); 88 | 89 | test('it returns false with array', () => { 90 | expect(has([], 'foo')).toBe(false); 91 | }); 92 | 93 | test('it returns false with string', () => { 94 | expect(has('foo', 'foo')).toBe(false); 95 | }); -------------------------------------------------------------------------------- /src/utils/Eloquent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Credit to this package: https://www.npmjs.com/package/dot-prop 3 | */ 4 | import {isObject} from "./Helper"; 5 | 6 | const disallowedKeys = [ 7 | '__proto__', 8 | 'prototype', 9 | 'constructor' 10 | ]; 11 | 12 | const isValidPath = pathSegments => !pathSegments.some(segment => disallowedKeys.includes(segment)); 13 | 14 | /** 15 | * Get the path segments split by '.' 16 | * @param path 17 | * @returns {[]|Array} 18 | */ 19 | function getPathSegments(path) { 20 | const pathArray = path.split('.'); 21 | const parts = []; 22 | 23 | for (let i = 0; i < pathArray.length; i++) { 24 | let p = pathArray[i]; 25 | 26 | while (p[p.length - 1] === '\\' && pathArray[i + 1] !== undefined) { 27 | p = p.slice(0, -1) + '.'; 28 | p += pathArray[++i]; 29 | } 30 | 31 | parts.push(p); 32 | } 33 | 34 | if (!isValidPath(parts)) { 35 | return []; 36 | } 37 | 38 | return parts; 39 | } 40 | 41 | /** 42 | * Get a property from a nested object using a dot path. 43 | * 44 | * @example find(data, 'notice.event') 45 | * 46 | * @param object 47 | * @param path 48 | * @param value 49 | * @returns {string|*} 50 | */ 51 | export function find(object, path, value) { 52 | value = 'Could not find "' + path + '" in the object.'; 53 | if (!isObject(object) || typeof path !== 'string') { 54 | return value === undefined ? object : value; 55 | } 56 | 57 | let pathArray = getPathSegments(path); 58 | // if (pathArray.length === 0) { 59 | // return; 60 | // } 61 | 62 | for (let i = 0; i < pathArray.length; i++) { 63 | if (!Object.prototype.propertyIsEnumerable.call(object, pathArray[i])) { 64 | return value; 65 | } 66 | 67 | object = object[pathArray[i]]; 68 | 69 | if (object === undefined || object === null) { 70 | if (i !== pathArray.length - 1) { 71 | return value; 72 | } 73 | 74 | break; 75 | } 76 | } 77 | 78 | return object; 79 | } 80 | 81 | /** 82 | * Check if a property exists in a nested object using a dot path. 83 | * 84 | * @example has(data, 'notice.event') 85 | * 86 | * @param object 87 | * @param path 88 | * @returns {boolean} 89 | */ 90 | export function has(object, path) { 91 | if (!isObject(object) || typeof path !== 'string') { 92 | return false; 93 | } 94 | 95 | const pathArray = getPathSegments(path); 96 | if (pathArray.length === 0) { 97 | return false; 98 | } 99 | 100 | for (let i = 0; i < pathArray.length; i++) { 101 | if (isObject(object)) { 102 | if (!(pathArray[i] in object)) { 103 | return false; 104 | } 105 | 106 | object = object[pathArray[i]]; 107 | } else { 108 | return false; 109 | } 110 | } 111 | 112 | return true; 113 | } 114 | -------------------------------------------------------------------------------- /src/utils/Rules.js: -------------------------------------------------------------------------------- 1 | import {warn} from "./Console"; 2 | import {CreditCard} from "./../Classes/CreditCard"; 3 | 4 | export function required(value, arg = null) { 5 | return (value !== '' && value !== null && typeof value !== 'undefined'); 6 | } 7 | 8 | export function integer (value, arg = null) { 9 | return Number.isInteger(parseInt(value)); 10 | } 11 | 12 | export function string (value, arg = null) { 13 | return typeof value === 'string'; 14 | } 15 | 16 | export function email (value, arg = null) { 17 | let re = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/; 18 | return re.test(value) 19 | } 20 | 21 | export function min (value, arg) { 22 | if(typeof value === 'object') 23 | return false; 24 | 25 | return parseInt(value) >= parseInt(arg); 26 | } 27 | 28 | export function max (value, arg) { 29 | if(typeof value === 'object') 30 | return false; 31 | 32 | return parseInt(value) <= parseInt(arg); 33 | } 34 | 35 | export function min_chars (value, arg) { 36 | if(typeof value !== 'string') 37 | return false; 38 | 39 | return value.length >= parseInt(arg); 40 | } 41 | 42 | export function max_chars (value, arg) { 43 | if(typeof value !== 'string') 44 | return false; 45 | 46 | return value.length <= parseInt(arg); 47 | } 48 | 49 | export function date (value, arg = null) { 50 | let date = new Date(value); 51 | return !isNaN(date.getFullYear()) && !isNaN(date.getMonth()) && !isNaN(date.getDate()) && date.toDateString() !== 'Invalid Date'; 52 | } 53 | 54 | export function before_or_equal (value, arg) { 55 | if(typeof value === 'object' || typeof arg === 'object' || typeof value === 'number' || typeof arg === 'number') 56 | return false; 57 | 58 | let originalDate = new Date(value); 59 | let comparingDate = new Date(arg); 60 | return originalDate.getTime() <= comparingDate.getTime(); 61 | } 62 | 63 | export function after_or_equal (value, arg) { 64 | if(typeof value === 'object' || typeof arg === 'object' || typeof value === 'number' || typeof arg === 'number') 65 | return false; 66 | 67 | let originalDate = new Date(value); 68 | let comparingDate = new Date(arg); 69 | return originalDate.getTime() >= comparingDate.getTime(); 70 | } 71 | 72 | export function required_if(value, arg) { 73 | if(typeof arg !== 'boolean') { 74 | warn('The value of required_if argument must be of type boolean.'); 75 | warn('The required_if rule is ignored. Please fix its argument'); 76 | return true; 77 | } 78 | 79 | return (!arg) ? true : required(value); 80 | } 81 | 82 | export function credit_card_number(value, arg) { 83 | let cc = new CreditCard(value, arg); 84 | return cc.isValid(); 85 | } 86 | 87 | export function credit_card_cvv(value, arg = null) { 88 | let numberLength = 3; 89 | let regex = new RegExp(`^[0-9]{${numberLength}}$`); 90 | return regex.test(value); 91 | } 92 | 93 | export function credit_card_month(value, arg = null) { 94 | return parseInt(value) <= 12 && parseInt(value) > 0; 95 | } 96 | 97 | export function credit_card_year(value, arg = null) { 98 | let currentYear = new Date().getFullYear().toString().substr(-2); 99 | return parseInt(value) >= parseInt(currentYear); 100 | } 101 | 102 | export function length(value, arg) { 103 | return value.length === arg; 104 | } 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/PrimitiveSocial/ps-validation/NodeCI) 2 | [![CodeFactor](https://www.codefactor.io/repository/github/primitivesocial/ps-validation/badge)](https://www.codefactor.io/repository/github/primitivesocial/ps-validation) 3 | ![downloads](https://img.shields.io/npm/dt/@primitivesocial/ps-validation) 4 | ![min-size](https://img.shields.io/bundlephobia/min/@primitivesocial/ps-validation/1.0.6) 5 | ![license](https://img.shields.io/github/license/PrimitiveSocial/ps-validation) 6 | 7 | A Vue plugin that provides out-of-the-box data validation rules, very much inspired from Laravel validation system. 8 | 9 | ## Installation 10 | 11 | ``` 12 | npm i @primitivesocial/ps-validation 13 | ``` 14 | 15 | ```js 16 | import PsValidation from "@primitivesocial/ps-validation"; 17 | Vue.use(PsValidation); 18 | ``` 19 | 20 | ## Basic Usage Example 21 | The plugin provides a data property `validator` that creates a new instance of the validator. 22 | 23 | In this simple example, we will demonstrate how to add a validation for `name` property before submitting the data using the method `submitData()`. 24 | 25 | ```js 26 | // Vue SFC 27 | export default { 28 | data() { 29 | return { 30 | name: '', 31 | } 32 | }, 33 | methods: { 34 | submitData() { 35 | axios.post(url, { data: { name: name} }); 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | - **First step define the validation rules needed** 42 | ```js 43 | export default { 44 | data() { 45 | return { 46 | name: '', 47 | // here we are adding the validation rules 48 | validationRules: [ 49 | { model: 'name', rules: 'required' }, 50 | ] 51 | } 52 | } 53 | } 54 | ``` 55 | - **Next we will setup the validator and validate our data before submitting it** 56 | ```js 57 | mounted() { 58 | this.$initValidator(); 59 | this.validator.setRules(this.validationRules); 60 | }, 61 | methods: { 62 | submitData() { 63 | this.validator.validate(); 64 | 65 | if(this.validator.passes()) 66 | axios.post(url, { data: { name: name} }); 67 | 68 | // You can also use .fails() 69 | if(this.validator.fails()) 70 | alert('Name is required'); 71 | } 72 | } 73 | ``` 74 | 75 | And that's it! 🦄 🦄 🚀 🚀 76 | 77 | ## Error rendering & Customization 78 | The plugin provides a helper `$error` to render the error in the Vue component template. 79 | Each rule has a default error message. 80 | ```html 81 | {{ $error('name') }} 82 | 83 | ``` 84 | 85 | You can customize the error message when setting up the validator. 86 | ```js 87 | this.validator 88 | .setCustomMessages({ 89 | 'name': 'The name field must not be empty.' 90 | }); 91 | ``` 92 | _Note: the key provided in the `setCustomMessages()` object parameter, is always set to: `data property` concatenated with `rule name`_ 93 | 94 | ## Support for dot path annotations 95 | You can validate deep nested properties inside your data object easily by adding dot path annotations. 96 | ```js 97 | data() { 98 | return { 99 | event: { 100 | speaker: { 101 | name: '', 102 | } 103 | }, 104 | validations: [ 105 | {model: 'event.speaker.name', rules: 'required'} 106 | ] 107 | } 108 | } 109 | ``` 110 | ## Working With Rules 111 | ### Adding multiple rule 112 | You can add multiple rule to the same property or model by separating them with `|` 113 | ```js 114 | { model: 'age', rules: 'required | integer | min:18' } 115 | ``` 116 | 117 | ### Available rules 118 | - **required** _The field under validation must be present in the input data and not empty_ 119 | - **integer** _The field under validation must be an integer_ 120 | - **email** _The field under validation must be a valid email_ 121 | - **string** _The field under validation must be an string_ 122 | - **date** _The field under validation must be a date_ 123 | - **min:value** _The field under validation must have a minimum value. Numbers are only evaluated for now_ 124 | - **max:value** _The field under validation must have a maximum value. Numbers are only evaluated for now_ 125 | - **before_or_equal:date** _The field under validation must be a value preceding or equal to the given date_ 126 | - **after_or_equal:date** _The field under validation must be a value after or equal to the given date_ 127 | - **required_if:boolean** _The field under validation must be present and not empty if the boolean condition is true_ 128 | - **credit_card_number:cardType** _The field under validation must be a valid credit card number of the specified type_ 129 | - **credit_card_cvv** _The field under validation must be a valid credit card cvv_ 130 | 131 | *Available credit card types for validation: `Visa, MasterCard, Amex, VisaElectron` 132 | 133 | ```js 134 | // example of combined rules 135 | data() { 136 | person: { 137 | is_student: false, 138 | age: null, 139 | registered_at: null, 140 | }, 141 | card: { 142 | number: null, 143 | cvv: null, 144 | type: 'Visa' 145 | }, 146 | registration_ends: '10/31/2020', 147 | validations: [ 148 | // age will be required only if is_student is true 149 | { model: 'person.age', rules: 'required_if:person.is_student | integer | min:18' } , 150 | // registered_at will be required, must be a date and before or equal to registration_ends date 151 | { model: 'person.registered_at', rules: 'required | date | before_or_equal:registration_ends' }, 152 | // credit card number and cvv validation 153 | { model: 'card.number', rules: 'credit_card_number:card.type' }, 154 | { model: 'card.cvv', rules: 'credit_card_cvv' }, 155 | ] 156 | } 157 | ``` 158 | 159 | ## Adding Custom rule 160 | You can easily extend the validator by adding a custom rule using the method `extend(ruleName, function, errorMessage)` 161 | 162 | ```js 163 | mounted() { 164 | this.validator.extend( 165 | 'alpha_dash', 166 | function(value, arg) { 167 | let regexp = /^[a-z_]+$/i; 168 | return !!regexp.test(value); 169 | }, 170 | 'this field must contain only letters as well as underscores.' 171 | ); 172 | }, 173 | data() { 174 | return { 175 | username: '', 176 | validations: [ 177 | { model: 'username', rules: 'string | alpha_dash' } 178 | ] 179 | } 180 | } 181 | ``` 182 | 183 | ## Developer friendly 184 | Along with the jest tests, the plugin provides helpful warning messages in the browser console in case something is missed by the developer. 185 | Here are few examples: 186 | 187 | When you try to validate without setting the rules to the validator 188 | ``` 189 | PsValidation debugger: You must specify the validation rules. 190 | ``` 191 | 192 | Or when you add a rule that doesn't exist or not defined. 193 | ``` 194 | PsValidation debugger: The rule wtv for the model noticeEvent.notifyDate is not defined. It will be ignored. 195 | ``` 196 | 197 | ## Author & Contribution 198 | Hey, I'm Elie Andraos, a web developer at [Primitive Social](https://twitter.com/PrimitiveSocial). 199 | Pull requests are always welcome. For major changes, please open an issue first to discuss what you would like to change. 200 | You can also [reach me out on twitter](https://twitter.com/andzilla31) for any question! -------------------------------------------------------------------------------- /src/Classes/Validator.js: -------------------------------------------------------------------------------- 1 | import {find} from "../utils/Eloquent"; 2 | import {hasArg} from "../utils/Helper"; 3 | import * as definedRules from "../utils/Rules"; 4 | import {warnIf, warn} from "../utils/Console"; 5 | 6 | export class Validator { 7 | componentInstance; 8 | rules; 9 | errorsBag; 10 | isValid; 11 | availableRules; 12 | rulesWithModelRelatedArguments; 13 | messages; 14 | customMessages; 15 | 16 | constructor(componentInstance) { 17 | this.componentInstance = componentInstance; 18 | this.rules = null; 19 | this.errorsBag = []; 20 | this.isValid = true; 21 | this.availableRules = [ 22 | 'required', 23 | 'integer', 24 | 'email', 25 | 'min', 26 | 'max', 27 | 'min_chars', 28 | 'max_chars', 29 | 'string', 30 | 'required_if', 31 | 'date', 32 | 'before_or_equal', 33 | 'after_or_equal', 34 | 'credit_card_number', 35 | 'credit_card_cvv', 36 | 'credit_card_month', 37 | 'credit_card_year', 38 | 'length' 39 | ]; 40 | this.rulesWithModelRelatedArguments = [ 41 | 'before_or_equal', 42 | 'after_or_equal', 43 | 'required_if', 44 | 'credit_card_number', 45 | 'credit_card_cvv', 46 | 'length' 47 | ]; 48 | this.messages = this.getDefaultErrorMessages(); 49 | this.customMessages = {}; 50 | 51 | } 52 | 53 | setRules(_rules) { 54 | // TODO: validate the rules api is correct 55 | this.rules = _rules; 56 | return this; 57 | } 58 | 59 | setCustomMessages(_customMessages) { 60 | warnIf(typeof _customMessages !== 'object', 'The validator customMessages must be an object'); 61 | this.customMessages = _customMessages; 62 | return this; 63 | } 64 | 65 | getRules() { 66 | return this.rules; 67 | } 68 | 69 | getData() { 70 | let _data = this.componentInstance.$data; 71 | let result = {}; 72 | 73 | Object.keys(_data).forEach(key => { 74 | if(key !== 'validator') 75 | result[key] = _data[key]; 76 | }); 77 | 78 | return result; 79 | } 80 | 81 | getDefaultErrorMessages() { 82 | return { 83 | required: 'This field is required', 84 | integer: 'This field must be a number', 85 | email: 'This field must be a valid email', 86 | min: 'This field must not be less than {arg}', 87 | max: 'This field must not be greater than {arg}', 88 | min_chars: 'This field length must not be less than {arg}', 89 | max_chars: 'This field length must not be greater than {arg}', 90 | string: 'This field must be characters', 91 | required_if: 'This field is required', 92 | date: 'This field must be a valid date', 93 | before_or_equal: 'This date must be before or equal to {arg}', 94 | after_or_equal: 'This date must be after or equal to {arg}', 95 | credit_card_number: 'The card number is invalid', 96 | credit_card_cvv: 'The card cvv is invalid', 97 | credit_card_month: 'The card month is invalid', 98 | credit_card_year: 'The card year is invalid', 99 | length: 'The length should be {arg}' 100 | }; 101 | } 102 | 103 | validate() { 104 | let validationRules = this.getRules(); 105 | this.errorsBag = []; 106 | this.isValid = true; 107 | 108 | warnIf(!validationRules, 'You must specify the validation rules.'); 109 | 110 | validationRules.forEach( (item) => { 111 | let modelName = item.model; 112 | let modelValue = find(this.getData(), modelName); 113 | let rules = item.rules.split('|'); 114 | 115 | // if the rules contains 'required_if' rule, ignore the other rules unless the 'required_if' condition is matched 116 | if(this.shouldIgnoreRequiredIfRules(rules)) 117 | return; 118 | 119 | // execute the rules 120 | rules.forEach( (_rule) => { 121 | this.executeRule(_rule, modelName, modelValue); 122 | }); 123 | }); 124 | 125 | // update the error bag inside the validator of the vue component instance 126 | if(typeof this.componentInstance === 'object' && this.componentInstance.$data) { 127 | this.componentInstance.$data.validator.errorsBag = this.errorsBag; 128 | } 129 | 130 | return this; 131 | 132 | } 133 | 134 | renderError(model) { 135 | if(!this.errorsBag.length) 136 | return null; 137 | 138 | let errors = this.errorsBag.filter(error => error.model === model); 139 | return (errors.length) ? errors[0].message : null; 140 | } 141 | 142 | getErrorMessage(model, rule, arg) { 143 | let key = model + '.' + rule; 144 | let message = null; 145 | 146 | // get custom error message if exists 147 | if(this.customMessages.hasOwnProperty(key)) { 148 | message = this.customMessages[key]; 149 | } 150 | else { 151 | message = this.messages[rule]; 152 | } 153 | 154 | return message.replace('{arg}', arg); 155 | } 156 | 157 | addError(model, rule, errorMessage) { 158 | let key = model + '.' + rule; 159 | 160 | this.errorsBag.push({ 161 | key: key, 162 | model: model, 163 | message: errorMessage 164 | }); 165 | } 166 | 167 | shouldIgnoreRequiredIfRules(rules) { 168 | let hasRequiredIfRule = false; 169 | let requiredIfRule = null; 170 | 171 | rules.map( (rule, key) => { 172 | if(rule.startsWith("required_if:")) { 173 | hasRequiredIfRule = true; 174 | requiredIfRule = rule; 175 | } 176 | }); 177 | 178 | if(hasRequiredIfRule) { 179 | let str = requiredIfRule.split(':'); 180 | let arg = find(this.getData(), str[1].trim()); 181 | if (!arg) 182 | return true; 183 | } 184 | return false; 185 | } 186 | 187 | executeRule(_rule, modelName, modelValue) { 188 | let rule = _rule.trim(); 189 | let arg = null; 190 | 191 | if(hasArg(rule)) { 192 | let str = rule.split(':'); 193 | rule = str[0].trim(); 194 | arg = str[1].trim(); 195 | 196 | // check rules which their argument is related to another model 197 | if(this.rulesWithModelRelatedArguments.includes(rule)) { 198 | arg = find(this.getData(), arg); 199 | } 200 | } 201 | 202 | // if a rule is not in the available rule, ignore it. 203 | if(!this.availableRules.includes(rule)) { 204 | warn('The rule ' + rule + ' for the model ' + modelName + ' is not defined. it will be ignored.'); 205 | return true; 206 | } 207 | 208 | // get the rule error message 209 | let errorMessage = this.getErrorMessage(modelName, rule, arg); 210 | 211 | // call the rule method with the model value & update the error bag 212 | if(!definedRules[rule](modelValue,arg)) { 213 | this.addError(modelName, rule, errorMessage); 214 | this.isValid = false; 215 | return false; 216 | } 217 | 218 | return true; 219 | } 220 | 221 | passes() { 222 | return !!(this.isValid); 223 | } 224 | 225 | fails() { 226 | return !!(!this.isValid); 227 | } 228 | 229 | extend(ruleName, func, message) { 230 | let validApi = 231 | warnIf(!ruleName, 'Please specify a rule name as first argument for the validator extend() method') 232 | && warnIf(!func, 'Please specify a function as second argument for the validator extend() method') 233 | && warnIf(!message, 'Please specify a message as third argument for the validator extend() method') 234 | && warnIf(typeof ruleName !== 'string', 'The first argument must be a string in the validator extend() method') 235 | && warnIf(typeof func !== 'function', 'The second argument must be a function in the validator extend() method') 236 | && warnIf(typeof message !== 'string', 'The third argument must be a string in the validator extend() method') 237 | && warnIf( definedRules.hasOwnProperty(ruleName), 'The rule already exists!'); 238 | 239 | if(!validApi) 240 | return; 241 | 242 | definedRules[ruleName] = func; 243 | this.availableRules.push(ruleName); 244 | this.messages[ruleName] = message; 245 | } 246 | } --------------------------------------------------------------------------------