import { warn } from './debug'
import { config } from './config'
import { hasOwn, isObject } from './utils'
import { printf, ucfirst } from './string'
/**
* A simple javascript validation library
*
* @param {Rules} [rules={}] Validation rules
* @param {Options} [options={}] Runtime options
*
* @constructor
*/
function Isntit(rules, options) {
// called as function
if (!(this instanceof Isntit)) {
warn('Isntit([rules [, options]]) is a constructor and should be called with the "new" keyword.');
return new Isntit(rules, options);
}
if (options && options.config) {
Object.assign(config, options.config);
delete options.config;
}
Object.assign(this.options, options || {});
this.cache = {
checkersToStep: {},
messages: {}
};
this.checkRules(rules);
this.rules = rules || {};
this.errors = {};
}
/**
* The validation rules object to pass to the {@link Isntit} constructor or to Isntit's validate method.
*
* @typedef {Object.<string, Object>} Rules
* @property {(Object.<string, Constraints>|boolean)} `fieldName` The rules to validate the value of field.
* @property {Constraints|boolean} `fieldName`.`checkerName` The constraints to pass to the checker.
*/
/**
* The constraints to pass to corresponding {@link Checker|checker}.
*
* @typedef {Object.<string, *>} Constraints
* @property {ErrorMessageProvider} [message] A customized error message.
* @property {string|number|boolean} [`constraintName`] The value of a constraint to pass to the checker.
* @see confirms|required|email|format|length|numeric
*/
/**
* Check that the value is the same as the value in another field.
*
* @type {Constraints}
* @property {string} field The field this field should confirm.
* @property {boolean} [strict] Whether to compare strictly ('===') both field values or not ('==').
* @property {ErrorMessageProvider} [message] A customized error message.
* @example
* "password_confirmation": true
* // is converted to
* "password_confirmation": {
* confirms: {
* field: "password"
* }
* }
*/
var confirms = {
validate: function(value, context) {
var data = context.data;
var _confirms = context.ruleSet.confirms;
_confirms['otherValue'] = data[_confirms.field];
return (_confirms.strict) ?
(value === _confirms.otherValue) :
(value == _confirms.otherValue);
},
types: ['boolean', { field: 'string' }]
}
/**
* Check that a field is not empty (uses {@link Isntit.isEmpty}).
*
* @type {(boolean|Constraints)}
* @property {ErrorMessageProvider} [message] A customized error message.
*/
var required = {
validate: function(value) {
return !Isntit.isEmpty(value);
},
types: 'boolean'
}
/**
* Check that the value is formated as an email (uses {@link Config|config.emailRE}).
*
* @name email
* @type {Constraints}
* @property {ErrorMessageProvider} [message] A customized error message.
*/
var email = {
validate: function(value){
return config.emailRE.test(value);
},
preprocess: function(value) {
var I = this;
if (typeof value !== 'string') {
I.cache.messages['email::preprocess'] = 'Values checked for email MUST be of type string. Given: ' + value;
return false;
}
return true;
},
types: 'boolean'
}
/**
* Check that the value matches against given RegExp.
*
* @name format
* @type {(regexp|Constraints)}
* @property {regexp} pattern The RegExp to test against.
* @property {ErrorMessageProvider} [message] A customized error message.
*/
var format = {
validate: function(value, context) {
var ruleSet = context.ruleSet;
var RE = (ruleSet.format instanceof RegExp) ? ruleSet.format : ruleSet.format.pattern;
return (RE.test(value));
},
preprocess: function(value) {
var I = this;
if (typeof value !== 'string' && typeof value !== 'number') {
I.cache.messages['format::preprocess'] = 'Values checked for format MUST either be of type string or number. Given: ' + value;
return false;
}
return true;
},
types: ['regexp', { pattern: 'regexp' }]
}
/**
* Check that value comply with legth constraints.
*
* @name length
* @type {Constraints}
* @property {number} [is] The exact length of the value.
* @property {number} [min] The minimum length of the value.
* @property {number} [max] The maximum length of the value.
* @property {ErrorMessageProvider} [message] A customized error message.
*/
var length = {
validate: {
is: function(value, context) {
return (value.length == context.ruleSet['length'].is);
},
min: function(value, context) {
return (value.length >= context.ruleSet['length'].min);
},
max: function(value, context) {
return (value.length <= context.ruleSet['length'].max);
}
},
preprocess: function(value) {
var I = this;
if (value.length === undefined) {
I.cache.messages['length::preprocess'] = 'Values checked for length MUST have a length property. Given: ' + value;
return false;
}
return true;
},
types: {
__all: 'number'
}
}
/**
* Check value by comparing it with given constraints.
*
* @name numeric
* @type {Constraints}
* @property {number} [equalTo]
* @property {number} [notEqualTo]
* @property {number} [greaterThan]
* @property {number} [greaterThanOrEqualTo]
* @property {number} [lessThan]
* @property {number} [lessThanOrEqualTo]
* @property {number} [greaterThan]
* @property {boolean} [noStrings] Whether to accept numbers as strings
* @property {boolean} [onlyIntegers] Whether to accept only integers.
* @property {ErrorMessageProvider} [message] A customized error message.
*/
var numeric = {
validate: {
equalTo: function(value, context) {
return (+value == context.ruleSet['numeric'].equalTo);
},
notEqualTo: function(value, context) {
return (+value != context.ruleSet['numeric'].notEqualTo);
},
greaterThan: function(value, context) {
return (+value > context.ruleSet['numeric'].greaterThan);
},
greaterThanOrEqualTo: function(value, context) {
return (+value >= context.ruleSet['numeric'].greaterThanOrEqualTo);
},
lessThan: function(value, context) {
return (+value < context.ruleSet['numeric'].lessThan);
},
lessThanOrEqualTo: function(value, context) {
return (+value <= context.ruleSet['numeric'].lessThanOrEqualTo);
},
noStrings: function(value) {
return (typeof value !== 'string');
},
onlyInteger: function(value) {
return ((+value) % 1 === 0);
}
},
preprocess: function(value) {
var I = this;
if (typeof value !== 'string' && typeof value !== 'number') {
I.cache.messages['numeric::preprocess'] = 'Values checked for numeric MUST either be of type string or number. Given: ' + value;
return false;
}
return true;
},
types: {
noStrings: 'boolean',
onlyInteger: 'boolean',
__others: 'number'
}
}
/**
* To customize the error messages you can either provide a string,
* a function that returns a string or an object with `constraintName`s
* as keys to a appropriated error message string. All error messages strings
* are parsed by {@link Isntit.printf} to replace any `%{placeholders}`
* with corresponding value taken from {@Constraints} properties (other than `message`).
*
* @typedef {(MessageString|MessageFunction|MessageObject)} ErrorMessageProvider
*/
/**
* The message to show if corresponding checker fails.
* This string is passed to {@link Isntit.printf} to replace `%{placeholders}`
* with corresponding value taken from {@Constraints} properties (other than message).
*
* @typedef {string} MessageString The string to show when the value is not valid.
*/
/**
* A function that returns an error message should corresponding checker validation fail.
*
* @typedef {function} MessageFunction
* @param {Context} context The context in which the failed checker has been called.
* @param {string} [constraint] The name of the constraint that failed for which a error message should be returned.
* @returns {MessageString}
*/
/**
* An object with `constraintName`s as keys and {@link MessageString}s or {@link MessageFunction}s as value.
*
* @typedef {Object.<string, (MessageString|MessageFunction)>} MessageObject
* @property {MessageString} [{@link Checker|`constraintName`}] A constraintName-MessageString pair.
*/
/**
* The options object to pass to the {@link Isntit} constructor.
*
* @typedef {Object} Options
* @property {boolean} [capitalize=true] Whether to capitalize the first character of error messages.
* @property {boolean} [devtools=true] Whether to enable devtools (only for development build).
* @property {boolean} [fullMessages=false] Whether to prefix error messages with corresponding field name.
* @property {Object} [config] The {@link config} properties to override.
*/
Isntit.prototype.options = {
capitalize: true,
devtools: config.env !== 'production',
fullMessages: false
};
/**
* Collection of {@link Checker|checker}s ordered in separated steps.
* The validation process simply loops over this Object. Step names have no effect
* on the order.
*
* @typedef {Object} Checkers
* @property {Step} before A collection of checkers to call 'before' validation.
* @property {Step} during A collection of checkers called 'during' validation.
* @property {Step} [`stepName`] A collection of checkers to call in the 'stepName'.
*/
/**
* A collection of {@link Checker|checker}s. Validation happens by steps. If one rule fails during current step, validation ends at the end of this step.
*
* @typedef {Object.<string, Checker>} Step
*/
/**
* A checker is responsible for the validation of data. It holds the validation logic and the optional pre-validation check and type rules.
*
* @typedef {Object.<string, (Object|function)>} Checker
* @property {(function|Object.<string, function>)} validate The logic that validates the value. Receives value and the current {@link Context|context} as arguments.
* @property {function} [preporcess] A function that is called before actually validating the value. This is generaly used to check the type of given value and fail if the type is wrong without even trying to validate it.
* @property {TypeRule} [types] When devtools are enabled, {@link Constraints|field rules} are checked with {@link checkType} to enforce the right types of constraints values.
* @example
* length: {
* validate: {
* is: function(value, context) {
* return (value.length == context.ruleSet['length'].is);
* },
* min: function(value, context) {
* return (value.length >= context.ruleSet['length'].min);
* },
* max: function(value, context) {
* return (value.length <= context.ruleSet['length'].max);
* }
* },
* preprocess: function(value, context) {
* var I = this;
* if (value.length === undefined) {
* I.cache.messages['length::preprocess'] = 'Values checked for length MUST have a length property. Given: ' + value;
* return false;
* }
* return true;
* },
* types: {
* __all: 'number'
* }
* }
*/
/**
* The default checker collection.
*
* @type {Checkers}
* @property {Step} before Checkers that are called before any other validation.
* @property {Checker} before.confirms Check that the current field value is equal to the value of the field constraint. {@link confirms}
* @property {Checker} before.require Check that given value {@link Isntit.isEmpty|is not empty}. {@link require}
* @property {Step} during The main validation step.
* @property {Checker} during.email Check given value against the {@link Config|config.emailRE} RegExp. {@link email}
* @property {Checker} during.format Check given value against the pattern constraint. {@link format}
* @property {Checker} during.length Check the length of given value. {@link length}
* @property {Checker} during.numeric Compare given value against the rule constraints. {@link numeric}
* @see Constraints
*/
export var checkers = {
before: {
confirms: confirms,
required: required
},
during: {
email: email,
format: format,
length: length,
numeric: numeric
}
}
/**
* Get the collection of steps and checkers currently in use.
*
* @returns {Checkers} The current Checkers.
*
* @see Step
*/
Isntit.getCheckers = function() {
return checkers;
}
/**
* Register a checker. You may add a checker to a new or existing step or override an existing checker.
*
* @param {Checker} checker The checker to register.
* @param {string} name The name to use.
* @param {string} step The step to register this checker to
* @param {string[]} checkersSteps A list of steps to use dureing validation. Note that the order matters.
* @see Step
*/
Isntit.registerChecker = function(checker, name, step, checkersSteps) {
var I = this;
if (typeof checker !== 'function' && !hasOwn(checker, 'validate')) {
if (arguments.length > 3) {
warn('Instance methode registerChecker() signature is:\n(object[, step[, checkersSteps]]), supplemental arguments will be ignored.', Array.from(arguments));
}
for(var key in checker) {
checkersSteps = step;
if(config.checkersSteps.indexOf(key) !== -1) {
step = key;
for(name in checker[step]) {
I.registerChecker(checker[step][name], name, step, checkersSteps);
}
} else {
step = name;
I.registerChecker(checker[key], key, step, checkersSteps);
}
}
} else {
step = step || 'during';
if (typeof name === 'undefined') {
throw new Error('When registering a callback as checker, you must provide a name for it: registerChecker(callable, name[, step[, checkersSteps]])');
}
if(!hasOwn(checkers, step)) {
checkers[step] = {};
if (typeof checkersSteps === 'undefined') {
config.checkersSteps.push(step);
}
}
if (hasOwn(checker, 'validate')) {
checkers[step][name] = checker;
} else {
checkers[step][name] = {
validate: checker
};
}
}
}
/**
* Check if an object is considered as empty.
*
* @param {*} obj Any javascript object
*
* @returns {boolean} Return if given object is empty or not.
*
* @see {@link Config|config.emptyStringRE}
*/
Isntit.isEmpty = function(obj) {
var typeOf = typeof obj;
if ((typeOf === 'string' || typeOf === 'array') && obj.length === 0) {
return true;
}
if ((typeOf === 'string') && config.emptyStringRE.test(obj)) {
return true;
}
if (isObject(obj) && Object.keys(obj).length === 0) {
return true;
}
for (var i = 0; i < config.emptyValues.length; i++) {
if (obj === config.emptyValues[i]) {
return true;
}
}
return false;
}
// Wraper to string.printf
// Documentation: see './string.printf'
Isntit.printf = printf;
// Wraper to string.ucfirst
// Documentation: see './string.ucfirst'
Isntit.ucfirst = ucfirst;
/**
* Set that an error occured in given context durring current validation.
*
* @param {Context} context The Context in which the error occured.
* @param {string} fieldName The name of the field under validation.
* @param {string} ruleName The name of the checker currently called.
* @param {string} [constraintName] The name of the constraint of the checker currently called.
*
* @private
*/
function _setError(context, fieldName, ruleName, constraintName) {
var I = this;
if (!hasOwn(I.errors, fieldName)) {
I.errors[fieldName] = {};
}
if (constraintName) {
if (!hasOwn(I.errors[fieldName], ruleName)) {
I.errors[fieldName][ruleName] = {};
}
I.errors[fieldName][ruleName][constraintName] = true;
} else {
I.errors[fieldName][ruleName] = true;
}
}
/**
* Check given value against given {@link Checker|checker} or its appropriated {@link Constraints|constraints}.
*
* @param {Checker} checker The Checker currently to call.
* @param {*} value The value that must be checked.
* @param {Context} context The context in which the value has to be checked.
*
* @private
*/
function _callChecker(checker, value, context) {
var I = this;
if (typeof checker.validate === 'function') {
if (!checker.validate.call(I, value, context)) {
_setError.call(I, context, context.fieldName, context.ruleName);
}
} else {
for (var constraint in context.ruleSet[context.ruleName]) {
if (!checker.validate[constraint].call(I, value, context)) {
_setError.call(I, context, context.fieldName, context.ruleName, constraint);
}
}
}
}
/**
* Get the appropriated error message in given {@link Context|context}.
* Error messages are cached if not already and retrieved from cache.
*
* @param {Context} context The Context in which the error happened.
* @param {string} [constraint] The name of the Constraint where the error happened.
*
* @returns {string} The appropriated error message.
*
* @private
*/
function _getMsg(context, constraint) {
var I = this;
var key = context.ruleName + (constraint ? '::' + constraint : '');
if (!I.cache.messages[key]) {
var message;
// Get the message from possible sources
message = context.ruleSet[context.ruleName].message ||
config.messages[context.ruleName] ||
config.messages.notValid;
// If constraint exists and is a prop on message, retrieve it or invalid
if (isObject(message)) {
message = message[constraint] || config.messages.notValid;
}
// If message is a function call it else asume its just a string
I.cache.messages[key] = (typeof message === 'function') ?
message(context, constraint) :
message;
}
return I.cache.messages[key];
}
/**
* Handle, prepare and set error messages for display.
*
* @param {string} fieldName The name of the field currently under validation.
* @param {*} value The value of the field currently under validation.
* @param {Context} context The current Context in which value is validated.
*
* @private
*/
function _handleErrors(fieldName, value, context) {
var I = this;
var message = [];
// Loop through rules
for(var ruleName in I.errors[fieldName]) {
if (I.errors[fieldName][ruleName] === true) {
message.push(_getMsg.call(I, context));
} else {
for(var constraint in I.errors[fieldName][ruleName]) {
message.push(_getMsg.call(I, context, constraint));
}
}
}
// Flatten message
message = message.join(', ');
// Prepare replacements for parsing
var replacements = {
value: context.value,
label: context.ruleSet.label || context.fieldName
};
// Add properties from rule definition :
// length.min = 1, numeric.noStrings = false,...
Object.assign(replacements, context.ruleSet[context.ruleName]);
// Should the message be prepended with %{label}
var noPrepend = message.charAt(0) === config.noLabelChar;
var fullMessage = context.rules[context.fieldName][context.ruleName]['fullMessage'] || I.options.fullMessages;
if (fullMessage && !noPrepend) {
message = '%{label} ' + message;
}
if (noPrepend) {
message = message.substr(1);
}
// Parse message
message = printf(message, replacements);
// Should the message be prepended capitalized
var capitalize = context.rules[context.fieldName][context.ruleName]['capitalize'] || I.options.capitalize;
if (capitalize) {
message = ucfirst(message);
}
// Replace array of messages with parsed message
I.errors[context.fieldName] = message;
}
/**
* The context in which the validate function of a checker is called (passed as second argument).
*
* @typedef {Object} Context
* @property {*} value The value of the field under validation.
* @property {string} `fieldName` The name of the field under validation.
* @property {Object.<string, *>} data The whole data object that is currently validated.
* @property {string} `ruleName` The name of the checker currently called.
* @property {Object.<string, Constraints>} ruleSet The collection of rules to check current field against.
* @property {Rules} rules All the rules curently used during validation.
* @property {string} step The name of the current {@link Step|step}.
*
* @see Checker
*/
/**
* Looping through all registered checkers, step by step, to validate the given value.
*
* @param {*} value The value under validation.
* @param {string} fieldName The name of the field under validation.
* @param {Object.<string, *>} data The data under validation.
* @param {Rules} rules The rules the data must comply with.
*
* @private
*/
function _loopThroughCheckers(value, fieldName, data, rules) {
var I = this;
for (var i = 0; i < config.checkersSteps.length; i++) {
var step = config.checkersSteps[i];
// Looping on rules for current data field
for (var ruleName in rules[fieldName]) {
// Select the right checker
var checker = checkers[step][ruleName];
if (typeof checker !== 'undefined') {
// Exit loop and warn if rule === false
if (rules[fieldName][ruleName] === false) {
warn(`Rule definition for '${ruleName}' set to 'false', are you shure?`, rules[fieldName]);
break;
}
// Set the current context
var context = {
value: value,
fieldName: fieldName,
data: data,
ruleName: ruleName,
ruleSet: rules[fieldName],
rules: rules,
step: step
};
// Call preprocess
if (typeof checker.preprocess === "function" && !checker.preprocess.call(I, value, context)) {
_setError.call(I, context, context.fieldName, context.ruleName, 'preprocess');
} else {
_callChecker.call(I, checker, value, context);
}
if (hasOwn(I.errors, fieldName)) {
_handleErrors.call(I, fieldName, value, context);
}
}
}
if (typeof I.errors[fieldName] !== 'undefined') {
break;
}
}
}
/**
* Validate given data.
*
* @param {Object.<string, *>} data The data to validate.
* @param {Rules} [rules=this.rules] The rules to validate aginst. If omited, the rules passed to the constructor are used.
*
* @returns {boolean|Object.<string, string>} Returns true if all data validates or a collection of fieldName-errorMessage pairs.
*/
Isntit.prototype.validate = function(data, rules) {
var I = this;
if (rules) {
I.checkRules(rules);
} else {
rules = I.rules;
}
I.errors = {};
// Looping on fields in data
for (var fieldName in data) {
if(hasOwn(data, fieldName)) {
// Check if it is a shorthand for confirms
if (rules[fieldName] === true) {
var matches = fieldName.match(config.confirmationRE);
if (matches) {
rules[fieldName] = {
confirms: {
field: matches[1]
}
}
}
}
// Get the value to check
var value = data[fieldName];
_loopThroughCheckers.call(I, value, fieldName, data, rules);
}
}
return (Isntit.isEmpty(I.errors)) ? true : I.errors;
}
Isntit.prototype.getCheckers = function() {
return checkers;
}
Isntit.prototype.getMessages = function() {
return this.errors;
}
Isntit.prototype.getStep = function(ruleName) {
var I = this;
if (!I.cache.checkersToStep[ruleName]) {
for (var i = 0; i < config.checkersSteps.length; i++) {
if (checkers[config.checkersSteps[i]][ruleName]) {
I.cache.checkersToStep[ruleName] = config.checkersSteps[i];
break;
}
}
if (!I.cache.checkersToStep[ruleName]) {
warn('No step found for "' + ruleName + '". Given: ' + JSON.stringify(checkers));
return false;
}
}
return I.cache.checkersToStep[ruleName];
}
export default Isntit;