From 7fc96ec9555e48cf46c96c52f2e5b9bbb76307c6 Mon Sep 17 00:00:00 2001 From: Philip Diffenderfer Date: Thu, 26 May 2016 01:18:33 -0400 Subject: [PATCH] Resolves #198 --- .gitignore | 1 + .npmignore | 0 .travis.yml | 5 + Gulpfile.js | 63 + LICENSE | 21 + README.md | 18 + bower.json | 25 + build/rekord-validation.js | 2013 +++++++++ build/rekord-validation.min.js | 2 + build/rekord-validation.min.js.map | 1 + jsdoc.json | 29 + package.json | 29 + src/footer.js | 27 + src/header.js | 33 + src/lib/expressions/date.js | 15 + src/lib/expressions/field.js | 11 + src/lib/expressions/relative.js | 65 + src/lib/expressions/today.js | 15 + src/lib/expressions/tomorrow.js | 16 + src/lib/expressions/yesterday.js | 16 + src/lib/rules/accepted.js | 32 + src/lib/rules/collection.js | 135 + src/lib/rules/dates.js | 111 + src/lib/rules/field_list.js | 78 + src/lib/rules/fields.js | 180 + src/lib/rules/foreign.js | 109 + src/lib/rules/if.js | 92 + src/lib/rules/list.js | 88 + src/lib/rules/range.js | 90 + src/lib/rules/regex.js | 96 + src/lib/rules/required.js | 7 + src/lib/rules/sizes.js | 119 + src/lib/rules/types.js | 96 + src/lib/transforms/abs.js | 14 + src/lib/transforms/apply.js | 9 + src/lib/transforms/base64.js | 12 + src/lib/transforms/ceil.js | 14 + src/lib/transforms/endOfDay.js | 7 + src/lib/transforms/filter.js | 28 + src/lib/transforms/floor.js | 14 + src/lib/transforms/mod.js | 21 + src/lib/transforms/null.js | 9 + src/lib/transforms/round.js | 14 + src/lib/transforms/startOfDay.js | 7 + src/lib/transforms/trim.js | 23 + src/lib/transforms/unbase64.js | 12 + src/lib/util.js | 151 + src/lib/validation.js | 212 + test/base.js | 521 +++ test/cases/rekord-validation.js | 6765 ++++++++++++++++++++++++++++ test/index.html | 35 + 51 files changed, 11506 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 Gulpfile.js create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bower.json create mode 100644 build/rekord-validation.js create mode 100644 build/rekord-validation.min.js create mode 100644 build/rekord-validation.min.js.map create mode 100644 jsdoc.json create mode 100644 package.json create mode 100644 src/footer.js create mode 100644 src/header.js create mode 100644 src/lib/expressions/date.js create mode 100644 src/lib/expressions/field.js create mode 100644 src/lib/expressions/relative.js create mode 100644 src/lib/expressions/today.js create mode 100644 src/lib/expressions/tomorrow.js create mode 100644 src/lib/expressions/yesterday.js create mode 100644 src/lib/rules/accepted.js create mode 100644 src/lib/rules/collection.js create mode 100644 src/lib/rules/dates.js create mode 100644 src/lib/rules/field_list.js create mode 100644 src/lib/rules/fields.js create mode 100644 src/lib/rules/foreign.js create mode 100644 src/lib/rules/if.js create mode 100644 src/lib/rules/list.js create mode 100644 src/lib/rules/range.js create mode 100644 src/lib/rules/regex.js create mode 100644 src/lib/rules/required.js create mode 100644 src/lib/rules/sizes.js create mode 100644 src/lib/rules/types.js create mode 100644 src/lib/transforms/abs.js create mode 100644 src/lib/transforms/apply.js create mode 100644 src/lib/transforms/base64.js create mode 100644 src/lib/transforms/ceil.js create mode 100644 src/lib/transforms/endOfDay.js create mode 100644 src/lib/transforms/filter.js create mode 100644 src/lib/transforms/floor.js create mode 100644 src/lib/transforms/mod.js create mode 100644 src/lib/transforms/null.js create mode 100644 src/lib/transforms/round.js create mode 100644 src/lib/transforms/startOfDay.js create mode 100644 src/lib/transforms/trim.js create mode 100644 src/lib/transforms/unbase64.js create mode 100644 src/lib/util.js create mode 100644 src/lib/validation.js create mode 100644 test/base.js create mode 100644 test/cases/rekord-validation.js create mode 100644 test/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..e69de29 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d4a62c7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - "0.10" +before_script: + - npm install -g gulp \ No newline at end of file diff --git a/Gulpfile.js b/Gulpfile.js new file mode 100644 index 0000000..72f3610 --- /dev/null +++ b/Gulpfile.js @@ -0,0 +1,63 @@ +'use strict'; + +var gulp = require('gulp'); +var sourcemaps = require('gulp-sourcemaps'); +var plugins = require('gulp-load-plugins')(); +var gutil = require('gulp-util'); +var qunit = require('gulp-qunit'); +var shell = require('gulp-shell'); +var merge = require('merge-stream'); +var size = require('gulp-check-filesize'); + +var build = { + filename: 'rekord-validation.js', + minified: 'rekord-validation.min.js', + output: './build/', + include: [ + './src/header.js', + './src/lib/**/*.js', + './src/footer.js' + ] +}; + +var executeMinifiedBuild = function(props) +{ + return function() { + return gulp + .src( props.include ) + .pipe( sourcemaps.init() ) + .pipe( plugins.concat( props.minified ) ) + .pipe( plugins.uglify().on('error', gutil.log) ) + .pipe( sourcemaps.write('.') ) + .pipe( size({enableGzip: true}) ) + .pipe( gulp.dest( props.output ) ) + ; + }; +}; + +var executeBuild = function(props) +{ + return function() { + return gulp + .src( props.include ) + .pipe( plugins.concat( props.filename ) ) + .pipe( size({enableGzip: true}) ) + .pipe( gulp.dest( props.output ) ) + ; + }; +}; + +var executeTest = function(file) +{ + return function() { + return gulp.src( file ).pipe( qunit() ); + }; +}; + +gulp.task( 'docs', shell.task(['./node_modules/.bin/jsdoc -c jsdoc.json'])); +gulp.task( 'clean', shell.task(['rm -rf build/*.js', 'rm -rf build/*.map'])); +gulp.task( 'test', executeTest( './test/index.html' ) ); + +gulp.task( 'js:min', executeMinifiedBuild( build ) ); +gulp.task( 'js', executeBuild( build ) ); +gulp.task( 'default', ['js:min', 'js']); diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e44b13a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2015-2016 Magnos, LLC. https://github.com/Rekord/rekord-validation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f86bf1f --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# Rekord Validation + +[![Build Status](https://travis-ci.org/Rekord/rekord-validation.svg?branch=master)](https://travis-ci.org/Rekord/rekord-validation) +[![devDependency Status](https://david-dm.org/Rekord/rekord-validation/dev-status.svg)](https://david-dm.org/Rekord/rekord-validation#info=devDependencies) +[![Dependency Status](https://david-dm.org/Rekord/rekord-validation.svg)](https://david-dm.org/Rekord/rekord-validation) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Rekord/rekord-validation/blob/master/LICENSE) +[![Alpha](https://img.shields.io/badge/State-Alpha-orange.svg)]() + +Rekord is a javascript REST ORM that is offline and real-time capable. + +rekord-validation adds rules, expressions, transforms, and custom validation functionality. + +**Installation** + +The easiest way to install rekord-validation is through bower via `bower install rekord-validation`. + +- rekord-validation.js is `46KB` (`7.55KB` gzipped) +- rekord-validation.min.js is `18KB` (`5.24KB` gzipped) diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..91aa9a9 --- /dev/null +++ b/bower.json @@ -0,0 +1,25 @@ +{ + "name": "rekord-validation", + "version": "1.0.0", + "homepage": "https://github.com/Rekord/rekord-validation", + "authors": [ + "Philip Diffenderfer " + ], + "description": "Advanced validation rules for rekord", + "main": "build/rekord-validation.js", + "keywords": [ + "javascript", + "orm", + "offline", + "realtime", + "validation" + ], + "license": "MIT", + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/build/rekord-validation.js b/build/rekord-validation.js new file mode 100644 index 0000000..f1cefe9 --- /dev/null +++ b/build/rekord-validation.js @@ -0,0 +1,2013 @@ +(function(global, Rekord, undefined) +{ + var Model = Rekord.Model; + var Database = Rekord.Database; + var Promise = Rekord.Promise; + var Collection = Rekord.Collection; + + var isEmpty = Rekord.isEmpty; + var isString = Rekord.isString; + var isArray = Rekord.isArray; + var isObject = Rekord.isObject; + var isFunction = Rekord.isFunction; + var isDate = Rekord.isDate; + var isNumber = Rekord.isNumber; + var isBoolean = Rekord.isBoolean; + var isValue = Rekord.isValue; + var isPrimitiveArray = Rekord.isPrimitiveArray; + var isRegExp = Rekord.isRegExp; + + var noop = Rekord.noop; + var equalsCompare = Rekord.equalsCompare; + var equals = Rekord.equals; + var indexOf = Rekord.indexOf; + var sizeof = Rekord.sizeof; + + var split = Rekord.split; + var transfer = Rekord.transfer; + var format = Rekord.format; + + var parseDate = Rekord.parseDate; + + var addMethod = Rekord.addMethod; + var replaceMethod = Rekord.replaceMethod; + +function tryParseFloat(x) +{ + var parsed = parseFloat( x ); + + if ( !isNaN( parsed ) ) + { + x = parsed; + } + + return x; +} + +function tryParseInt(x) +{ + var parsed = parseInt( x ); + + if ( !isNaN( parsed ) ) + { + x = parsed; + } + + return x; +} + +function startOfDay(d) +{ + if ( isDate( d ) ) + { + d.setHours( 0, 0, 0, 0 ); + } + else if ( isNumber( d ) ) + { + d = d - (d % 86400000); + } + + return d; +} + +function endOfDay(d) +{ + if ( isDate( d ) ) + { + d.setHours( 23, 59, 59, 999 ); + } + else if ( isNumber( d ) ) + { + d = d - (d % 86400000) + 86400000 - 1; + } + + return d; +} + +function ruleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + checkNoParams( ruleName, field, params ); + + var messageTemplate = determineMessage( ruleName, message ); + + return function(value, model, setMessage) + { + function setValue( newValue ) + { + value = newValue; + } + + if ( isInvalid( value, model, setValue ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} + +function determineMessage(ruleName, message) +{ + return message || Validation.Rules[ ruleName ].message; +} + +function joinFriendly(arr, lastSeparator, itemSeparator, getAlias) +{ + var copy = arr.slice(); + + if ( getAlias ) + { + for (var i = 0; i < copy.length; i++) + { + copy[ i ] = getAlias( copy[ i ] ); + } + } + + var last = copy.pop(); + var lastSeparator = lastSeparator || 'and'; + var itemSeparator = itemSeparator || ', '; + + switch (copy.length) { + case 0: + return last; + case 1: + return copy[ 0 ] + ' ' + lastSeparator + ' ' + last; + default: + return copy.join( itemSeparator ) + itemSeparator + lastSeparator + ' ' + last; + } +} + +function mapFromArray(arr, value) +{ + var map = {}; + + for (var i = 0; i < arr.length; i++) + { + map[ arr[ i ] ] = value; + } + + return map; +} + +function checkNoParams(ruleName, field, params) +{ + if ( params ) + { + throw 'the rule ' + ruleName + ' for field ' + field + ' has no arguments'; + } +} + +function generateMessage(field, alias, value, model, message, extra) +{ + if ( isFunction( message ) ) + { + message = message( field, alias, value, model, extra ); + } + + var base = {}; + base.$field = field; + base.$alias = alias; + base.$value = value; + + transfer( model, base ); + + if ( isObject( extra ) ) + { + transfer( extra, base ); + } + + return format( message, base ); +} + +Rekord.on( Rekord.Events.Plugins, function(model, db, options) +{ + var validation = options.validation || Database.Defaults.validation; + + if ( isEmpty( validation ) ) + { + return; + } + + var rules = validation.rules || {}; + var messages = validation.messages || {}; + var aliases = validation.aliases || {}; + var required = !!validation.required; + + function getAlias(field) + { + return aliases[ field ] || field; + } + + db.validations = {}; + + for ( var field in rules ) + { + db.validations[ field ] = Validation.parseRules( rules[ field ], field, db, getAlias, messages[ field ] ) + } + + addMethod( model.prototype, '$validate', function() + { + var $this = this; + + this.$trigger( Model.Events.PreValidate, [this] ); + + this.$valid = true; + this.$validations = {}; + this.$validationMessages.length = 0; + + for (var field in db.validations) + { + var chain = db.validations[ field ]; + var value = this.$get( field ); + var fieldValid = true; + + var setMessage = function(message) + { + // Only accept for the first valid message + if ( message && fieldValid ) + { + fieldValid = false; + + $this.$validations[ field ] = message; + $this.$validationMessages.push( message ); + $this.$valid = false; + } + }; + + for (var i = 0; i < chain.length && fieldValid && value !== Validation.Stop; i++) + { + value = chain[ i ]( value, this, setMessage ); + } + } + + this.$trigger( this.$valid ? Model.Events.ValidatePass : Model.Events.ValidateFail, [this] ); + + return this.$valid; + }); + + replaceMethod( model.prototype, '$init', function($init) + { + return function() + { + this.$valid = undefined; + this.$validations = {}; + this.$validationMessages = []; + + return $init.apply( this, arguments ); + }; + }); + + if ( required ) + { + replaceMethod( model.prototype, '$save', function($save) + { + return function() + { + if ( this.$isDeleted() ) + { + Rekord.debug( Rekord.Debugs.SAVE_DELETED, this.$db, this ); + + return Promise.resolve( this ); + } + + if ( !this.$validate() ) + { + return Promise.resolve( this ); + } + + return $save.apply( this, arguments ); + }; + }); + } +}); + +Model.Events.PreValidate = 'pre-validate'; + +Model.Events.ValidatePass = 'validate-pass'; + +Model.Events.ValidateFail = 'validate-fail'; + +var Validation = +{ + Rules: {}, + Expression: {}, + Expressions: [], + Delimiter: /([|])/, + Escape: '\\', + RuleSeparator: ':', + Stop: {}, + + parseRules: function(rules, field, database, getAlias, message) + { + var validators = []; + + if ( isString( rules ) ) + { + rules = split( rules, this.Delimiter, this.Escape ); + } + + if ( isArray( rules ) ) + { + for (var i = 0; i < rules.length; i++) + { + var rule = rules[ i ]; + var validator = this.parseRule( rule, field, database, getAlias, message ); + + validators.push( validator ); + } + } + else if ( isObject( rules ) ) + { + for (var rule in rules) + { + var ruleMessageOrData = rules[ rule ]; + + var ruleMessage = isObject( ruleMessageOrData ) ? ruleMessageOrData.message : + ( isString( ruleMessageOrData ) ? ruleMessageOrData : undefined ); + + var ruleInput = isObject( ruleMessageOrData ) && ruleMessageOrData.message ? ruleMessageOrData.input : + ( isString( ruleMessageOrData ) ? undefined : ruleMessageOrData ); + + var validator = this.parseRule( rule, field, database, getAlias, ruleMessage || message, ruleInput ); + + validators.push( validator ); + } + } + + return validators; + }, + + parseRule: function(rule, field, database, getAlias, message, input) + { + var colon = rule.indexOf( this.RuleSeparator ); + var ruleName = colon === -1 ? rule : rule.substring( 0, colon ); + + if ( ruleName.charAt( 0 ) === '$' ) + { + return this.customValidator( ruleName, field, database, getAlias, message ); + } + + var ruleParams = colon === -1 ? input : rule.substring( colon + 1 ); + var validatorFactory = Validation.Rules[ ruleName ]; + + if ( !validatorFactory ) + { + throw ruleName + ' is not a valid rule'; + } + + return validatorFactory( field, ruleParams, database, getAlias, message ); + }, + + parseExpression: function(expr, database) + { + var parsers = Validation.Expressions; + + for (var i = 0; i < parsers.length; i++) + { + var parser = parsers[ i ]; + var expressionFunction = parser( expr, database ); + + if ( isFunction( expressionFunction ) ) + { + return expressionFunction; // (value, model) + } + } + + return noop; + }, + + customValidator: function(functionName, field, database, getAlias, message) + { + return function(value, model, setMessage) + { + var result = model[ functionName ]( value, getAlias, message ); + + if ( isString( result ) ) + { + setMessage( result ); + } + + return value; + }; + } +}; + +Validation.Expression.date = +Validation.Expressions.push(function(expr, database) +{ + var parsed = parseDate( expr ); + + if ( parsed !== false ) + { + var parsedTime = parsed.getTime(); + + return function(value, model) + { + return parsedTime; + }; + } +}) - 1; + +Validation.Expression.field = +Validation.Expressions.push(function(expr, database) +{ + if ( indexOf( database.fields, expr ) ) + { + return function(value, model) + { + return model.$get( expr ); + }; + } +}) - 1; + + +var RELATIVE_REGEX = /^([+-]\d+(\.\d+)?)\s*(.+)$/; + +var RELATIVE_UNITS = { + ms: 1, + millisecond: 1, + milliseconds: 1, + s: 1000, + second: 1000, + seconds: 1000, + min: 1000 * 60, + mins: 1000 * 60, + minute: 1000 * 60, + minutes: 1000 * 60, + hr: 1000 * 60 * 60, + hour: 1000 * 60 * 60, + hours: 1000 * 60 * 60, + day: 1000 * 60 * 60 * 24, + days: 1000 * 60 * 60 * 24, + wk: 1000 * 60 * 60 * 24 * 7, + week: 1000 * 60 * 60 * 24 * 7, + weeks: 1000 * 60 * 60 * 24 * 7, + month: ['getMonth', 'setMonth'], + months: ['getMonth', 'setMonth'], + yr: ['getFullYear', 'setFullYear'], + year: ['getFullYear', 'setFullYear'], + years: ['getFullYear', 'setFullYear'] +}; + +Validation.Expression.relative = +Validation.Expressions.push(function(expr, database) +{ + var parsed = RELATIVE_REGEX.exec( expr ); + + if ( parsed !== null ) + { + var amount = parseFloat( parsed[ 1 ] ); + var unit = parsed[ 3 ]; + var unitScale = RELATIVE_UNITS[ unit ]; + + if ( !unitScale ) + { + throw unit + ' is not a valid unit.'; + } + + return function(value, model) + { + var relative = new Date(); + + if ( isNumber( unitScale ) ) + { + relative.setTime( relative.getTime() + unitScale * amount ); + } + else + { + var getter = unitScale[0]; + var setter = unitScale[1]; + + relative[ setter ]( relative[ getter ]() + amount ); + } + + return relative.getTime(); + }; + } +}) - 1; + +Validation.Expression.today = +Validation.Expressions.push(function(expr, database) +{ + if ( expr === 'today' ) + { + return function(value, model) + { + var today = new Date(); + + startOfDay( today ); + + return today.getTime(); + }; + } +}) - 1; + +Validation.Expression.tomorrow = +Validation.Expressions.push(function(expr, database) +{ + if ( expr === 'tomorrow' ) + { + return function(value, model) + { + var tomorrow = new Date(); + + tomorrow.setDate( tomorrow.getDate() + 1 ); + startOfDay( tomorrow ); + + return tomorrow.getTime(); + }; + } +}) - 1; + +Validation.Expression.yesterday = +Validation.Expressions.push(function(expr, database) +{ + if ( expr === 'yesterday' ) + { + return function(value, model) + { + var yesterday = new Date(); + + yesterday.setDate( yesterday.getDate() - 1 ); + startOfDay( yesterday ); + + return yesterday.getTime(); + }; + } +}) - 1; + +// accepted +Validation.Rules.accepted = function(field, params, database, getAlias, message) +{ + checkNoParams( 'accepted', field, params ); + + var messageTemplate = determineMessage( 'accepted', message ); + var acceptable = Validation.Rules.accepted.acceptable; + + return function(value, model, setMessage) + { + var valueString = (value + '').toLowerCase(); + var accepted = acceptable[ valueString ]; + + if ( !accepted ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) ); + } + + return value; + }; +}; + +Validation.Rules.accepted.message = '{$alias} has not been accepted.'; + +Validation.Rules.accepted.acceptable = +{ + '1': true, + 'yes': true, + 'on': true, + 'y': true, + 'true': true +}; + +// contains:field,value +collectionRuleGenerator('contains', + '{$alias} does not contain an item whose {$matchAlias} equals {$matchValue}.', + function isInvalid(value, model, matchField, matchValue, equality) + { + return !value.contains(function isMatch(m) + { + return m !== model && equality( matchValue, m.$get( matchField ) ); + }); + } +); + +// not_contains:field,value +collectionRuleGenerator('not_contains', + '{$alias} contains an item whose {$matchAlias} equals {$matchValue}.', + function isInvalid(value, model, matchField, matchValue, equality) + { + return value.contains(function isMatch(m) + { + return m !== model && equality( matchValue, m.$get( matchField ) ); + }); + } +); + +function collectionRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires field & value arguments'; + } + + var matchField, matchValue, equality; + + if ( isString( params ) ) + { + var comma = params.indexOf(','); + + if ( comma === -1 ) + { + throw ruleName + ' validation rule requires field & value arguments'; + } + + matchField = params.substring( 0, comma ); + matchValue = params.substring( comma + 1 ); + } + else if ( isArray( params ) ) + { + matchField = params[ 0 ]; + matchValue = params[ 1 ]; + equality = params[ 2 ]; + } + else if ( isObject( params ) ) + { + matchField = params.field; + matchValue = params.value; + equality = params.equals; + } + + if ( !isFunction( equality ) ) + { + equality = equalsCompare; + } + + if ( indexOf( database.fields, matchField ) === -1 ) + { + throw otherField + ' is not a valid field for the ' + ruleName + ' rule'; + } + + var messageTemplate = determineMessage( ruleName, message ); + var extra = { + $matchField: matchField, + $matchAlias: getAlias( matchField ), + $matchValue: matchValue + }; + + return function(value, model, setMessage) + { + if ( isInvalid( value, model, matchField, matchValue, equality ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} + +Validation.Rules.validate = function(field, params, database, getAlias, message) +{ + // message, models, validations + var messageOption = params || 'message'; + var messageTemplate = determineMessage( 'validate', message ); + + return function(value, model, setMessage) + { + if ( isArray( value ) ) + { + var invalid = new Collection(); + + for (var i = 0; i < value.length; i++) + { + var model = value[ i ]; + + if ( model && model.$validate && !model.$validate() ) + { + invalid.push( model ); + } + } + + if ( invalid.length ) + { + switch (messageOption) + { + case 'models': + setMessage( invalid ); + break; + case 'validations': + setMessage( invalid.pluck( '$validations', '$$key' ) ); + break; + default: // message + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) ); + break; + } + } + } + + return value; + }; +}; + +Validation.Rules.validate.message = '{$alias} is not valid.'; + +// after:today +dateRuleGenerator('after', + '{$alias} must be after {$date}.', + function isInvalid(value, date) { + return value < endOfDay( date ); + } +); + +// after_on:tomorrow +dateRuleGenerator('after_on', + '{$alias} must be after or equal to {$date}.', + function isInvalid(value, date) { + return value < date; + } +); + +// before:yesterday +dateRuleGenerator('before', + '{$alias} must be before {$date}.', + function isInvalid(value, date) { + return value > date; + } +); + +// before_on:+2days +dateRuleGenerator('before_on', + '{$alias} must be before or equal to {$date}.', + function isInvalid(value, date) { + return value > endOfDay( date ); + } +); + +// date +ruleGenerator('date_like', + '{$alias} must be a valid date.', + function isInvalid(value, model, setValue) { + var parsed = parseDate( value ); + var invalid = parsed === false; + if ( !invalid ) { + setValue( parsed.getTime() ); + } + return invalid; + } +); + +function dateRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires a date expression argument'; + } + + var dateExpression; + + if ( isString( params ) ) + { + dateExpression = Validation.parseExpression( params, database ); + } + else if ( isFunction( params ) ) + { + dateExpression = params; + } + else + { + var parsed = parseDate( params ); + + if ( parsed !== false ) + { + var parsedTime = parsed.getTime(); + + dateExpression = function() + { + return parsedTime; + }; + } + } + + if ( !dateExpression || dateExpression === noop ) + { + throw params + ' is not a valid date expression for the ' + ruleName + ' rule'; + } + + var messageTemplate = determineMessage( ruleName, message ); + var extra = { + $date: params + }; + + return function(value, model, setMessage) + { + var parsed = parseDate( value ); + + if ( parsed !== false ) + { + value = parsed.getTime(); + + var date = dateExpression( value, model ); + + if ( isNumber( date ) && isInvalid( value, date ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} + + +// required_if:X,Y,... +fieldListRuleGenerator('required_if', + '{$alias} is required.', + function isInvalid(value, model, field, values, map) { + var required = map[ model.$get( field ) ]; + + return required && isEmpty( value ); + } +); + +// required_unless:X,Y,... +fieldListRuleGenerator('required_unless', + '{$alias} is required.', + function isInvalid(value, model, field, values, map) { + var required = !map[ model.$get( field ) ]; + + return required && isEmpty( value ); + } +); + +function fieldListRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires a field and list arguments'; + } + + var matchField, matchValues; + + if ( isString( params ) ) + { + var parts = split( params, /(,)/, '\\' ); + + matchField = parts.shift(); + matchValues = parts; + } + else if ( isArray( params ) ) + { + matchField = params.shift(); + matchValues = params; + } + else if ( isObject( params ) ) + { + matchField = params.field; + matchValues = params.values; + } + + if ( indexOf( database.fields, matchField ) === false ) + { + throw matchField + ' is not a valid field for the ' + ruleName + ' rule'; + } + + var messageTemplate = determineMessage( ruleName, message ); + var list = joinFriendly( matchValues ); + var extra = { + $params: params, + $matchField: matchField, + $matchAlias: getAlias( matchField ), + $list: list + }; + var map = mapFromArray( matchValues, true ); + + return function(value, model, setMessage) + { + if ( isInvalid( value, model, matchField, matchValues, map ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} + +// confirmed:X +fieldsRuleGenerator('confirmed', + '{$alias} must match {$fieldAliases}.', + function isInvalid(value, model, fields, setValue) { + var confirmed = true; + + for (var i = 0; i < fields.length; i++) + { + if ( !equals( value, model.$get( fields[ i ] ) ) ) + { + confirmed = false; + } + } + + return !confirmed; + } +); + +// different:X +fieldsRuleGenerator('different', + '{$alias} must not match {$fieldAliases}.', + function isInvalid(value, model, fields, setValue) { + var different = false; + + for (var i = 0; i < fields.length; i++) + { + if ( !equals( value, model.$get( fields[ i ] ) ) ) + { + different = true; + } + } + + return !different; + } +); + +// if_valid:X +fieldsRuleGenerator('if_valid', + '', + function isInvalid(value, model, fields, setValue) { + var valid = true; + + for (var i = 0; i < fields.length && valid; i++) + { + if ( model.$validations[ fields[ i ] ] ) + { + valid = false; + } + } + + if ( !valid ) + { + setValue( Validation.Stop ); + } + + return false; + } +); + +// The field under validation must be present only if any of the other specified fields are present. +// required_with:X,Y,... +fieldsRuleGenerator('required_with', + '{$alias} is required.', + function isInvalid(value, model, fields, setValue) { + var required = false; + + for (var i = 0; i < fields.length && !required; i++) + { + if ( !isEmpty( model.$get( fields[ i ] ) ) ) + { + required = true; + } + } + + return required && isEmpty( value ); + } +); + +// The field under validation must be present only if all of the other specified fields are present. +// required_with_all:X,Y,... +fieldsRuleGenerator('required_with_all', + '{$alias} is required.', + function isInvalid(value, model, fields, setValue) { + var required = true; + + for (var i = 0; i < fields.length && required; i++) + { + if ( isEmpty( model.$get( fields[ i ] ) ) ) + { + required = false; + } + } + + return required && isEmpty( value ); + } +); + +// The field under validation must be present only when any of the other specified fields are not present. +// required_without:X,Y,... +fieldsRuleGenerator('required_without', + '{$alias} is required.', + function isInvalid(value, model, fields, setValue) { + var required = false; + + for (var i = 0; i < fields.length && !required; i++) + { + if ( isEmpty( model.$get( fields[ i ] ) ) ) + { + required = true; + } + } + + return required && isEmpty( value ); + } +); + +// The field under validation must be present only when all of the other specified fields are not present. +// required_without_all:X,Y,... +fieldsRuleGenerator('required_without_all', + '{$alias} is required.', + function isInvalid(value, model, fields, setValue) { + var required = true; + + for (var i = 0; i < fields.length && required; i++) + { + if ( !isEmpty( model.$get( fields[ i ] ) ) ) + { + required = false; + } + } + + return required && isEmpty( value ); + } +); + +function fieldsRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires an array of fields argument'; + } + + var fields = split( params, /(\s*,\s*)/, '\\' ); + + for (var i = 0; i < fields.length; i++) + { + if ( indexOf( database.fields, fields[ i ] ) === -1 ) + { + throw fields[ i ] + ' is not a valid field for the ' + ruleName + ' rule'; + } + } + + var messageTemplate = determineMessage( ruleName, message ); + var fieldNames = joinFriendly( fields ); + var fieldAliases = joinFriendly( fields, false, false, getAlias ); + var extra = { + $fields: fieldNames, + $fieldAliases: fieldAliases + }; + + return function(value, model, setMessage) + { + function setValue( newValue ) + { + value = newValue; + } + + if ( isInvalid( value, model, fields, setValue ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +}; + +// exists:X,Y +foreignRuleGenerator('exists', + '{$alias} must match an existing {$matchAlias} in a {$class}', + function isInvalid(value, model, models, fieldName) + { + return !models.contains(function isDifferentMatch(m) + { + return m !== model && equals( value, m.$get( fieldName ) ); + }); + } +); + +// unique:X,Y +foreignRuleGenerator('unique', + '{$alias} must be a unique {$matchAlias} in a {$class}', + function isInvalid(value, model, models, fieldName) + { + return models.contains(function isDifferentMatch(m) + { + return m !== model && equals( value, m.$get( fieldName ) ); + }); + } +); + +// 'ruleName' +// 'ruleName:name' +// 'ruleName:,field' +// 'ruleName:name,field' +// 'ruleName:name,field': '...' +// 'ruleName': {input: {field: 'field', model: 'name'}, message: '...'} +// 'ruleName': {input: {field: 'field', model: ModelClass}, message: '...'} +// 'ruleName': {input: {field: 'field', models: [...]}, message: '...'} +// 'ruleName': {field: 'field', model: 'name'} +// 'ruleName': {field: 'field', model: ModelClass} +// 'ruleName': {field: 'field', models: [...]} +function foreignRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + var modelName, models, fieldName; + + if ( !isValue( params ) || isString( params ) ) + { + var parts = split( params || '', /(\s*,\s*)/, '\\' ); + modelName = parts[0] || database.name; + fieldName = parts[1] || field; + models = null; + } + else if ( isArray( params ) ) + { + modelName = isString( params[0] ) ? params.shift() : database.name; + fieldName = isString( params[0] ) ? params.shift() : field; + models = new ModelCollection( database, params ); + } + else if ( isObject( params ) ) + { + modelName = params.model || database.name; + fieldName = params.field || field; + models = params.models; + } + + if ( !models ) + { + if ( !modelName ) + { + throw 'model, model class, or models is required for ' + ruleName + ' rule'; + } + + if ( isString( modelName ) ) + { + Rekord.get( modelName ).success(function(modelClass) + { + models = modelClass.all(); + }); + } + else if ( isRekord( modelName ) ) + { + models = modelName.all(); + } + } + + if ( indexOf( database.fields, fieldName ) === false ) + { + throw fieldName + ' is not a valid field for the ' + ruleName + ' rule'; + } + + var messageTemplate = determineMessage( ruleName, message ); + var extra = { + $class: modelName, + $matchField: fieldName, + $matchAlias: getAlias( fieldName ) + }; + + return function(value, model, setMessage) + { + if ( models && isValue( value ) ) + { + if ( isInvalid( value, model, models, fieldName ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} + +// if:due_date:before:today|required + +// if all rules pass for the given field, continue with remaining rules +subRuleGenerator('if', + function isInvalid(invalidCount, totalCount) { + return invalidCount > 0; + } +); + +// if any rules pass for the given field, continue with remaining rules +subRuleGenerator('if_any', + function isInvalid(invalidCount, totalCount) { + return invalidCount >= totalCount; + } +); + +// if no rules pass for the given field, continue with remaining rules +subRuleGenerator('if_not', + function isInvalid(invalidCount, totalCount) { + return invalidCount < totalCount; + } +); + + + +function subRuleGenerator(ruleName, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires a validation rule argument'; + } + + var otherField, otherRules; + + if ( isString( params ) ) + { + var colon = params.indexOf( ':' ); + + if ( colon === -1 ) + { + throw params + ' is not a valid argument for the ' + ruleName + ' rule'; + } + + otherField = params.substring( 0, colon ) || field; + otherRules = params.substring( colon + 1 ); + } + else if ( isArray( params ) ) + { + otherField = params.shift() || field; + otherRules = params; + } + else if ( isObject( params ) ) + { + otherField = params.field || field; + otherRules = params.rules; + } + + if ( indexOf( database.fields, otherField ) === -1 ) + { + throw otherField + ' is not a valid field for the ' + ruleName + ' rule'; + } + + if ( !otherRules ) + { + throw 'rules are required for the ' + ruleName + ' rule'; + } + + var validators = Validation.parseRules( otherRules, otherField, database, getAlias ); + + return function(value, model, setMessage) + { + var invalids = 0; + + var setInvalid = function(message) + { + if ( message ) + { + invalids++; + } + }; + + for (var i = 0; i < validators.length; i++) + { + validators[ i ]( value, model, setInvalid ); + } + + return isInvalid( invalids, validators.length ) ? Validation.Stop : value; + }; + }; +} + +// in:X,Y,Z,... +listRuleGenerator('in', + '{$alias} must be one of {$list}.', + function isInvalid(value, model, inList) + { + return !inList( value, model ); + } +); + +// not_in:X,Y,Z,... +listRuleGenerator('not_in', + '{$alias} must not be one of {$list}.', + function isInvalid(value, model, inList) + { + return inList( value, model ) + } +); + +function listRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires a list argument'; + } + + var values, inList = false; + + if ( isString( params ) ) + { + values = split( params, /(,)/, '\\' ); + } + else if ( isArray( params ) ) + { + values = params; + } + else if ( isFunction( params ) ) + { + values = inList; + } + + if ( inList !== false ) + { + if ( !values || values.length === 0 ) + { + throw params + ' is not a valid list of values for the ' + ruleName + ' rule'; + } + } + + if ( isPrimitiveArray( values ) ) + { + var map = mapFromArray( values, true ); + + inList = function(value) + { + return map[ value ]; + }; + } + else + { + inList = function(value) + { + return indexOf( values, value, equals ); + }; + } + + var messageTemplate = determineMessage( ruleName, message ); + var list = joinFriendly( values, 'or' ); + var extra = { + $params: params, + $list: list + }; + + return function(value, model, setMessage) + { + if ( isInvalid( value, model, inList ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + + return value; + }; + }; + + + Validation.Rules[ ruleName ].message = defaultMessage; +} + +// between:3,10 +rangeRuleGenerator('between', { + 'string': '{$alias} must have between {$start} to {$end} characters.', + 'number': '{$alias} must be between {$start} and {$end}.', + 'object': '{$alias} must have between {$start} to {$end} items.' + }, + function isInvalid(value, start, end) { + return value < start || value > end; + } +); + +// not_between +rangeRuleGenerator('not_between', { + 'string': '{$alias} must not have between {$start} to {$end} characters.', + 'number': '{$alias} must not be between {$start} and {$end}.', + 'object': '{$alias} must not have between {$start} to {$end} items.' + }, + function isInvalid(value, start, end) { + return value >= start && value <= end; + } +); + +function rangeRuleGenerator(ruleName, defaultMessages, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires a range argument'; + } + + var start, end; + + if ( isString( params ) ) + { + var range = split( params, /(\s*,\s*)/, '\\' ); + + start = parseFloat( range[0] ); + end = parseFloat( range[1] ); + } + else if ( isArray( params ) ) + { + start = params[ 0 ]; + end = params[ 1 ]; + } + else if ( isObject( params ) ) + { + start = params.start; + end = params.end; + } + + if ( isNaN( start ) || isNaN( end ) ) + { + throw params + ' is not a valid range of numbers for the ' + ruleName + ' rule'; + } + + if ( isString( message ) ) + { + message = { + 'string': message, + 'number': message, + 'object': message + }; + } + + var messageTemplate = determineMessage( ruleName, message ); + var extra = { + $start: start, + $end: end + }; + + return function(value, model, setMessage) + { + var size = sizeof( value ); + var type = typeof( value ); + var typeMessage = messageTemplate[ type ]; + + if ( typeMessage && isInvalid( size, start, end ) ) + { + extra.$size = size; + + setMessage( generateMessage( field, getAlias( field ), value, model, typeMessage, extra ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessages; +} + + + +regexRuleGenerator('alpha', + '{$alias} should only contain alphabetic characters.', + /^[a-zA-Z]*$/ +); + +regexRuleGenerator('alpha_dash', + '{$alias} should only contain alpha-numeric characters, dashes, and underscores.', + /^[a-zA-Z0-9_-]*$/ +); + +regexRuleGenerator('alpha_num', + '{$alias} should only contain alpha-numeric characters.', + /^[a-zA-Z0-9]*$/ +); + +regexRuleGenerator('email', + '{$alias} is not a valid email.', + /^.+@.+\..+$/ +); + +regexRuleGenerator('url', + '{$alias} is not a valid URL.', + /^(https?:\/\/)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/ +); + +regexRuleGenerator('uri', + '{$alias} is not a valid URI.', + /^(\w+:\/\/)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/ +); + +regexRuleGenerator('phone', + '{$alias} is not a valid phone number.', + /^1?\W*([2-9][0-8][0-9])\W*([2-9][0-9]{2})\W*([0-9]{4})(\se?x?t?(\d*))?$/ +); + +function regexRuleGenerator(ruleName, defaultMessage, regex) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + checkNoParams( ruleName, field, params ); + + var messageTemplate = determineMessage( ruleName, message ); + + return function(value, model, setMessage) + { + if ( !regex.test( value ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} + +Validation.Rules.regex = function(field, params, database, getAlias, message) +{ + var regex; + + if ( isString( params ) ) + { + var parsed = /^\/(.*)\/([gmi]*)$/.exec( params ); + + if ( parsed ) + { + regex = new RegExp( parsed[1], parsed[2] ); + } + } + else if ( isRegExp( params ) ) + { + regex = params; + } + + if ( !regex ) + { + throw params + ' is not a valid regular expression for the regex rule'; + } + + var messageTemplate = determineMessage( 'regex', message ); + + return function(value, model, setMessage) + { + if ( !regex.test( value ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) ); + } + + return value; + }; +}; + +Validation.Rules.regex.message = '{$alias} is not a valid value.'; + +// required +ruleGenerator('required', + '{$alias} is required.', + function isInvalid(value) { + return isEmpty( value ); + } +); + +// min:3 +sizeRuleGenerator('min', { + 'string': '{$alias} must have a minimum of {$number} characters.', + 'number': '{$alias} must be at least {$number}.', + 'object': '{$alias} must have at least {$number} items.' + }, + function isInvalid(value, number) { + return value < number; + } +); + +// greater_than:0 +sizeRuleGenerator('greater_than', { + 'string': '{$alias} must have more than {$number} characters.', + 'number': '{$alias} must be greater than {$number}.', + 'object': '{$alias} must have more than {$number} items.' + }, + function isInvalid(value, number) { + return value <= number; + } +); + +// max:10 +sizeRuleGenerator('max', { + 'string': '{$alias} must have no more than {$number} characters.', + 'number': '{$alias} must be no more than {$number}.', + 'object': '{$alias} must have no more than {$number} items.' + }, + function isInvalid(value, number) { + return value > number; + } +); + +// less_than:5 +sizeRuleGenerator('less_than', { + 'string': '{$alias} must have less than {$number} characters.', + 'number': '{$alias} must be less than {$number}.', + 'object': '{$alias} must have less than {$number} items.' + }, + function isInvalid(value, number) { + return value >= number; + } +); + +// equal:4.5 +sizeRuleGenerator('equal', { + 'string': '{$alias} must have {$number} characters.', + 'number': '{$alias} must equal {$number}.', + 'object': '{$alias} must have {$number} items.' + }, + function isInvalid(value, number) { + return value !== number; + } +); + +// not_equal:0 +sizeRuleGenerator('not_equal', { + 'string': '{$alias} must not have {$number} characters.', + 'number': '{$alias} must not equal {$number}.', + 'object': '{$alias} must not have {$number} items.' + }, + function isInvalid(value, number) { + return value === number; + } +); + +function sizeRuleGenerator(ruleName, defaultMessages, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + var number; + + if ( isString( params ) ) + { + number = parseFloat( params ); + } + else if ( isNumber( params ) ) + { + number = params; + } + + if ( isNaN( number ) ) + { + throw '"' + params + '" is not a valid number for the ' + ruleName + ' rule'; + } + + if ( isString( message ) ) + { + message = { + 'string': message, + 'number': message, + 'object': message + }; + } + + var messageTemplate = determineMessage( ruleName, message ); + var extra = { + $number: params + }; + + return function(value, model, setMessage) + { + var size = sizeof( value ); + var type = typeof( value ); + var typeMessage = messageTemplate[ type ]; + + if ( typeMessage && isInvalid( size, number ) ) + { + extra.$size = size; + + setMessage( generateMessage( field, getAlias( field ), value, model, typeMessage, extra ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessages; +} + + +ruleGenerator('array', + '{$alias} must be an array.', + function isInvalid(value) { + return !isArray( value ); + } +); + +ruleGenerator('object', + '{$alias} must be an object.', + function isInvalid(value) { + return !isObject( value ); + } +); + +ruleGenerator('string', + '{$alias} must be a string.', + function isInvalid(value) { + return !isString( value ); + } +); + +ruleGenerator('number', + '{$alias} must be a number.', + function isInvalid(value) { + return !isNumber( value ); + } +); + +ruleGenerator('boolean', + '{$alias} must be a true or false.', + function isInvalid(value) { + return !isBoolean( value ); + } +); + +ruleGenerator('model', + '{$alias} must have a value.', + function isInvalid(value) { + return !(value instanceof Model); + } +); + +ruleGenerator('whole', + '{$alias} must be a whole number.', + function isInvalid(value, model, setValue) { + var parsed = tryParseInt( value ); + var numeric = parseFloat( value ); + var invalid = !isNumber( parsed ); + if ( !invalid ) { + invalid = Math.floor( parsed ) !== numeric; + if ( !invalid ) { + setValue( parsed ); + } + } + return invalid; + } +); + +ruleGenerator('numeric', + '{$alias} must be numeric.', + function isInvalid(value, model, setValue) { + var parsed = tryParseFloat( value ); + var invalid = !isNumber( parsed ); + if ( !invalid ) { + setValue( parsed ); + } + return invalid; + } +); + +ruleGenerator('yesno', + '{$alias} must be a yes or no.', + function isInvalid(value, model, setValue) { + var mapped = Validation.Rules.yesno.map[ value ]; + var invalid = !isBoolean( mapped ); + if ( !invalid ) { + setValue( mapped ); + } + return invalid; + } +); + +Validation.Rules.yesno.map = +{ + 'true': true, + 't': true, + 'yes': true, + 'y': true, + '1': true, + 'false': false, + 'f': false, + 'no': false, + 'n': false, + '0': false +}; + +Validation.Rules.abs = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + value = tryParseFloat( value ); + + if ( isNumber( value ) ) + { + value = Math.abs( value ); + } + + return value; + }; +}; + +Validation.Rules.apply = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + model.$set( field, value ); + + return value; + }; +}; + +Validation.Rules.base64 = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + if ( global.btoa ) + { + value = global.btoa( value ); + } + + return value; + }; +}; + +Validation.Rules.ceil = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + value = tryParseFloat( value ); + + if ( isNumber( value ) ) + { + value = Math.ceil( value ); + } + + return value; + }; +}; + +Validation.Rules.endOfDay = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + return endOfDay( value ); + }; +}; + +Validation.Rules.filter = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + if ( isArray( value ) ) + { + for (var i = value.length - 1; i >= 0; i--) + { + if ( !isValue( value[ i ] ) ) + { + value.splice( i, 1 ); + } + } + } + else if ( isObject( value ) ) + { + for (var prop in value) + { + if ( !isValue( value[ prop ] ) ) + { + delete value[ prop ]; + } + } + } + + return value; + }; +}; + +Validation.Rules.floor = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + value = tryParseFloat( value ); + + if ( isNumber( value ) ) + { + value = Math.floor( value ); + } + + return value; + }; +}; + +Validation.Rules.mod = function(field, params, database, alias, message) +{ + var number = tryParseFloat( params ); + + if ( !isNumber( number ) ) + { + throw '"' + number + '" is not a valid number for the mod rule.'; + } + + return function(value, model, setMessage) + { + value = tryParseFloat( value ); + + if ( isNumber( value ) ) + { + value = value % number; + } + + return value; + }; +}; + +Validation.Rules.null = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + model.$set( field, null ); + + return null; + }; +}; + +Validation.Rules.round = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + value = tryParseFloat( value ); + + if ( isNumber( value ) ) + { + value = Math.round( value ); + } + + return value; + }; +}; + +Validation.Rules.startOfDay = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + return startOfDay( value ); + }; +}; + +Validation.Rules.trim = function(field, params, database, alias, message) +{ + // String.trim polyfill + if ( !String.prototype.trim ) + { + var regex = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; + + String.prototype.trim = function() + { + return this.replace( regex, '' ); + }; + } + + return function(value, model, setMessage) + { + if ( isString( value ) ) + { + value = value.trim(); + } + + return value; + }; +}; + +Validation.Rules.unbase64 = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + if ( global.atob ) + { + value = global.atob( value ); + } + + return value; + }; +}; + + + + Rekord.Validation = Validation; + + Rekord.ruleGenerator = ruleGenerator; + Rekord.rangeRuleGenerator = rangeRuleGenerator; + Rekord.collectionRuleGenerator = collectionRuleGenerator; + Rekord.dateRuleGenerator = dateRuleGenerator; + Rekord.fieldListRuleGenerator = fieldListRuleGenerator; + Rekord.fieldsRuleGenerator = fieldsRuleGenerator; + Rekord.foreignRuleGenerator = foreignRuleGenerator; + Rekord.subRuleGenerator = subRuleGenerator; + Rekord.listRuleGenerator = listRuleGenerator; + Rekord.regexRuleGenerator = regexRuleGenerator; + Rekord.sizeRuleGenerator = sizeRuleGenerator; + + Rekord.joinFriendly = joinFriendly; + Rekord.tryParseFloat = tryParseFloat; + Rekord.tryParseInt = tryParseInt; + Rekord.startOfDay = startOfDay; + Rekord.endOfDay = endOfDay; + Rekord.determineMessage = determineMessage; + Rekord.mapFromArray = mapFromArray; + Rekord.checkNoParams = checkNoParams; + Rekord.generateMessage = generateMessage; + +})(this, Rekord); diff --git a/build/rekord-validation.min.js b/build/rekord-validation.min.js new file mode 100644 index 0000000..7c6553e --- /dev/null +++ b/build/rekord-validation.min.js @@ -0,0 +1,2 @@ +!function(e,t,r){function n(e){var t=parseFloat(e);return isNaN(t)||(e=t),e}function a(e){var t=parseInt(e);return isNaN(t)||(e=t),e}function i(e){return V(e)?e.setHours(0,0,0,0):z(e)&&(e-=e%864e5),e}function s(e){return V(e)?e.setHours(23,59,59,999):z(e)&&(e=e-e%864e5+864e5-1),e}function u(e,t,r){B.Rules[e]=function(t,n,a,i,s){c(e,t,n);var u=o(e,s);return function(e,n,a){function s(t){e=t}return r(e,n,s)&&a(m(t,i(t),e,n,u)),e}},B.Rules[e].message=t}function o(e,t){return t||B.Rules[e].message}function l(e,t,r,n){var a=e.slice();if(n)for(var i=0;ie}),h("before","{$alias} must be before {$date}.",function(e,t){return e>t}),h("before_on","{$alias} must be before or equal to {$date}.",function(e,t){return e>s(t)}),u("date_like","{$alias} must be a valid date.",function(e,t,r){var n=W(e),a=n===!1;return a||r(n.getTime()),a}),v("required_if","{$alias} is required.",function(e,t,r,n,a){var i=a[t.$get(r)];return i&&_(e)}),v("required_unless","{$alias} is required.",function(e,t,r,n,a){var i=!a[t.$get(r)];return i&&_(e)}),$("confirmed","{$alias} must match {$fieldAliases}.",function(e,t,r,n){for(var a=!0,i=0;i0}),b("if_any",function(e,t){return e>=t}),b("if_not",function(e,t){return t>e}),p("in","{$alias} must be one of {$list}.",function(e,t,r){return!r(e,t)}),p("not_in","{$alias} must not be one of {$list}.",function(e,t,r){return r(e,t)}),R("between",{string:"{$alias} must have between {$start} to {$end} characters.",number:"{$alias} must be between {$start} and {$end}.",object:"{$alias} must have between {$start} to {$end} items."},function(e,t,r){return t>e||e>r}),R("not_between",{string:"{$alias} must not have between {$start} to {$end} characters.",number:"{$alias} must not be between {$start} and {$end}.",object:"{$alias} must not have between {$start} to {$end} items."},function(e,t,r){return e>=t&&r>=e}),w("alpha","{$alias} should only contain alphabetic characters.",/^[a-zA-Z]*$/),w("alpha_dash","{$alias} should only contain alpha-numeric characters, dashes, and underscores.",/^[a-zA-Z0-9_-]*$/),w("alpha_num","{$alias} should only contain alpha-numeric characters.",/^[a-zA-Z0-9]*$/),w("email","{$alias} is not a valid email.",/^.+@.+\..+$/),w("url","{$alias} is not a valid URL.",/^(https?:\/\/)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)$/),w("uri","{$alias} is not a valid URI.",/^(\w+:\/\/)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)$/),w("phone","{$alias} is not a valid phone number.",/^1?\W*([2-9][0-8][0-9])\W*([2-9][0-9]{2})\W*([0-9]{4})(\se?x?t?(\d*))?$/),B.Rules.regex=function(e,t,r,n,a){var i;if(A(t)){var s=/^\/(.*)\/([gmi]*)$/.exec(t);s&&(i=new RegExp(s[1],s[2]))}else T(t)&&(i=t);if(!i)throw t+" is not a valid regular expression for the regex rule";var u=o("regex",a);return function(t,r,a){return i.test(t)||a(m(e,n(e),t,r,u)),t}},B.Rules.regex.message="{$alias} is not a valid value.",u("required","{$alias} is required.",function(e){return _(e)}),y("min",{string:"{$alias} must have a minimum of {$number} characters.",number:"{$alias} must be at least {$number}.",object:"{$alias} must have at least {$number} items."},function(e,t){return t>e}),y("greater_than",{string:"{$alias} must have more than {$number} characters.",number:"{$alias} must be greater than {$number}.",object:"{$alias} must have more than {$number} items."},function(e,t){return t>=e}),y("max",{string:"{$alias} must have no more than {$number} characters.",number:"{$alias} must be no more than {$number}.",object:"{$alias} must have no more than {$number} items."},function(e,t){return e>t}),y("less_than",{string:"{$alias} must have less than {$number} characters.",number:"{$alias} must be less than {$number}.",object:"{$alias} must have less than {$number} items."},function(e,t){return e>=t}),y("equal",{string:"{$alias} must have {$number} characters.",number:"{$alias} must equal {$number}.",object:"{$alias} must have {$number} items."},function(e,t){return e!==t}),y("not_equal",{string:"{$alias} must not have {$number} characters.",number:"{$alias} must not equal {$number}.",object:"{$alias} must not have {$number} items."},function(e,t){return e===t}),u("array","{$alias} must be an array.",function(e){return!D(e)}),u("object","{$alias} must be an object.",function(e){return!M(e)}),u("string","{$alias} must be a string.",function(e){return!A(e)}),u("number","{$alias} must be a number.",function(e){return!z(e)}),u("boolean","{$alias} must be a true or false.",function(e){return!N(e)}),u("model","{$alias} must have a value.",function(e){return!(e instanceof x)}),u("whole","{$alias} must be a whole number.",function(e,t,r){var n=a(e),i=parseFloat(e),s=!z(n);return s||(s=Math.floor(n)!==i,s||r(n)),s}),u("numeric","{$alias} must be numeric.",function(e,t,r){var a=n(e),i=!z(a);return i||r(a),i}),u("yesno","{$alias} must be a yes or no.",function(e,t,r){var n=B.Rules.yesno.map[e],a=!N(n);return a||r(n),a}),B.Rules.yesno.map={"true":!0,t:!0,yes:!0,y:!0,1:!0,"false":!1,f:!1,no:!1,n:!1,0:!1},B.Rules.abs=function(e,t,r,a,i){return function(e,t,r){return e=n(e),z(e)&&(e=Math.abs(e)),e}},B.Rules.apply=function(e,t,r,n,a){return function(t,r,n){return r.$set(e,t),t}},B.Rules.base64=function(t,r,n,a,i){return function(t,r,n){return e.btoa&&(t=e.btoa(t)),t}},B.Rules.ceil=function(e,t,r,a,i){return function(e,t,r){return e=n(e),z(e)&&(e=Math.ceil(e)),e}},B.Rules.endOfDay=function(e,t,r,n,a){return function(e,t,r){return s(e)}},B.Rules.filter=function(e,t,r,n,a){return function(e,t,r){if(D(e))for(var n=e.length-1;n>=0;n--)k(e[n])||e.splice(n,1);else if(M(e))for(var a in e)k(e[a])||delete e[a];return e}},B.Rules.floor=function(e,t,r,a,i){return function(e,t,r){return e=n(e),z(e)&&(e=Math.floor(e)),e}},B.Rules.mod=function(e,t,r,a,i){var s=n(t);if(!z(s))throw'"'+s+'" is not a valid number for the mod rule.';return function(e,t,r){return e=n(e),z(e)&&(e%=s),e}},B.Rules["null"]=function(e,t,r,n,a){return function(t,r,n){return r.$set(e,null),null}},B.Rules.round=function(e,t,r,a,i){return function(e,t,r){return e=n(e),z(e)&&(e=Math.round(e)),e}},B.Rules.startOfDay=function(e,t,r,n,a){return function(e,t,r){return i(e)}},B.Rules.trim=function(e,t,r,n,a){if(!String.prototype.trim){var i=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;String.prototype.trim=function(){return this.replace(i,"")}}return function(e,t,r){return A(e)&&(e=e.trim()),e}},B.Rules.unbase64=function(t,r,n,a,i){return function(t,r,n){return e.atob&&(t=e.atob(t)),t}},t.Validation=B,t.ruleGenerator=u,t.rangeRuleGenerator=R,t.collectionRuleGenerator=d,t.dateRuleGenerator=h,t.fieldListRuleGenerator=v,t.fieldsRuleGenerator=$,t.foreignRuleGenerator=g,t.subRuleGenerator=b,t.listRuleGenerator=p,t.regexRuleGenerator=w,t.sizeRuleGenerator=y,t.joinFriendly=l,t.tryParseFloat=n,t.tryParseInt=a,t.startOfDay=i,t.endOfDay=s,t.determineMessage=o,t.mapFromArray=f,t.checkNoParams=c,t.generateMessage=m}(this,Rekord); +//# sourceMappingURL=rekord-validation.min.js.map diff --git a/build/rekord-validation.min.js.map b/build/rekord-validation.min.js.map new file mode 100644 index 0000000..bc83aac --- /dev/null +++ b/build/rekord-validation.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["header.js","util.js","rules/collection.js","rules/dates.js","rules/field_list.js","rules/fields.js","rules/foreign.js","rules/if.js","rules/list.js","rules/range.js","rules/regex.js","rules/sizes.js","validation.js","expressions/date.js","expressions/field.js","expressions/relative.js","expressions/today.js","expressions/tomorrow.js","expressions/yesterday.js","rules/accepted.js","rules/required.js","rules/types.js","transforms/abs.js","transforms/apply.js","transforms/base64.js","transforms/ceil.js","transforms/endOfDay.js","transforms/filter.js","transforms/floor.js","transforms/mod.js","transforms/null.js","transforms/round.js","transforms/startOfDay.js","transforms/trim.js","transforms/unbase64.js","footer.js"],"names":["global","Rekord","undefined","tryParseFloat","x","parsed","parseFloat","isNaN","tryParseInt","parseInt","startOfDay","d","isDate","setHours","isNumber","endOfDay","ruleGenerator","ruleName","defaultMessage","isInvalid","Validation","Rules","field","params","database","getAlias","message","checkNoParams","messageTemplate","determineMessage","value","model","setMessage","setValue","newValue","generateMessage","joinFriendly","arr","lastSeparator","itemSeparator","copy","slice","i","length","last","pop","join","mapFromArray","map","alias","extra","isFunction","base","$field","$alias","$value","transfer","isObject","format","collectionRuleGenerator","matchField","matchValue","equality","isString","comma","indexOf","substring","isArray","equals","equalsCompare","fields","otherField","$matchField","$matchAlias","$matchValue","dateRuleGenerator","dateExpression","parseExpression","parseDate","parsedTime","getTime","noop","$date","date","fieldListRuleGenerator","matchValues","parts","split","shift","values","list","$params","$list","fieldsRuleGenerator","fieldNames","fieldAliases","$fields","$fieldAliases","foreignRuleGenerator","modelName","models","fieldName","isValue","name","ModelCollection","get","success","modelClass","all","isRekord","$class","subRuleGenerator","otherRules","colon","rules","validators","parseRules","invalids","setInvalid","Stop","listRuleGenerator","inList","isPrimitiveArray","rangeRuleGenerator","defaultMessages","start","end","range","string","number","object","$start","$end","size","sizeof","type","typeMessage","$size","regexRuleGenerator","regex","test","sizeRuleGenerator","$number","Model","Database","Promise","Collection","isEmpty","isBoolean","isRegExp","addMethod","replaceMethod","on","Events","Plugins","db","options","aliases","validation","Defaults","messages","required","validations","prototype","$this","this","$trigger","PreValidate","$valid","$validations","$validationMessages","chain","$get","fieldValid","push","ValidatePass","ValidateFail","$init","apply","arguments","$save","$isDeleted","debug","Debugs","SAVE_DELETED","$db","resolve","$validate","Expression","Expressions","Delimiter","Escape","RuleSeparator","rule","validator","parseRule","ruleMessageOrData","ruleMessage","ruleInput","input","charAt","customValidator","ruleParams","validatorFactory","expr","parsers","parser","expressionFunction","functionName","result","RELATIVE_REGEX","RELATIVE_UNITS","ms","millisecond","milliseconds","s","second","seconds","min","mins","minute","minutes","hr","hour","hours","day","days","wk","week","weeks","month","months","yr","year","years","relative","exec","amount","unit","unitScale","Date","setTime","getter","setter","today","tomorrow","setDate","getDate","yesterday","accepted","acceptable","valueString","toLowerCase","1","yes","y","true","contains","m","validate","messageOption","invalid","pluck","confirmed","different","valid","invalidCount","totalCount","RegExp","numeric","Math","floor","mapped","yesno","t","false","f","no","n","0","abs","$set","base64","btoa","ceil","filter","splice","prop","mod","round","trim","String","replace","unbase64","atob"],"mappings":"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","file":"rekord-validation.min.js","sourcesContent":["(function(global, Rekord, undefined)\n{\n var Model = Rekord.Model;\n var Database = Rekord.Database;\n var Promise = Rekord.Promise;\n var Collection = Rekord.Collection;\n\n var isEmpty = Rekord.isEmpty;\n var isString = Rekord.isString;\n var isArray = Rekord.isArray;\n var isObject = Rekord.isObject;\n var isFunction = Rekord.isFunction;\n var isDate = Rekord.isDate;\n var isNumber = Rekord.isNumber;\n var isBoolean = Rekord.isBoolean;\n var isValue = Rekord.isValue;\n var isPrimitiveArray = Rekord.isPrimitiveArray;\n var isRegExp = Rekord.isRegExp;\n\n var noop = Rekord.noop;\n var equalsCompare = Rekord.equalsCompare;\n var equals = Rekord.equals;\n var indexOf = Rekord.indexOf;\n var sizeof = Rekord.sizeof;\n\n var split = Rekord.split;\n var transfer = Rekord.transfer;\n var format = Rekord.format;\n\n var parseDate = Rekord.parseDate;\n\n var addMethod = Rekord.addMethod;\n var replaceMethod = Rekord.replaceMethod;\n","function tryParseFloat(x)\n{\n var parsed = parseFloat( x );\n\n if ( !isNaN( parsed ) )\n {\n x = parsed;\n }\n\n return x;\n}\n\nfunction tryParseInt(x)\n{\n var parsed = parseInt( x );\n\n if ( !isNaN( parsed ) )\n {\n x = parsed;\n }\n\n return x;\n}\n\nfunction startOfDay(d)\n{\n if ( isDate( d ) )\n {\n d.setHours( 0, 0, 0, 0 );\n }\n else if ( isNumber( d ) )\n {\n d = d - (d % 86400000);\n }\n\n return d;\n}\n\nfunction endOfDay(d)\n{\n if ( isDate( d ) )\n {\n d.setHours( 23, 59, 59, 999 );\n }\n else if ( isNumber( d ) )\n {\n d = d - (d % 86400000) + 86400000 - 1;\n }\n\n return d;\n}\n\nfunction ruleGenerator(ruleName, defaultMessage, isInvalid)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n checkNoParams( ruleName, field, params );\n\n var messageTemplate = determineMessage( ruleName, message );\n\n return function(value, model, setMessage)\n {\n function setValue( newValue )\n {\n value = newValue;\n }\n\n if ( isInvalid( value, model, setValue ) )\n {\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) );\n }\n\n return value;\n };\n };\n\n Validation.Rules[ ruleName ].message = defaultMessage;\n}\n\nfunction determineMessage(ruleName, message)\n{\n return message || Validation.Rules[ ruleName ].message;\n}\n\nfunction joinFriendly(arr, lastSeparator, itemSeparator, getAlias)\n{\n var copy = arr.slice();\n\n if ( getAlias )\n {\n for (var i = 0; i < copy.length; i++)\n {\n copy[ i ] = getAlias( copy[ i ] );\n }\n }\n\n var last = copy.pop();\n var lastSeparator = lastSeparator || 'and';\n var itemSeparator = itemSeparator || ', ';\n\n switch (copy.length) {\n case 0:\n return last;\n case 1:\n return copy[ 0 ] + ' ' + lastSeparator + ' ' + last;\n default:\n return copy.join( itemSeparator ) + itemSeparator + lastSeparator + ' ' + last;\n }\n}\n\nfunction mapFromArray(arr, value)\n{\n var map = {};\n\n for (var i = 0; i < arr.length; i++)\n {\n map[ arr[ i ] ] = value;\n }\n\n return map;\n}\n\nfunction checkNoParams(ruleName, field, params)\n{\n if ( params )\n {\n throw 'the rule ' + ruleName + ' for field ' + field + ' has no arguments';\n }\n}\n\nfunction generateMessage(field, alias, value, model, message, extra)\n{\n if ( isFunction( message ) )\n {\n message = message( field, alias, value, model, extra );\n }\n\n var base = {};\n base.$field = field;\n base.$alias = alias;\n base.$value = value;\n\n transfer( model, base );\n\n if ( isObject( extra ) )\n {\n transfer( extra, base );\n }\n\n return format( message, base );\n}\n","// contains:field,value\ncollectionRuleGenerator('contains',\n '{$alias} does not contain an item whose {$matchAlias} equals {$matchValue}.',\n function isInvalid(value, model, matchField, matchValue, equality)\n {\n return !value.contains(function isMatch(m)\n {\n return m !== model && equality( matchValue, m.$get( matchField ) );\n });\n }\n);\n\n// not_contains:field,value\ncollectionRuleGenerator('not_contains',\n '{$alias} contains an item whose {$matchAlias} equals {$matchValue}.',\n function isInvalid(value, model, matchField, matchValue, equality)\n {\n return value.contains(function isMatch(m)\n {\n return m !== model && equality( matchValue, m.$get( matchField ) );\n });\n }\n);\n\nfunction collectionRuleGenerator(ruleName, defaultMessage, isInvalid)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n if ( !params )\n {\n throw ruleName + ' validation rule requires field & value arguments';\n }\n\n var matchField, matchValue, equality;\n\n if ( isString( params ) )\n {\n var comma = params.indexOf(',');\n\n if ( comma === -1 )\n {\n throw ruleName + ' validation rule requires field & value arguments';\n }\n\n matchField = params.substring( 0, comma );\n matchValue = params.substring( comma + 1 );\n }\n else if ( isArray( params ) )\n {\n matchField = params[ 0 ];\n matchValue = params[ 1 ];\n equality = params[ 2 ];\n }\n else if ( isObject( params ) )\n {\n matchField = params.field;\n matchValue = params.value;\n equality = params.equals;\n }\n\n if ( !isFunction( equality ) )\n {\n equality = equalsCompare;\n }\n\n if ( indexOf( database.fields, matchField ) === -1 )\n {\n throw otherField + ' is not a valid field for the ' + ruleName + ' rule';\n }\n\n var messageTemplate = determineMessage( ruleName, message );\n var extra = {\n $matchField: matchField,\n $matchAlias: getAlias( matchField ),\n $matchValue: matchValue\n };\n\n return function(value, model, setMessage)\n {\n if ( isInvalid( value, model, matchField, matchValue, equality ) )\n {\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) );\n }\n\n return value;\n };\n };\n\n Validation.Rules[ ruleName ].message = defaultMessage;\n}\n\nValidation.Rules.validate = function(field, params, database, getAlias, message)\n{\n // message, models, validations\n var messageOption = params || 'message';\n var messageTemplate = determineMessage( 'validate', message );\n\n return function(value, model, setMessage)\n {\n if ( isArray( value ) )\n {\n var invalid = new Collection();\n\n for (var i = 0; i < value.length; i++)\n {\n var model = value[ i ];\n\n if ( model && model.$validate && !model.$validate() )\n {\n invalid.push( model );\n }\n }\n\n if ( invalid.length )\n {\n switch (messageOption)\n {\n case 'models':\n setMessage( invalid );\n break;\n case 'validations':\n setMessage( invalid.pluck( '$validations', '$$key' ) );\n break;\n default: // message\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) );\n break;\n }\n }\n }\n\n return value;\n };\n};\n\nValidation.Rules.validate.message = '{$alias} is not valid.';\n","// after:today\ndateRuleGenerator('after',\n '{$alias} must be after {$date}.',\n function isInvalid(value, date) {\n return value < endOfDay( date );\n }\n);\n\n// after_on:tomorrow\ndateRuleGenerator('after_on',\n '{$alias} must be after or equal to {$date}.',\n function isInvalid(value, date) {\n return value < date;\n }\n);\n\n// before:yesterday\ndateRuleGenerator('before',\n '{$alias} must be before {$date}.',\n function isInvalid(value, date) {\n return value > date;\n }\n);\n\n// before_on:+2days\ndateRuleGenerator('before_on',\n '{$alias} must be before or equal to {$date}.',\n function isInvalid(value, date) {\n return value > endOfDay( date );\n }\n);\n\n// date\nruleGenerator('date_like',\n '{$alias} must be a valid date.',\n function isInvalid(value, model, setValue) {\n var parsed = parseDate( value );\n var invalid = parsed === false;\n if ( !invalid ) {\n setValue( parsed.getTime() );\n }\n return invalid;\n }\n);\n\nfunction dateRuleGenerator(ruleName, defaultMessage, isInvalid)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n if ( !params )\n {\n throw ruleName + ' validation rule requires a date expression argument';\n }\n\n var dateExpression;\n\n if ( isString( params ) )\n {\n dateExpression = Validation.parseExpression( params, database );\n }\n else if ( isFunction( params ) )\n {\n dateExpression = params;\n }\n else\n {\n var parsed = parseDate( params );\n\n if ( parsed !== false )\n {\n var parsedTime = parsed.getTime();\n\n dateExpression = function()\n {\n return parsedTime;\n };\n }\n }\n\n if ( !dateExpression || dateExpression === noop )\n {\n throw params + ' is not a valid date expression for the ' + ruleName + ' rule';\n }\n\n var messageTemplate = determineMessage( ruleName, message );\n var extra = {\n $date: params\n };\n\n return function(value, model, setMessage)\n {\n var parsed = parseDate( value );\n\n if ( parsed !== false )\n {\n value = parsed.getTime();\n\n var date = dateExpression( value, model );\n\n if ( isNumber( date ) && isInvalid( value, date ) )\n {\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) );\n }\n }\n\n return value;\n };\n };\n\n Validation.Rules[ ruleName ].message = defaultMessage;\n}\n","\n// required_if:X,Y,...\nfieldListRuleGenerator('required_if',\n '{$alias} is required.',\n function isInvalid(value, model, field, values, map) {\n var required = map[ model.$get( field ) ];\n\n return required && isEmpty( value );\n }\n);\n\n// required_unless:X,Y,...\nfieldListRuleGenerator('required_unless',\n '{$alias} is required.',\n function isInvalid(value, model, field, values, map) {\n var required = !map[ model.$get( field ) ];\n\n return required && isEmpty( value );\n }\n);\n\nfunction fieldListRuleGenerator(ruleName, defaultMessage, isInvalid)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n if ( !params )\n {\n throw ruleName + ' validation rule requires a field and list arguments';\n }\n\n var matchField, matchValues;\n\n if ( isString( params ) )\n {\n var parts = split( params, /(,)/, '\\\\' );\n\n matchField = parts.shift();\n matchValues = parts;\n }\n else if ( isArray( params ) )\n {\n matchField = params.shift();\n matchValues = params;\n }\n else if ( isObject( params ) )\n {\n matchField = params.field;\n matchValues = params.values;\n }\n\n if ( indexOf( database.fields, matchField ) === false )\n {\n throw matchField + ' is not a valid field for the ' + ruleName + ' rule';\n }\n\n var messageTemplate = determineMessage( ruleName, message );\n var list = joinFriendly( matchValues );\n var extra = {\n $params: params,\n $matchField: matchField,\n $matchAlias: getAlias( matchField ),\n $list: list\n };\n var map = mapFromArray( matchValues, true );\n\n return function(value, model, setMessage)\n {\n if ( isInvalid( value, model, matchField, matchValues, map ) )\n {\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) );\n }\n\n return value;\n };\n };\n\n Validation.Rules[ ruleName ].message = defaultMessage;\n}\n","// confirmed:X\nfieldsRuleGenerator('confirmed',\n '{$alias} must match {$fieldAliases}.',\n function isInvalid(value, model, fields, setValue) {\n var confirmed = true;\n\n for (var i = 0; i < fields.length; i++)\n {\n if ( !equals( value, model.$get( fields[ i ] ) ) )\n {\n confirmed = false;\n }\n }\n\n return !confirmed;\n }\n);\n\n// different:X\nfieldsRuleGenerator('different',\n '{$alias} must not match {$fieldAliases}.',\n function isInvalid(value, model, fields, setValue) {\n var different = false;\n\n for (var i = 0; i < fields.length; i++)\n {\n if ( !equals( value, model.$get( fields[ i ] ) ) )\n {\n different = true;\n }\n }\n\n return !different;\n }\n);\n\n// if_valid:X\nfieldsRuleGenerator('if_valid',\n '',\n function isInvalid(value, model, fields, setValue) {\n var valid = true;\n\n for (var i = 0; i < fields.length && valid; i++)\n {\n if ( model.$validations[ fields[ i ] ] )\n {\n valid = false;\n }\n }\n\n if ( !valid )\n {\n setValue( Validation.Stop );\n }\n\n return false;\n }\n);\n\n// The field under validation must be present only if any of the other specified fields are present.\n// required_with:X,Y,...\nfieldsRuleGenerator('required_with',\n '{$alias} is required.',\n function isInvalid(value, model, fields, setValue) {\n var required = false;\n\n for (var i = 0; i < fields.length && !required; i++)\n {\n if ( !isEmpty( model.$get( fields[ i ] ) ) )\n {\n required = true;\n }\n }\n\n return required && isEmpty( value );\n }\n);\n\n// The field under validation must be present only if all of the other specified fields are present.\n// required_with_all:X,Y,...\nfieldsRuleGenerator('required_with_all',\n '{$alias} is required.',\n function isInvalid(value, model, fields, setValue) {\n var required = true;\n\n for (var i = 0; i < fields.length && required; i++)\n {\n if ( isEmpty( model.$get( fields[ i ] ) ) )\n {\n required = false;\n }\n }\n\n return required && isEmpty( value );\n }\n);\n\n// The field under validation must be present only when any of the other specified fields are not present.\n// required_without:X,Y,...\nfieldsRuleGenerator('required_without',\n '{$alias} is required.',\n function isInvalid(value, model, fields, setValue) {\n var required = false;\n\n for (var i = 0; i < fields.length && !required; i++)\n {\n if ( isEmpty( model.$get( fields[ i ] ) ) )\n {\n required = true;\n }\n }\n\n return required && isEmpty( value );\n }\n);\n\n// The field under validation must be present only when all of the other specified fields are not present.\n// required_without_all:X,Y,...\nfieldsRuleGenerator('required_without_all',\n '{$alias} is required.',\n function isInvalid(value, model, fields, setValue) {\n var required = true;\n\n for (var i = 0; i < fields.length && required; i++)\n {\n if ( !isEmpty( model.$get( fields[ i ] ) ) )\n {\n required = false;\n }\n }\n\n return required && isEmpty( value );\n }\n);\n\nfunction fieldsRuleGenerator(ruleName, defaultMessage, isInvalid)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n if ( !params )\n {\n throw ruleName + ' validation rule requires an array of fields argument';\n }\n\n var fields = split( params, /(\\s*,\\s*)/, '\\\\' );\n\n for (var i = 0; i < fields.length; i++)\n {\n if ( indexOf( database.fields, fields[ i ] ) === -1 )\n {\n throw fields[ i ] + ' is not a valid field for the ' + ruleName + ' rule';\n }\n }\n\n var messageTemplate = determineMessage( ruleName, message );\n var fieldNames = joinFriendly( fields );\n var fieldAliases = joinFriendly( fields, false, false, getAlias );\n var extra = {\n $fields: fieldNames,\n $fieldAliases: fieldAliases\n };\n\n return function(value, model, setMessage)\n {\n function setValue( newValue )\n {\n value = newValue;\n }\n\n if ( isInvalid( value, model, fields, setValue ) )\n {\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) );\n }\n\n return value;\n };\n };\n\n Validation.Rules[ ruleName ].message = defaultMessage;\n};\n","// exists:X,Y\nforeignRuleGenerator('exists',\n '{$alias} must match an existing {$matchAlias} in a {$class}',\n function isInvalid(value, model, models, fieldName)\n {\n return !models.contains(function isDifferentMatch(m)\n {\n return m !== model && equals( value, m.$get( fieldName ) );\n });\n }\n);\n\n// unique:X,Y\nforeignRuleGenerator('unique',\n '{$alias} must be a unique {$matchAlias} in a {$class}',\n function isInvalid(value, model, models, fieldName)\n {\n return models.contains(function isDifferentMatch(m)\n {\n return m !== model && equals( value, m.$get( fieldName ) );\n });\n }\n);\n\n// 'ruleName'\n// 'ruleName:name'\n// 'ruleName:,field'\n// 'ruleName:name,field'\n// 'ruleName:name,field': '...'\n// 'ruleName': {input: {field: 'field', model: 'name'}, message: '...'}\n// 'ruleName': {input: {field: 'field', model: ModelClass}, message: '...'}\n// 'ruleName': {input: {field: 'field', models: [...]}, message: '...'}\n// 'ruleName': {field: 'field', model: 'name'}\n// 'ruleName': {field: 'field', model: ModelClass}\n// 'ruleName': {field: 'field', models: [...]}\nfunction foreignRuleGenerator(ruleName, defaultMessage, isInvalid)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n var modelName, models, fieldName;\n\n if ( !isValue( params ) || isString( params ) )\n {\n var parts = split( params || '', /(\\s*,\\s*)/, '\\\\' );\n modelName = parts[0] || database.name;\n fieldName = parts[1] || field;\n models = null;\n }\n else if ( isArray( params ) )\n {\n modelName = isString( params[0] ) ? params.shift() : database.name;\n fieldName = isString( params[0] ) ? params.shift() : field;\n models = new ModelCollection( database, params );\n }\n else if ( isObject( params ) )\n {\n modelName = params.model || database.name;\n fieldName = params.field || field;\n models = params.models;\n }\n\n if ( !models )\n {\n if ( !modelName )\n {\n throw 'model, model class, or models is required for ' + ruleName + ' rule';\n }\n\n if ( isString( modelName ) )\n {\n Rekord.get( modelName ).success(function(modelClass)\n {\n models = modelClass.all();\n });\n }\n else if ( isRekord( modelName ) )\n {\n models = modelName.all();\n }\n }\n\n if ( indexOf( database.fields, fieldName ) === false )\n {\n throw fieldName + ' is not a valid field for the ' + ruleName + ' rule';\n }\n\n var messageTemplate = determineMessage( ruleName, message );\n var extra = {\n $class: modelName,\n $matchField: fieldName,\n $matchAlias: getAlias( fieldName )\n };\n\n return function(value, model, setMessage)\n {\n if ( models && isValue( value ) )\n {\n if ( isInvalid( value, model, models, fieldName ) )\n {\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) );\n }\n }\n\n return value;\n };\n };\n\n Validation.Rules[ ruleName ].message = defaultMessage;\n}\n","// if:due_date:before:today|required\n\n// if all rules pass for the given field, continue with remaining rules\nsubRuleGenerator('if',\n function isInvalid(invalidCount, totalCount) {\n return invalidCount > 0;\n }\n);\n\n// if any rules pass for the given field, continue with remaining rules\nsubRuleGenerator('if_any',\n function isInvalid(invalidCount, totalCount) {\n return invalidCount >= totalCount;\n }\n);\n\n// if no rules pass for the given field, continue with remaining rules\nsubRuleGenerator('if_not',\n function isInvalid(invalidCount, totalCount) {\n return invalidCount < totalCount;\n }\n);\n\n\n\nfunction subRuleGenerator(ruleName, isInvalid)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n if ( !params )\n {\n throw ruleName + ' validation rule requires a validation rule argument';\n }\n\n var otherField, otherRules;\n\n if ( isString( params ) )\n {\n var colon = params.indexOf( ':' );\n\n if ( colon === -1 )\n {\n throw params + ' is not a valid argument for the ' + ruleName + ' rule';\n }\n\n otherField = params.substring( 0, colon ) || field;\n otherRules = params.substring( colon + 1 );\n }\n else if ( isArray( params ) )\n {\n otherField = params.shift() || field;\n otherRules = params;\n }\n else if ( isObject( params ) )\n {\n otherField = params.field || field;\n otherRules = params.rules;\n }\n\n if ( indexOf( database.fields, otherField ) === -1 )\n {\n throw otherField + ' is not a valid field for the ' + ruleName + ' rule';\n }\n\n if ( !otherRules )\n {\n throw 'rules are required for the ' + ruleName + ' rule';\n }\n\n var validators = Validation.parseRules( otherRules, otherField, database, getAlias );\n\n return function(value, model, setMessage)\n {\n var invalids = 0;\n\n var setInvalid = function(message)\n {\n if ( message )\n {\n invalids++;\n }\n };\n\n for (var i = 0; i < validators.length; i++)\n {\n validators[ i ]( value, model, setInvalid );\n }\n\n return isInvalid( invalids, validators.length ) ? Validation.Stop : value;\n };\n };\n}\n","// in:X,Y,Z,...\nlistRuleGenerator('in',\n '{$alias} must be one of {$list}.',\n function isInvalid(value, model, inList)\n {\n return !inList( value, model );\n }\n);\n\n// not_in:X,Y,Z,...\nlistRuleGenerator('not_in',\n '{$alias} must not be one of {$list}.',\n function isInvalid(value, model, inList)\n {\n return inList( value, model )\n }\n);\n\nfunction listRuleGenerator(ruleName, defaultMessage, isInvalid)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n if ( !params )\n {\n throw ruleName + ' validation rule requires a list argument';\n }\n\n var values, inList = false;\n\n if ( isString( params ) )\n {\n values = split( params, /(,)/, '\\\\' );\n }\n else if ( isArray( params ) )\n {\n values = params;\n }\n else if ( isFunction( params ) )\n {\n values = inList;\n }\n\n if ( inList !== false )\n {\n if ( !values || values.length === 0 )\n {\n throw params + ' is not a valid list of values for the ' + ruleName + ' rule';\n }\n }\n\n if ( isPrimitiveArray( values ) )\n {\n var map = mapFromArray( values, true );\n\n inList = function(value)\n {\n return map[ value ];\n };\n }\n else\n {\n inList = function(value)\n {\n return indexOf( values, value, equals );\n };\n }\n\n var messageTemplate = determineMessage( ruleName, message );\n var list = joinFriendly( values, 'or' );\n var extra = {\n $params: params,\n $list: list\n };\n\n return function(value, model, setMessage)\n {\n if ( isInvalid( value, model, inList ) )\n {\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) );\n }\n\n return value;\n };\n };\n\n\n Validation.Rules[ ruleName ].message = defaultMessage;\n}\n","// between:3,10\nrangeRuleGenerator('between', {\n 'string': '{$alias} must have between {$start} to {$end} characters.',\n 'number': '{$alias} must be between {$start} and {$end}.',\n 'object': '{$alias} must have between {$start} to {$end} items.'\n },\n function isInvalid(value, start, end) {\n return value < start || value > end;\n }\n);\n\n// not_between\nrangeRuleGenerator('not_between', {\n 'string': '{$alias} must not have between {$start} to {$end} characters.',\n 'number': '{$alias} must not be between {$start} and {$end}.',\n 'object': '{$alias} must not have between {$start} to {$end} items.'\n },\n function isInvalid(value, start, end) {\n return value >= start && value <= end;\n }\n);\n\nfunction rangeRuleGenerator(ruleName, defaultMessages, isInvalid)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n if ( !params )\n {\n throw ruleName + ' validation rule requires a range argument';\n }\n\n var start, end;\n\n if ( isString( params ) )\n {\n var range = split( params, /(\\s*,\\s*)/, '\\\\' );\n\n start = parseFloat( range[0] );\n end = parseFloat( range[1] );\n }\n else if ( isArray( params ) )\n {\n start = params[ 0 ];\n end = params[ 1 ];\n }\n else if ( isObject( params ) )\n {\n start = params.start;\n end = params.end;\n }\n\n if ( isNaN( start ) || isNaN( end ) )\n {\n throw params + ' is not a valid range of numbers for the ' + ruleName + ' rule';\n }\n\n if ( isString( message ) )\n {\n message = {\n 'string': message,\n 'number': message,\n 'object': message\n };\n }\n\n var messageTemplate = determineMessage( ruleName, message );\n var extra = {\n $start: start,\n $end: end\n };\n\n return function(value, model, setMessage)\n {\n var size = sizeof( value );\n var type = typeof( value );\n var typeMessage = messageTemplate[ type ];\n\n if ( typeMessage && isInvalid( size, start, end ) )\n {\n extra.$size = size;\n\n setMessage( generateMessage( field, getAlias( field ), value, model, typeMessage, extra ) );\n }\n\n return value;\n };\n };\n\n Validation.Rules[ ruleName ].message = defaultMessages;\n}\n","\n\nregexRuleGenerator('alpha',\n '{$alias} should only contain alphabetic characters.',\n /^[a-zA-Z]*$/\n);\n\nregexRuleGenerator('alpha_dash',\n '{$alias} should only contain alpha-numeric characters, dashes, and underscores.',\n /^[a-zA-Z0-9_-]*$/\n);\n\nregexRuleGenerator('alpha_num',\n '{$alias} should only contain alpha-numeric characters.',\n /^[a-zA-Z0-9]*$/\n);\n\nregexRuleGenerator('email',\n '{$alias} is not a valid email.',\n /^.+@.+\\..+$/\n);\n\nregexRuleGenerator('url',\n '{$alias} is not a valid URL.',\n /^(https?:\\/\\/)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)$/\n);\n\nregexRuleGenerator('uri',\n '{$alias} is not a valid URI.',\n /^(\\w+:\\/\\/)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)$/\n);\n\nregexRuleGenerator('phone',\n '{$alias} is not a valid phone number.',\n /^1?\\W*([2-9][0-8][0-9])\\W*([2-9][0-9]{2})\\W*([0-9]{4})(\\se?x?t?(\\d*))?$/\n);\n\nfunction regexRuleGenerator(ruleName, defaultMessage, regex)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n checkNoParams( ruleName, field, params );\n\n var messageTemplate = determineMessage( ruleName, message );\n\n return function(value, model, setMessage)\n {\n if ( !regex.test( value ) )\n {\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) );\n }\n\n return value;\n };\n };\n\n Validation.Rules[ ruleName ].message = defaultMessage;\n}\n\nValidation.Rules.regex = function(field, params, database, getAlias, message)\n{\n var regex;\n\n if ( isString( params ) )\n {\n var parsed = /^\\/(.*)\\/([gmi]*)$/.exec( params );\n\n if ( parsed )\n {\n regex = new RegExp( parsed[1], parsed[2] );\n }\n }\n else if ( isRegExp( params ) )\n {\n regex = params;\n }\n\n if ( !regex )\n {\n throw params + ' is not a valid regular expression for the regex rule';\n }\n\n var messageTemplate = determineMessage( 'regex', message );\n\n return function(value, model, setMessage)\n {\n if ( !regex.test( value ) )\n {\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) );\n }\n\n return value;\n };\n};\n\nValidation.Rules.regex.message = '{$alias} is not a valid value.';\n","// min:3\nsizeRuleGenerator('min', {\n 'string': '{$alias} must have a minimum of {$number} characters.',\n 'number': '{$alias} must be at least {$number}.',\n 'object': '{$alias} must have at least {$number} items.'\n },\n function isInvalid(value, number) {\n return value < number;\n }\n);\n\n// greater_than:0\nsizeRuleGenerator('greater_than', {\n 'string': '{$alias} must have more than {$number} characters.',\n 'number': '{$alias} must be greater than {$number}.',\n 'object': '{$alias} must have more than {$number} items.'\n },\n function isInvalid(value, number) {\n return value <= number;\n }\n);\n\n// max:10\nsizeRuleGenerator('max', {\n 'string': '{$alias} must have no more than {$number} characters.',\n 'number': '{$alias} must be no more than {$number}.',\n 'object': '{$alias} must have no more than {$number} items.'\n },\n function isInvalid(value, number) {\n return value > number;\n }\n);\n\n// less_than:5\nsizeRuleGenerator('less_than', {\n 'string': '{$alias} must have less than {$number} characters.',\n 'number': '{$alias} must be less than {$number}.',\n 'object': '{$alias} must have less than {$number} items.'\n },\n function isInvalid(value, number) {\n return value >= number;\n }\n);\n\n// equal:4.5\nsizeRuleGenerator('equal', {\n 'string': '{$alias} must have {$number} characters.',\n 'number': '{$alias} must equal {$number}.',\n 'object': '{$alias} must have {$number} items.'\n },\n function isInvalid(value, number) {\n return value !== number;\n }\n);\n\n// not_equal:0\nsizeRuleGenerator('not_equal', {\n 'string': '{$alias} must not have {$number} characters.',\n 'number': '{$alias} must not equal {$number}.',\n 'object': '{$alias} must not have {$number} items.'\n },\n function isInvalid(value, number) {\n return value === number;\n }\n);\n\nfunction sizeRuleGenerator(ruleName, defaultMessages, isInvalid)\n{\n Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message)\n {\n var number;\n\n if ( isString( params ) )\n {\n number = parseFloat( params );\n }\n else if ( isNumber( params ) )\n {\n number = params;\n }\n\n if ( isNaN( number ) )\n {\n throw '\"' + params + '\" is not a valid number for the ' + ruleName + ' rule';\n }\n\n if ( isString( message ) )\n {\n message = {\n 'string': message,\n 'number': message,\n 'object': message\n };\n }\n\n var messageTemplate = determineMessage( ruleName, message );\n var extra = {\n $number: params\n };\n\n return function(value, model, setMessage)\n {\n var size = sizeof( value );\n var type = typeof( value );\n var typeMessage = messageTemplate[ type ];\n\n if ( typeMessage && isInvalid( size, number ) )\n {\n extra.$size = size;\n\n setMessage( generateMessage( field, getAlias( field ), value, model, typeMessage, extra ) );\n }\n\n return value;\n };\n };\n\n Validation.Rules[ ruleName ].message = defaultMessages;\n}\n","Rekord.on( Rekord.Events.Plugins, function(model, db, options)\n{\n var validation = options.validation || Database.Defaults.validation;\n\n if ( isEmpty( validation ) )\n {\n return;\n }\n\n var rules = validation.rules || {};\n var messages = validation.messages || {};\n var aliases = validation.aliases || {};\n var required = !!validation.required;\n\n function getAlias(field)\n {\n return aliases[ field ] || field;\n }\n\n db.validations = {};\n\n for ( var field in rules )\n {\n db.validations[ field ] = Validation.parseRules( rules[ field ], field, db, getAlias, messages[ field ] )\n }\n\n addMethod( model.prototype, '$validate', function()\n {\n var $this = this;\n\n this.$trigger( Model.Events.PreValidate, [this] );\n\n this.$valid = true;\n this.$validations = {};\n this.$validationMessages.length = 0;\n\n for (var field in db.validations)\n {\n var chain = db.validations[ field ];\n var value = this.$get( field );\n var fieldValid = true;\n\n var setMessage = function(message)\n {\n // Only accept for the first valid message\n if ( message && fieldValid )\n {\n fieldValid = false;\n\n $this.$validations[ field ] = message;\n $this.$validationMessages.push( message );\n $this.$valid = false;\n }\n };\n\n for (var i = 0; i < chain.length && fieldValid && value !== Validation.Stop; i++)\n {\n value = chain[ i ]( value, this, setMessage );\n }\n }\n\n this.$trigger( this.$valid ? Model.Events.ValidatePass : Model.Events.ValidateFail, [this] );\n\n return this.$valid;\n });\n\n replaceMethod( model.prototype, '$init', function($init)\n {\n return function()\n {\n this.$valid = undefined;\n this.$validations = {};\n this.$validationMessages = [];\n\n return $init.apply( this, arguments );\n };\n });\n\n if ( required )\n {\n replaceMethod( model.prototype, '$save', function($save)\n {\n return function()\n {\n if ( this.$isDeleted() )\n {\n Rekord.debug( Rekord.Debugs.SAVE_DELETED, this.$db, this );\n\n return Promise.resolve( this );\n }\n\n if ( !this.$validate() )\n {\n return Promise.resolve( this );\n }\n\n return $save.apply( this, arguments );\n };\n });\n }\n});\n\nModel.Events.PreValidate = 'pre-validate';\n\nModel.Events.ValidatePass = 'validate-pass';\n\nModel.Events.ValidateFail = 'validate-fail';\n\nvar Validation =\n{\n Rules: {},\n Expression: {},\n Expressions: [],\n Delimiter: /([|])/,\n Escape: '\\\\',\n RuleSeparator: ':',\n Stop: {},\n\n parseRules: function(rules, field, database, getAlias, message)\n {\n var validators = [];\n\n if ( isString( rules ) )\n {\n rules = split( rules, this.Delimiter, this.Escape );\n }\n\n if ( isArray( rules ) )\n {\n for (var i = 0; i < rules.length; i++)\n {\n var rule = rules[ i ];\n var validator = this.parseRule( rule, field, database, getAlias, message );\n\n validators.push( validator );\n }\n }\n else if ( isObject( rules ) )\n {\n for (var rule in rules)\n {\n var ruleMessageOrData = rules[ rule ];\n\n var ruleMessage = isObject( ruleMessageOrData ) ? ruleMessageOrData.message :\n ( isString( ruleMessageOrData ) ? ruleMessageOrData : undefined );\n\n var ruleInput = isObject( ruleMessageOrData ) && ruleMessageOrData.message ? ruleMessageOrData.input :\n ( isString( ruleMessageOrData ) ? undefined : ruleMessageOrData );\n\n var validator = this.parseRule( rule, field, database, getAlias, ruleMessage || message, ruleInput );\n\n validators.push( validator );\n }\n }\n\n return validators;\n },\n\n parseRule: function(rule, field, database, getAlias, message, input)\n {\n var colon = rule.indexOf( this.RuleSeparator );\n var ruleName = colon === -1 ? rule : rule.substring( 0, colon );\n\n if ( ruleName.charAt( 0 ) === '$' )\n {\n return this.customValidator( ruleName, field, database, getAlias, message );\n }\n\n var ruleParams = colon === -1 ? input : rule.substring( colon + 1 );\n var validatorFactory = Validation.Rules[ ruleName ];\n\n if ( !validatorFactory )\n {\n throw ruleName + ' is not a valid rule';\n }\n\n return validatorFactory( field, ruleParams, database, getAlias, message );\n },\n\n parseExpression: function(expr, database)\n {\n var parsers = Validation.Expressions;\n\n for (var i = 0; i < parsers.length; i++)\n {\n var parser = parsers[ i ];\n var expressionFunction = parser( expr, database );\n\n if ( isFunction( expressionFunction ) )\n {\n return expressionFunction; // (value, model)\n }\n }\n\n return noop;\n },\n\n customValidator: function(functionName, field, database, getAlias, message)\n {\n return function(value, model, setMessage)\n {\n var result = model[ functionName ]( value, getAlias, message );\n\n if ( isString( result ) )\n {\n setMessage( result );\n }\n\n return value;\n };\n }\n};\n","Validation.Expression.date =\nValidation.Expressions.push(function(expr, database)\n{\n var parsed = parseDate( expr );\n\n if ( parsed !== false )\n {\n var parsedTime = parsed.getTime();\n\n return function(value, model)\n {\n return parsedTime;\n };\n }\n}) - 1;\n","Validation.Expression.field =\nValidation.Expressions.push(function(expr, database)\n{\n if ( indexOf( database.fields, expr ) )\n {\n return function(value, model)\n {\n return model.$get( expr );\n };\n }\n}) - 1;\n","\nvar RELATIVE_REGEX = /^([+-]\\d+(\\.\\d+)?)\\s*(.+)$/;\n\nvar RELATIVE_UNITS = {\n ms: 1,\n millisecond: 1,\n milliseconds: 1,\n s: 1000,\n second: 1000,\n seconds: 1000,\n min: 1000 * 60,\n mins: 1000 * 60,\n minute: 1000 * 60,\n minutes: 1000 * 60,\n hr: 1000 * 60 * 60,\n hour: 1000 * 60 * 60,\n hours: 1000 * 60 * 60,\n day: 1000 * 60 * 60 * 24,\n days: 1000 * 60 * 60 * 24,\n wk: 1000 * 60 * 60 * 24 * 7,\n week: 1000 * 60 * 60 * 24 * 7,\n weeks: 1000 * 60 * 60 * 24 * 7,\n month: ['getMonth', 'setMonth'],\n months: ['getMonth', 'setMonth'],\n yr: ['getFullYear', 'setFullYear'],\n year: ['getFullYear', 'setFullYear'],\n years: ['getFullYear', 'setFullYear']\n};\n\nValidation.Expression.relative =\nValidation.Expressions.push(function(expr, database)\n{\n var parsed = RELATIVE_REGEX.exec( expr );\n\n if ( parsed !== null )\n {\n var amount = parseFloat( parsed[ 1 ] );\n var unit = parsed[ 3 ];\n var unitScale = RELATIVE_UNITS[ unit ];\n\n if ( !unitScale )\n {\n throw unit + ' is not a valid unit.';\n }\n\n return function(value, model)\n {\n var relative = new Date();\n\n if ( isNumber( unitScale ) )\n {\n relative.setTime( relative.getTime() + unitScale * amount );\n }\n else\n {\n var getter = unitScale[0];\n var setter = unitScale[1];\n\n relative[ setter ]( relative[ getter ]() + amount );\n }\n\n return relative.getTime();\n };\n }\n}) - 1;\n","Validation.Expression.today =\nValidation.Expressions.push(function(expr, database)\n{\n if ( expr === 'today' )\n {\n return function(value, model)\n {\n var today = new Date();\n\n startOfDay( today );\n\n return today.getTime();\n };\n }\n}) - 1;\n","Validation.Expression.tomorrow =\nValidation.Expressions.push(function(expr, database)\n{\n if ( expr === 'tomorrow' )\n {\n return function(value, model)\n {\n var tomorrow = new Date();\n\n tomorrow.setDate( tomorrow.getDate() + 1 );\n startOfDay( tomorrow );\n\n return tomorrow.getTime();\n };\n }\n}) - 1;\n","Validation.Expression.yesterday =\nValidation.Expressions.push(function(expr, database)\n{\n if ( expr === 'yesterday' )\n {\n return function(value, model)\n {\n var yesterday = new Date();\n\n yesterday.setDate( yesterday.getDate() - 1 );\n startOfDay( yesterday );\n\n return yesterday.getTime();\n };\n }\n}) - 1;\n","// accepted\nValidation.Rules.accepted = function(field, params, database, getAlias, message)\n{\n checkNoParams( 'accepted', field, params );\n\n var messageTemplate = determineMessage( 'accepted', message );\n var acceptable = Validation.Rules.accepted.acceptable;\n\n return function(value, model, setMessage)\n {\n var valueString = (value + '').toLowerCase();\n var accepted = acceptable[ valueString ];\n\n if ( !accepted )\n {\n setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) );\n }\n\n return value;\n };\n};\n\nValidation.Rules.accepted.message = '{$alias} has not been accepted.';\n\nValidation.Rules.accepted.acceptable =\n{\n '1': true,\n 'yes': true,\n 'on': true,\n 'y': true,\n 'true': true\n};\n","// required\nruleGenerator('required',\n '{$alias} is required.',\n function isInvalid(value) {\n return isEmpty( value );\n }\n);\n","\nruleGenerator('array',\n '{$alias} must be an array.',\n function isInvalid(value) {\n return !isArray( value );\n }\n);\n\nruleGenerator('object',\n '{$alias} must be an object.',\n function isInvalid(value) {\n return !isObject( value );\n }\n);\n\nruleGenerator('string',\n '{$alias} must be a string.',\n function isInvalid(value) {\n return !isString( value );\n }\n);\n\nruleGenerator('number',\n '{$alias} must be a number.',\n function isInvalid(value) {\n return !isNumber( value );\n }\n);\n\nruleGenerator('boolean',\n '{$alias} must be a true or false.',\n function isInvalid(value) {\n return !isBoolean( value );\n }\n);\n\nruleGenerator('model',\n '{$alias} must have a value.',\n function isInvalid(value) {\n return !(value instanceof Model);\n }\n);\n\nruleGenerator('whole',\n '{$alias} must be a whole number.',\n function isInvalid(value, model, setValue) {\n var parsed = tryParseInt( value );\n var numeric = parseFloat( value );\n var invalid = !isNumber( parsed );\n if ( !invalid ) {\n invalid = Math.floor( parsed ) !== numeric;\n if ( !invalid ) {\n setValue( parsed );\n }\n }\n return invalid;\n }\n);\n\nruleGenerator('numeric',\n '{$alias} must be numeric.',\n function isInvalid(value, model, setValue) {\n var parsed = tryParseFloat( value );\n var invalid = !isNumber( parsed );\n if ( !invalid ) {\n setValue( parsed );\n }\n return invalid;\n }\n);\n\nruleGenerator('yesno',\n '{$alias} must be a yes or no.',\n function isInvalid(value, model, setValue) {\n var mapped = Validation.Rules.yesno.map[ value ];\n var invalid = !isBoolean( mapped );\n if ( !invalid ) {\n setValue( mapped );\n }\n return invalid;\n }\n);\n\nValidation.Rules.yesno.map =\n{\n 'true': true,\n 't': true,\n 'yes': true,\n 'y': true,\n '1': true,\n 'false': false,\n 'f': false,\n 'no': false,\n 'n': false,\n '0': false\n};\n","Validation.Rules.abs = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n value = tryParseFloat( value );\n\n if ( isNumber( value ) )\n {\n value = Math.abs( value );\n }\n\n return value;\n };\n};\n","Validation.Rules.apply = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n model.$set( field, value );\n \n return value;\n };\n};\n","Validation.Rules.base64 = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n if ( global.btoa )\n {\n value = global.btoa( value );\n }\n\n return value;\n };\n};\n","Validation.Rules.ceil = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n value = tryParseFloat( value );\n \n if ( isNumber( value ) )\n {\n value = Math.ceil( value );\n }\n\n return value;\n };\n};\n","Validation.Rules.endOfDay = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n return endOfDay( value );\n };\n};\n","Validation.Rules.filter = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n if ( isArray( value ) )\n {\n for (var i = value.length - 1; i >= 0; i--)\n {\n if ( !isValue( value[ i ] ) )\n {\n value.splice( i, 1 );\n }\n }\n }\n else if ( isObject( value ) )\n {\n for (var prop in value)\n {\n if ( !isValue( value[ prop ] ) )\n {\n delete value[ prop ];\n }\n }\n }\n\n return value;\n };\n};\n","Validation.Rules.floor = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n value = tryParseFloat( value );\n \n if ( isNumber( value ) )\n {\n value = Math.floor( value );\n }\n\n return value;\n };\n};\n","Validation.Rules.mod = function(field, params, database, alias, message)\n{\n var number = tryParseFloat( params );\n\n if ( !isNumber( number ) )\n {\n throw '\"' + number + '\" is not a valid number for the mod rule.';\n }\n\n return function(value, model, setMessage)\n {\n value = tryParseFloat( value );\n\n if ( isNumber( value ) )\n {\n value = value % number;\n }\n\n return value;\n };\n};\n","Validation.Rules.null = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n model.$set( field, null );\n\n return null;\n };\n};\n","Validation.Rules.round = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n value = tryParseFloat( value );\n\n if ( isNumber( value ) )\n {\n value = Math.round( value );\n }\n\n return value;\n };\n};\n","Validation.Rules.startOfDay = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n return startOfDay( value );\n };\n};\n","Validation.Rules.trim = function(field, params, database, alias, message)\n{\n // String.trim polyfill\n if ( !String.prototype.trim )\n {\n var regex = /^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g;\n\n String.prototype.trim = function()\n {\n return this.replace( regex, '' );\n };\n }\n\n return function(value, model, setMessage)\n {\n if ( isString( value ) )\n {\n value = value.trim();\n }\n\n return value;\n };\n};\n","Validation.Rules.unbase64 = function(field, params, database, alias, message)\n{\n return function(value, model, setMessage)\n {\n if ( global.atob )\n {\n value = global.atob( value );\n }\n\n return value;\n };\n};\n","\n\n Rekord.Validation = Validation;\n\n Rekord.ruleGenerator = ruleGenerator;\n Rekord.rangeRuleGenerator = rangeRuleGenerator;\n Rekord.collectionRuleGenerator = collectionRuleGenerator;\n Rekord.dateRuleGenerator = dateRuleGenerator;\n Rekord.fieldListRuleGenerator = fieldListRuleGenerator;\n Rekord.fieldsRuleGenerator = fieldsRuleGenerator;\n Rekord.foreignRuleGenerator = foreignRuleGenerator;\n Rekord.subRuleGenerator = subRuleGenerator;\n Rekord.listRuleGenerator = listRuleGenerator;\n Rekord.regexRuleGenerator = regexRuleGenerator;\n Rekord.sizeRuleGenerator = sizeRuleGenerator;\n\n Rekord.joinFriendly = joinFriendly;\n Rekord.tryParseFloat = tryParseFloat;\n Rekord.tryParseInt = tryParseInt;\n Rekord.startOfDay = startOfDay;\n Rekord.endOfDay = endOfDay;\n Rekord.determineMessage = determineMessage;\n Rekord.mapFromArray = mapFromArray;\n Rekord.checkNoParams = checkNoParams;\n Rekord.generateMessage = generateMessage;\n\n})(this, Rekord);\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..4c3a06c --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,29 @@ +{ + "source": { + "include": [ + "src/lib/" + ] + }, + "templates": { + "applicationName": "Rekord Validation", + "linenums": true, + "cleverLinks": true, + "default": { + "outputSourceFiles": true + } + }, + "opts": { + "destination": "../rekord-validation-docs/docs/", + "recurse": "true", + "template": "./node_modules/jaguarjs-jsdoc", + "readme": "./README.md", + "pedantic": true, + "debug": true + }, + "plugins": [ + "plugins/markdown" + ], + "tags": { + "allowUnknownTags": true + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8c807d3 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "rekord-validation", + "version": "1.0.0", + "description": "Advanced validation rules for rekord", + "author": "Philip Diffenderfer", + "license": "MIT", + "repository": { + "type": "git", + "url": "git://github.com/Rekord/rekord-validation.git" + }, + "scripts": { + "test": "gulp test" + }, + "devDependencies": { + "gulp": "^3.9.0", + "gulp-check-filesize": "^2.0.1", + "gulp-concat": "^2.6.0", + "gulp-load-plugins": "^1.1.0", + "gulp-qunit": "^1.3.0", + "gulp-shell": "^0.5.1", + "gulp-sourcemaps": "^1.6.0", + "gulp-uglify": "^1.5.1", + "gulp-util": "^3.0.7", + "gulp-watch": "^4.3.5", + "jaguarjs-jsdoc": "0.0.1", + "jsdoc": "^3.4.0", + "merge-stream": "^1.0.0" + } +} diff --git a/src/footer.js b/src/footer.js new file mode 100644 index 0000000..db96818 --- /dev/null +++ b/src/footer.js @@ -0,0 +1,27 @@ + + + Rekord.Validation = Validation; + + Rekord.ruleGenerator = ruleGenerator; + Rekord.rangeRuleGenerator = rangeRuleGenerator; + Rekord.collectionRuleGenerator = collectionRuleGenerator; + Rekord.dateRuleGenerator = dateRuleGenerator; + Rekord.fieldListRuleGenerator = fieldListRuleGenerator; + Rekord.fieldsRuleGenerator = fieldsRuleGenerator; + Rekord.foreignRuleGenerator = foreignRuleGenerator; + Rekord.subRuleGenerator = subRuleGenerator; + Rekord.listRuleGenerator = listRuleGenerator; + Rekord.regexRuleGenerator = regexRuleGenerator; + Rekord.sizeRuleGenerator = sizeRuleGenerator; + + Rekord.joinFriendly = joinFriendly; + Rekord.tryParseFloat = tryParseFloat; + Rekord.tryParseInt = tryParseInt; + Rekord.startOfDay = startOfDay; + Rekord.endOfDay = endOfDay; + Rekord.determineMessage = determineMessage; + Rekord.mapFromArray = mapFromArray; + Rekord.checkNoParams = checkNoParams; + Rekord.generateMessage = generateMessage; + +})(this, Rekord); diff --git a/src/header.js b/src/header.js new file mode 100644 index 0000000..fd80764 --- /dev/null +++ b/src/header.js @@ -0,0 +1,33 @@ +(function(global, Rekord, undefined) +{ + var Model = Rekord.Model; + var Database = Rekord.Database; + var Promise = Rekord.Promise; + var Collection = Rekord.Collection; + + var isEmpty = Rekord.isEmpty; + var isString = Rekord.isString; + var isArray = Rekord.isArray; + var isObject = Rekord.isObject; + var isFunction = Rekord.isFunction; + var isDate = Rekord.isDate; + var isNumber = Rekord.isNumber; + var isBoolean = Rekord.isBoolean; + var isValue = Rekord.isValue; + var isPrimitiveArray = Rekord.isPrimitiveArray; + var isRegExp = Rekord.isRegExp; + + var noop = Rekord.noop; + var equalsCompare = Rekord.equalsCompare; + var equals = Rekord.equals; + var indexOf = Rekord.indexOf; + var sizeof = Rekord.sizeof; + + var split = Rekord.split; + var transfer = Rekord.transfer; + var format = Rekord.format; + + var parseDate = Rekord.parseDate; + + var addMethod = Rekord.addMethod; + var replaceMethod = Rekord.replaceMethod; diff --git a/src/lib/expressions/date.js b/src/lib/expressions/date.js new file mode 100644 index 0000000..941a25a --- /dev/null +++ b/src/lib/expressions/date.js @@ -0,0 +1,15 @@ +Validation.Expression.date = +Validation.Expressions.push(function(expr, database) +{ + var parsed = parseDate( expr ); + + if ( parsed !== false ) + { + var parsedTime = parsed.getTime(); + + return function(value, model) + { + return parsedTime; + }; + } +}) - 1; diff --git a/src/lib/expressions/field.js b/src/lib/expressions/field.js new file mode 100644 index 0000000..538a29d --- /dev/null +++ b/src/lib/expressions/field.js @@ -0,0 +1,11 @@ +Validation.Expression.field = +Validation.Expressions.push(function(expr, database) +{ + if ( indexOf( database.fields, expr ) ) + { + return function(value, model) + { + return model.$get( expr ); + }; + } +}) - 1; diff --git a/src/lib/expressions/relative.js b/src/lib/expressions/relative.js new file mode 100644 index 0000000..697e28f --- /dev/null +++ b/src/lib/expressions/relative.js @@ -0,0 +1,65 @@ + +var RELATIVE_REGEX = /^([+-]\d+(\.\d+)?)\s*(.+)$/; + +var RELATIVE_UNITS = { + ms: 1, + millisecond: 1, + milliseconds: 1, + s: 1000, + second: 1000, + seconds: 1000, + min: 1000 * 60, + mins: 1000 * 60, + minute: 1000 * 60, + minutes: 1000 * 60, + hr: 1000 * 60 * 60, + hour: 1000 * 60 * 60, + hours: 1000 * 60 * 60, + day: 1000 * 60 * 60 * 24, + days: 1000 * 60 * 60 * 24, + wk: 1000 * 60 * 60 * 24 * 7, + week: 1000 * 60 * 60 * 24 * 7, + weeks: 1000 * 60 * 60 * 24 * 7, + month: ['getMonth', 'setMonth'], + months: ['getMonth', 'setMonth'], + yr: ['getFullYear', 'setFullYear'], + year: ['getFullYear', 'setFullYear'], + years: ['getFullYear', 'setFullYear'] +}; + +Validation.Expression.relative = +Validation.Expressions.push(function(expr, database) +{ + var parsed = RELATIVE_REGEX.exec( expr ); + + if ( parsed !== null ) + { + var amount = parseFloat( parsed[ 1 ] ); + var unit = parsed[ 3 ]; + var unitScale = RELATIVE_UNITS[ unit ]; + + if ( !unitScale ) + { + throw unit + ' is not a valid unit.'; + } + + return function(value, model) + { + var relative = new Date(); + + if ( isNumber( unitScale ) ) + { + relative.setTime( relative.getTime() + unitScale * amount ); + } + else + { + var getter = unitScale[0]; + var setter = unitScale[1]; + + relative[ setter ]( relative[ getter ]() + amount ); + } + + return relative.getTime(); + }; + } +}) - 1; diff --git a/src/lib/expressions/today.js b/src/lib/expressions/today.js new file mode 100644 index 0000000..6c9c145 --- /dev/null +++ b/src/lib/expressions/today.js @@ -0,0 +1,15 @@ +Validation.Expression.today = +Validation.Expressions.push(function(expr, database) +{ + if ( expr === 'today' ) + { + return function(value, model) + { + var today = new Date(); + + startOfDay( today ); + + return today.getTime(); + }; + } +}) - 1; diff --git a/src/lib/expressions/tomorrow.js b/src/lib/expressions/tomorrow.js new file mode 100644 index 0000000..d965b17 --- /dev/null +++ b/src/lib/expressions/tomorrow.js @@ -0,0 +1,16 @@ +Validation.Expression.tomorrow = +Validation.Expressions.push(function(expr, database) +{ + if ( expr === 'tomorrow' ) + { + return function(value, model) + { + var tomorrow = new Date(); + + tomorrow.setDate( tomorrow.getDate() + 1 ); + startOfDay( tomorrow ); + + return tomorrow.getTime(); + }; + } +}) - 1; diff --git a/src/lib/expressions/yesterday.js b/src/lib/expressions/yesterday.js new file mode 100644 index 0000000..512edb3 --- /dev/null +++ b/src/lib/expressions/yesterday.js @@ -0,0 +1,16 @@ +Validation.Expression.yesterday = +Validation.Expressions.push(function(expr, database) +{ + if ( expr === 'yesterday' ) + { + return function(value, model) + { + var yesterday = new Date(); + + yesterday.setDate( yesterday.getDate() - 1 ); + startOfDay( yesterday ); + + return yesterday.getTime(); + }; + } +}) - 1; diff --git a/src/lib/rules/accepted.js b/src/lib/rules/accepted.js new file mode 100644 index 0000000..6464c98 --- /dev/null +++ b/src/lib/rules/accepted.js @@ -0,0 +1,32 @@ +// accepted +Validation.Rules.accepted = function(field, params, database, getAlias, message) +{ + checkNoParams( 'accepted', field, params ); + + var messageTemplate = determineMessage( 'accepted', message ); + var acceptable = Validation.Rules.accepted.acceptable; + + return function(value, model, setMessage) + { + var valueString = (value + '').toLowerCase(); + var accepted = acceptable[ valueString ]; + + if ( !accepted ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) ); + } + + return value; + }; +}; + +Validation.Rules.accepted.message = '{$alias} has not been accepted.'; + +Validation.Rules.accepted.acceptable = +{ + '1': true, + 'yes': true, + 'on': true, + 'y': true, + 'true': true +}; diff --git a/src/lib/rules/collection.js b/src/lib/rules/collection.js new file mode 100644 index 0000000..857ccdf --- /dev/null +++ b/src/lib/rules/collection.js @@ -0,0 +1,135 @@ +// contains:field,value +collectionRuleGenerator('contains', + '{$alias} does not contain an item whose {$matchAlias} equals {$matchValue}.', + function isInvalid(value, model, matchField, matchValue, equality) + { + return !value.contains(function isMatch(m) + { + return m !== model && equality( matchValue, m.$get( matchField ) ); + }); + } +); + +// not_contains:field,value +collectionRuleGenerator('not_contains', + '{$alias} contains an item whose {$matchAlias} equals {$matchValue}.', + function isInvalid(value, model, matchField, matchValue, equality) + { + return value.contains(function isMatch(m) + { + return m !== model && equality( matchValue, m.$get( matchField ) ); + }); + } +); + +function collectionRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires field & value arguments'; + } + + var matchField, matchValue, equality; + + if ( isString( params ) ) + { + var comma = params.indexOf(','); + + if ( comma === -1 ) + { + throw ruleName + ' validation rule requires field & value arguments'; + } + + matchField = params.substring( 0, comma ); + matchValue = params.substring( comma + 1 ); + } + else if ( isArray( params ) ) + { + matchField = params[ 0 ]; + matchValue = params[ 1 ]; + equality = params[ 2 ]; + } + else if ( isObject( params ) ) + { + matchField = params.field; + matchValue = params.value; + equality = params.equals; + } + + if ( !isFunction( equality ) ) + { + equality = equalsCompare; + } + + if ( indexOf( database.fields, matchField ) === -1 ) + { + throw otherField + ' is not a valid field for the ' + ruleName + ' rule'; + } + + var messageTemplate = determineMessage( ruleName, message ); + var extra = { + $matchField: matchField, + $matchAlias: getAlias( matchField ), + $matchValue: matchValue + }; + + return function(value, model, setMessage) + { + if ( isInvalid( value, model, matchField, matchValue, equality ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} + +Validation.Rules.validate = function(field, params, database, getAlias, message) +{ + // message, models, validations + var messageOption = params || 'message'; + var messageTemplate = determineMessage( 'validate', message ); + + return function(value, model, setMessage) + { + if ( isArray( value ) ) + { + var invalid = new Collection(); + + for (var i = 0; i < value.length; i++) + { + var model = value[ i ]; + + if ( model && model.$validate && !model.$validate() ) + { + invalid.push( model ); + } + } + + if ( invalid.length ) + { + switch (messageOption) + { + case 'models': + setMessage( invalid ); + break; + case 'validations': + setMessage( invalid.pluck( '$validations', '$$key' ) ); + break; + default: // message + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) ); + break; + } + } + } + + return value; + }; +}; + +Validation.Rules.validate.message = '{$alias} is not valid.'; diff --git a/src/lib/rules/dates.js b/src/lib/rules/dates.js new file mode 100644 index 0000000..7980d1c --- /dev/null +++ b/src/lib/rules/dates.js @@ -0,0 +1,111 @@ +// after:today +dateRuleGenerator('after', + '{$alias} must be after {$date}.', + function isInvalid(value, date) { + return value < endOfDay( date ); + } +); + +// after_on:tomorrow +dateRuleGenerator('after_on', + '{$alias} must be after or equal to {$date}.', + function isInvalid(value, date) { + return value < date; + } +); + +// before:yesterday +dateRuleGenerator('before', + '{$alias} must be before {$date}.', + function isInvalid(value, date) { + return value > date; + } +); + +// before_on:+2days +dateRuleGenerator('before_on', + '{$alias} must be before or equal to {$date}.', + function isInvalid(value, date) { + return value > endOfDay( date ); + } +); + +// date +ruleGenerator('date_like', + '{$alias} must be a valid date.', + function isInvalid(value, model, setValue) { + var parsed = parseDate( value ); + var invalid = parsed === false; + if ( !invalid ) { + setValue( parsed.getTime() ); + } + return invalid; + } +); + +function dateRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires a date expression argument'; + } + + var dateExpression; + + if ( isString( params ) ) + { + dateExpression = Validation.parseExpression( params, database ); + } + else if ( isFunction( params ) ) + { + dateExpression = params; + } + else + { + var parsed = parseDate( params ); + + if ( parsed !== false ) + { + var parsedTime = parsed.getTime(); + + dateExpression = function() + { + return parsedTime; + }; + } + } + + if ( !dateExpression || dateExpression === noop ) + { + throw params + ' is not a valid date expression for the ' + ruleName + ' rule'; + } + + var messageTemplate = determineMessage( ruleName, message ); + var extra = { + $date: params + }; + + return function(value, model, setMessage) + { + var parsed = parseDate( value ); + + if ( parsed !== false ) + { + value = parsed.getTime(); + + var date = dateExpression( value, model ); + + if ( isNumber( date ) && isInvalid( value, date ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} diff --git a/src/lib/rules/field_list.js b/src/lib/rules/field_list.js new file mode 100644 index 0000000..d35e390 --- /dev/null +++ b/src/lib/rules/field_list.js @@ -0,0 +1,78 @@ + +// required_if:X,Y,... +fieldListRuleGenerator('required_if', + '{$alias} is required.', + function isInvalid(value, model, field, values, map) { + var required = map[ model.$get( field ) ]; + + return required && isEmpty( value ); + } +); + +// required_unless:X,Y,... +fieldListRuleGenerator('required_unless', + '{$alias} is required.', + function isInvalid(value, model, field, values, map) { + var required = !map[ model.$get( field ) ]; + + return required && isEmpty( value ); + } +); + +function fieldListRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires a field and list arguments'; + } + + var matchField, matchValues; + + if ( isString( params ) ) + { + var parts = split( params, /(,)/, '\\' ); + + matchField = parts.shift(); + matchValues = parts; + } + else if ( isArray( params ) ) + { + matchField = params.shift(); + matchValues = params; + } + else if ( isObject( params ) ) + { + matchField = params.field; + matchValues = params.values; + } + + if ( indexOf( database.fields, matchField ) === false ) + { + throw matchField + ' is not a valid field for the ' + ruleName + ' rule'; + } + + var messageTemplate = determineMessage( ruleName, message ); + var list = joinFriendly( matchValues ); + var extra = { + $params: params, + $matchField: matchField, + $matchAlias: getAlias( matchField ), + $list: list + }; + var map = mapFromArray( matchValues, true ); + + return function(value, model, setMessage) + { + if ( isInvalid( value, model, matchField, matchValues, map ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} diff --git a/src/lib/rules/fields.js b/src/lib/rules/fields.js new file mode 100644 index 0000000..54f7463 --- /dev/null +++ b/src/lib/rules/fields.js @@ -0,0 +1,180 @@ +// confirmed:X +fieldsRuleGenerator('confirmed', + '{$alias} must match {$fieldAliases}.', + function isInvalid(value, model, fields, setValue) { + var confirmed = true; + + for (var i = 0; i < fields.length; i++) + { + if ( !equals( value, model.$get( fields[ i ] ) ) ) + { + confirmed = false; + } + } + + return !confirmed; + } +); + +// different:X +fieldsRuleGenerator('different', + '{$alias} must not match {$fieldAliases}.', + function isInvalid(value, model, fields, setValue) { + var different = false; + + for (var i = 0; i < fields.length; i++) + { + if ( !equals( value, model.$get( fields[ i ] ) ) ) + { + different = true; + } + } + + return !different; + } +); + +// if_valid:X +fieldsRuleGenerator('if_valid', + '', + function isInvalid(value, model, fields, setValue) { + var valid = true; + + for (var i = 0; i < fields.length && valid; i++) + { + if ( model.$validations[ fields[ i ] ] ) + { + valid = false; + } + } + + if ( !valid ) + { + setValue( Validation.Stop ); + } + + return false; + } +); + +// The field under validation must be present only if any of the other specified fields are present. +// required_with:X,Y,... +fieldsRuleGenerator('required_with', + '{$alias} is required.', + function isInvalid(value, model, fields, setValue) { + var required = false; + + for (var i = 0; i < fields.length && !required; i++) + { + if ( !isEmpty( model.$get( fields[ i ] ) ) ) + { + required = true; + } + } + + return required && isEmpty( value ); + } +); + +// The field under validation must be present only if all of the other specified fields are present. +// required_with_all:X,Y,... +fieldsRuleGenerator('required_with_all', + '{$alias} is required.', + function isInvalid(value, model, fields, setValue) { + var required = true; + + for (var i = 0; i < fields.length && required; i++) + { + if ( isEmpty( model.$get( fields[ i ] ) ) ) + { + required = false; + } + } + + return required && isEmpty( value ); + } +); + +// The field under validation must be present only when any of the other specified fields are not present. +// required_without:X,Y,... +fieldsRuleGenerator('required_without', + '{$alias} is required.', + function isInvalid(value, model, fields, setValue) { + var required = false; + + for (var i = 0; i < fields.length && !required; i++) + { + if ( isEmpty( model.$get( fields[ i ] ) ) ) + { + required = true; + } + } + + return required && isEmpty( value ); + } +); + +// The field under validation must be present only when all of the other specified fields are not present. +// required_without_all:X,Y,... +fieldsRuleGenerator('required_without_all', + '{$alias} is required.', + function isInvalid(value, model, fields, setValue) { + var required = true; + + for (var i = 0; i < fields.length && required; i++) + { + if ( !isEmpty( model.$get( fields[ i ] ) ) ) + { + required = false; + } + } + + return required && isEmpty( value ); + } +); + +function fieldsRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires an array of fields argument'; + } + + var fields = split( params, /(\s*,\s*)/, '\\' ); + + for (var i = 0; i < fields.length; i++) + { + if ( indexOf( database.fields, fields[ i ] ) === -1 ) + { + throw fields[ i ] + ' is not a valid field for the ' + ruleName + ' rule'; + } + } + + var messageTemplate = determineMessage( ruleName, message ); + var fieldNames = joinFriendly( fields ); + var fieldAliases = joinFriendly( fields, false, false, getAlias ); + var extra = { + $fields: fieldNames, + $fieldAliases: fieldAliases + }; + + return function(value, model, setMessage) + { + function setValue( newValue ) + { + value = newValue; + } + + if ( isInvalid( value, model, fields, setValue ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +}; diff --git a/src/lib/rules/foreign.js b/src/lib/rules/foreign.js new file mode 100644 index 0000000..fa4c4cb --- /dev/null +++ b/src/lib/rules/foreign.js @@ -0,0 +1,109 @@ +// exists:X,Y +foreignRuleGenerator('exists', + '{$alias} must match an existing {$matchAlias} in a {$class}', + function isInvalid(value, model, models, fieldName) + { + return !models.contains(function isDifferentMatch(m) + { + return m !== model && equals( value, m.$get( fieldName ) ); + }); + } +); + +// unique:X,Y +foreignRuleGenerator('unique', + '{$alias} must be a unique {$matchAlias} in a {$class}', + function isInvalid(value, model, models, fieldName) + { + return models.contains(function isDifferentMatch(m) + { + return m !== model && equals( value, m.$get( fieldName ) ); + }); + } +); + +// 'ruleName' +// 'ruleName:name' +// 'ruleName:,field' +// 'ruleName:name,field' +// 'ruleName:name,field': '...' +// 'ruleName': {input: {field: 'field', model: 'name'}, message: '...'} +// 'ruleName': {input: {field: 'field', model: ModelClass}, message: '...'} +// 'ruleName': {input: {field: 'field', models: [...]}, message: '...'} +// 'ruleName': {field: 'field', model: 'name'} +// 'ruleName': {field: 'field', model: ModelClass} +// 'ruleName': {field: 'field', models: [...]} +function foreignRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + var modelName, models, fieldName; + + if ( !isValue( params ) || isString( params ) ) + { + var parts = split( params || '', /(\s*,\s*)/, '\\' ); + modelName = parts[0] || database.name; + fieldName = parts[1] || field; + models = null; + } + else if ( isArray( params ) ) + { + modelName = isString( params[0] ) ? params.shift() : database.name; + fieldName = isString( params[0] ) ? params.shift() : field; + models = new ModelCollection( database, params ); + } + else if ( isObject( params ) ) + { + modelName = params.model || database.name; + fieldName = params.field || field; + models = params.models; + } + + if ( !models ) + { + if ( !modelName ) + { + throw 'model, model class, or models is required for ' + ruleName + ' rule'; + } + + if ( isString( modelName ) ) + { + Rekord.get( modelName ).success(function(modelClass) + { + models = modelClass.all(); + }); + } + else if ( isRekord( modelName ) ) + { + models = modelName.all(); + } + } + + if ( indexOf( database.fields, fieldName ) === false ) + { + throw fieldName + ' is not a valid field for the ' + ruleName + ' rule'; + } + + var messageTemplate = determineMessage( ruleName, message ); + var extra = { + $class: modelName, + $matchField: fieldName, + $matchAlias: getAlias( fieldName ) + }; + + return function(value, model, setMessage) + { + if ( models && isValue( value ) ) + { + if ( isInvalid( value, model, models, fieldName ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} diff --git a/src/lib/rules/if.js b/src/lib/rules/if.js new file mode 100644 index 0000000..0c02172 --- /dev/null +++ b/src/lib/rules/if.js @@ -0,0 +1,92 @@ +// if:due_date:before:today|required + +// if all rules pass for the given field, continue with remaining rules +subRuleGenerator('if', + function isInvalid(invalidCount, totalCount) { + return invalidCount > 0; + } +); + +// if any rules pass for the given field, continue with remaining rules +subRuleGenerator('if_any', + function isInvalid(invalidCount, totalCount) { + return invalidCount >= totalCount; + } +); + +// if no rules pass for the given field, continue with remaining rules +subRuleGenerator('if_not', + function isInvalid(invalidCount, totalCount) { + return invalidCount < totalCount; + } +); + + + +function subRuleGenerator(ruleName, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires a validation rule argument'; + } + + var otherField, otherRules; + + if ( isString( params ) ) + { + var colon = params.indexOf( ':' ); + + if ( colon === -1 ) + { + throw params + ' is not a valid argument for the ' + ruleName + ' rule'; + } + + otherField = params.substring( 0, colon ) || field; + otherRules = params.substring( colon + 1 ); + } + else if ( isArray( params ) ) + { + otherField = params.shift() || field; + otherRules = params; + } + else if ( isObject( params ) ) + { + otherField = params.field || field; + otherRules = params.rules; + } + + if ( indexOf( database.fields, otherField ) === -1 ) + { + throw otherField + ' is not a valid field for the ' + ruleName + ' rule'; + } + + if ( !otherRules ) + { + throw 'rules are required for the ' + ruleName + ' rule'; + } + + var validators = Validation.parseRules( otherRules, otherField, database, getAlias ); + + return function(value, model, setMessage) + { + var invalids = 0; + + var setInvalid = function(message) + { + if ( message ) + { + invalids++; + } + }; + + for (var i = 0; i < validators.length; i++) + { + validators[ i ]( value, model, setInvalid ); + } + + return isInvalid( invalids, validators.length ) ? Validation.Stop : value; + }; + }; +} diff --git a/src/lib/rules/list.js b/src/lib/rules/list.js new file mode 100644 index 0000000..5b8d796 --- /dev/null +++ b/src/lib/rules/list.js @@ -0,0 +1,88 @@ +// in:X,Y,Z,... +listRuleGenerator('in', + '{$alias} must be one of {$list}.', + function isInvalid(value, model, inList) + { + return !inList( value, model ); + } +); + +// not_in:X,Y,Z,... +listRuleGenerator('not_in', + '{$alias} must not be one of {$list}.', + function isInvalid(value, model, inList) + { + return inList( value, model ) + } +); + +function listRuleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires a list argument'; + } + + var values, inList = false; + + if ( isString( params ) ) + { + values = split( params, /(,)/, '\\' ); + } + else if ( isArray( params ) ) + { + values = params; + } + else if ( isFunction( params ) ) + { + values = inList; + } + + if ( inList !== false ) + { + if ( !values || values.length === 0 ) + { + throw params + ' is not a valid list of values for the ' + ruleName + ' rule'; + } + } + + if ( isPrimitiveArray( values ) ) + { + var map = mapFromArray( values, true ); + + inList = function(value) + { + return map[ value ]; + }; + } + else + { + inList = function(value) + { + return indexOf( values, value, equals ); + }; + } + + var messageTemplate = determineMessage( ruleName, message ); + var list = joinFriendly( values, 'or' ); + var extra = { + $params: params, + $list: list + }; + + return function(value, model, setMessage) + { + if ( isInvalid( value, model, inList ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate, extra ) ); + } + + return value; + }; + }; + + + Validation.Rules[ ruleName ].message = defaultMessage; +} diff --git a/src/lib/rules/range.js b/src/lib/rules/range.js new file mode 100644 index 0000000..8903186 --- /dev/null +++ b/src/lib/rules/range.js @@ -0,0 +1,90 @@ +// between:3,10 +rangeRuleGenerator('between', { + 'string': '{$alias} must have between {$start} to {$end} characters.', + 'number': '{$alias} must be between {$start} and {$end}.', + 'object': '{$alias} must have between {$start} to {$end} items.' + }, + function isInvalid(value, start, end) { + return value < start || value > end; + } +); + +// not_between +rangeRuleGenerator('not_between', { + 'string': '{$alias} must not have between {$start} to {$end} characters.', + 'number': '{$alias} must not be between {$start} and {$end}.', + 'object': '{$alias} must not have between {$start} to {$end} items.' + }, + function isInvalid(value, start, end) { + return value >= start && value <= end; + } +); + +function rangeRuleGenerator(ruleName, defaultMessages, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + if ( !params ) + { + throw ruleName + ' validation rule requires a range argument'; + } + + var start, end; + + if ( isString( params ) ) + { + var range = split( params, /(\s*,\s*)/, '\\' ); + + start = parseFloat( range[0] ); + end = parseFloat( range[1] ); + } + else if ( isArray( params ) ) + { + start = params[ 0 ]; + end = params[ 1 ]; + } + else if ( isObject( params ) ) + { + start = params.start; + end = params.end; + } + + if ( isNaN( start ) || isNaN( end ) ) + { + throw params + ' is not a valid range of numbers for the ' + ruleName + ' rule'; + } + + if ( isString( message ) ) + { + message = { + 'string': message, + 'number': message, + 'object': message + }; + } + + var messageTemplate = determineMessage( ruleName, message ); + var extra = { + $start: start, + $end: end + }; + + return function(value, model, setMessage) + { + var size = sizeof( value ); + var type = typeof( value ); + var typeMessage = messageTemplate[ type ]; + + if ( typeMessage && isInvalid( size, start, end ) ) + { + extra.$size = size; + + setMessage( generateMessage( field, getAlias( field ), value, model, typeMessage, extra ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessages; +} diff --git a/src/lib/rules/regex.js b/src/lib/rules/regex.js new file mode 100644 index 0000000..f81620e --- /dev/null +++ b/src/lib/rules/regex.js @@ -0,0 +1,96 @@ + + +regexRuleGenerator('alpha', + '{$alias} should only contain alphabetic characters.', + /^[a-zA-Z]*$/ +); + +regexRuleGenerator('alpha_dash', + '{$alias} should only contain alpha-numeric characters, dashes, and underscores.', + /^[a-zA-Z0-9_-]*$/ +); + +regexRuleGenerator('alpha_num', + '{$alias} should only contain alpha-numeric characters.', + /^[a-zA-Z0-9]*$/ +); + +regexRuleGenerator('email', + '{$alias} is not a valid email.', + /^.+@.+\..+$/ +); + +regexRuleGenerator('url', + '{$alias} is not a valid URL.', + /^(https?:\/\/)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/ +); + +regexRuleGenerator('uri', + '{$alias} is not a valid URI.', + /^(\w+:\/\/)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/ +); + +regexRuleGenerator('phone', + '{$alias} is not a valid phone number.', + /^1?\W*([2-9][0-8][0-9])\W*([2-9][0-9]{2})\W*([0-9]{4})(\se?x?t?(\d*))?$/ +); + +function regexRuleGenerator(ruleName, defaultMessage, regex) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + checkNoParams( ruleName, field, params ); + + var messageTemplate = determineMessage( ruleName, message ); + + return function(value, model, setMessage) + { + if ( !regex.test( value ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} + +Validation.Rules.regex = function(field, params, database, getAlias, message) +{ + var regex; + + if ( isString( params ) ) + { + var parsed = /^\/(.*)\/([gmi]*)$/.exec( params ); + + if ( parsed ) + { + regex = new RegExp( parsed[1], parsed[2] ); + } + } + else if ( isRegExp( params ) ) + { + regex = params; + } + + if ( !regex ) + { + throw params + ' is not a valid regular expression for the regex rule'; + } + + var messageTemplate = determineMessage( 'regex', message ); + + return function(value, model, setMessage) + { + if ( !regex.test( value ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) ); + } + + return value; + }; +}; + +Validation.Rules.regex.message = '{$alias} is not a valid value.'; diff --git a/src/lib/rules/required.js b/src/lib/rules/required.js new file mode 100644 index 0000000..ea66981 --- /dev/null +++ b/src/lib/rules/required.js @@ -0,0 +1,7 @@ +// required +ruleGenerator('required', + '{$alias} is required.', + function isInvalid(value) { + return isEmpty( value ); + } +); diff --git a/src/lib/rules/sizes.js b/src/lib/rules/sizes.js new file mode 100644 index 0000000..ffc7171 --- /dev/null +++ b/src/lib/rules/sizes.js @@ -0,0 +1,119 @@ +// min:3 +sizeRuleGenerator('min', { + 'string': '{$alias} must have a minimum of {$number} characters.', + 'number': '{$alias} must be at least {$number}.', + 'object': '{$alias} must have at least {$number} items.' + }, + function isInvalid(value, number) { + return value < number; + } +); + +// greater_than:0 +sizeRuleGenerator('greater_than', { + 'string': '{$alias} must have more than {$number} characters.', + 'number': '{$alias} must be greater than {$number}.', + 'object': '{$alias} must have more than {$number} items.' + }, + function isInvalid(value, number) { + return value <= number; + } +); + +// max:10 +sizeRuleGenerator('max', { + 'string': '{$alias} must have no more than {$number} characters.', + 'number': '{$alias} must be no more than {$number}.', + 'object': '{$alias} must have no more than {$number} items.' + }, + function isInvalid(value, number) { + return value > number; + } +); + +// less_than:5 +sizeRuleGenerator('less_than', { + 'string': '{$alias} must have less than {$number} characters.', + 'number': '{$alias} must be less than {$number}.', + 'object': '{$alias} must have less than {$number} items.' + }, + function isInvalid(value, number) { + return value >= number; + } +); + +// equal:4.5 +sizeRuleGenerator('equal', { + 'string': '{$alias} must have {$number} characters.', + 'number': '{$alias} must equal {$number}.', + 'object': '{$alias} must have {$number} items.' + }, + function isInvalid(value, number) { + return value !== number; + } +); + +// not_equal:0 +sizeRuleGenerator('not_equal', { + 'string': '{$alias} must not have {$number} characters.', + 'number': '{$alias} must not equal {$number}.', + 'object': '{$alias} must not have {$number} items.' + }, + function isInvalid(value, number) { + return value === number; + } +); + +function sizeRuleGenerator(ruleName, defaultMessages, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + var number; + + if ( isString( params ) ) + { + number = parseFloat( params ); + } + else if ( isNumber( params ) ) + { + number = params; + } + + if ( isNaN( number ) ) + { + throw '"' + params + '" is not a valid number for the ' + ruleName + ' rule'; + } + + if ( isString( message ) ) + { + message = { + 'string': message, + 'number': message, + 'object': message + }; + } + + var messageTemplate = determineMessage( ruleName, message ); + var extra = { + $number: params + }; + + return function(value, model, setMessage) + { + var size = sizeof( value ); + var type = typeof( value ); + var typeMessage = messageTemplate[ type ]; + + if ( typeMessage && isInvalid( size, number ) ) + { + extra.$size = size; + + setMessage( generateMessage( field, getAlias( field ), value, model, typeMessage, extra ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessages; +} diff --git a/src/lib/rules/types.js b/src/lib/rules/types.js new file mode 100644 index 0000000..93de498 --- /dev/null +++ b/src/lib/rules/types.js @@ -0,0 +1,96 @@ + +ruleGenerator('array', + '{$alias} must be an array.', + function isInvalid(value) { + return !isArray( value ); + } +); + +ruleGenerator('object', + '{$alias} must be an object.', + function isInvalid(value) { + return !isObject( value ); + } +); + +ruleGenerator('string', + '{$alias} must be a string.', + function isInvalid(value) { + return !isString( value ); + } +); + +ruleGenerator('number', + '{$alias} must be a number.', + function isInvalid(value) { + return !isNumber( value ); + } +); + +ruleGenerator('boolean', + '{$alias} must be a true or false.', + function isInvalid(value) { + return !isBoolean( value ); + } +); + +ruleGenerator('model', + '{$alias} must have a value.', + function isInvalid(value) { + return !(value instanceof Model); + } +); + +ruleGenerator('whole', + '{$alias} must be a whole number.', + function isInvalid(value, model, setValue) { + var parsed = tryParseInt( value ); + var numeric = parseFloat( value ); + var invalid = !isNumber( parsed ); + if ( !invalid ) { + invalid = Math.floor( parsed ) !== numeric; + if ( !invalid ) { + setValue( parsed ); + } + } + return invalid; + } +); + +ruleGenerator('numeric', + '{$alias} must be numeric.', + function isInvalid(value, model, setValue) { + var parsed = tryParseFloat( value ); + var invalid = !isNumber( parsed ); + if ( !invalid ) { + setValue( parsed ); + } + return invalid; + } +); + +ruleGenerator('yesno', + '{$alias} must be a yes or no.', + function isInvalid(value, model, setValue) { + var mapped = Validation.Rules.yesno.map[ value ]; + var invalid = !isBoolean( mapped ); + if ( !invalid ) { + setValue( mapped ); + } + return invalid; + } +); + +Validation.Rules.yesno.map = +{ + 'true': true, + 't': true, + 'yes': true, + 'y': true, + '1': true, + 'false': false, + 'f': false, + 'no': false, + 'n': false, + '0': false +}; diff --git a/src/lib/transforms/abs.js b/src/lib/transforms/abs.js new file mode 100644 index 0000000..20b56aa --- /dev/null +++ b/src/lib/transforms/abs.js @@ -0,0 +1,14 @@ +Validation.Rules.abs = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + value = tryParseFloat( value ); + + if ( isNumber( value ) ) + { + value = Math.abs( value ); + } + + return value; + }; +}; diff --git a/src/lib/transforms/apply.js b/src/lib/transforms/apply.js new file mode 100644 index 0000000..5a8dd72 --- /dev/null +++ b/src/lib/transforms/apply.js @@ -0,0 +1,9 @@ +Validation.Rules.apply = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + model.$set( field, value ); + + return value; + }; +}; diff --git a/src/lib/transforms/base64.js b/src/lib/transforms/base64.js new file mode 100644 index 0000000..b173582 --- /dev/null +++ b/src/lib/transforms/base64.js @@ -0,0 +1,12 @@ +Validation.Rules.base64 = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + if ( global.btoa ) + { + value = global.btoa( value ); + } + + return value; + }; +}; diff --git a/src/lib/transforms/ceil.js b/src/lib/transforms/ceil.js new file mode 100644 index 0000000..f74324e --- /dev/null +++ b/src/lib/transforms/ceil.js @@ -0,0 +1,14 @@ +Validation.Rules.ceil = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + value = tryParseFloat( value ); + + if ( isNumber( value ) ) + { + value = Math.ceil( value ); + } + + return value; + }; +}; diff --git a/src/lib/transforms/endOfDay.js b/src/lib/transforms/endOfDay.js new file mode 100644 index 0000000..0afca54 --- /dev/null +++ b/src/lib/transforms/endOfDay.js @@ -0,0 +1,7 @@ +Validation.Rules.endOfDay = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + return endOfDay( value ); + }; +}; diff --git a/src/lib/transforms/filter.js b/src/lib/transforms/filter.js new file mode 100644 index 0000000..52db75e --- /dev/null +++ b/src/lib/transforms/filter.js @@ -0,0 +1,28 @@ +Validation.Rules.filter = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + if ( isArray( value ) ) + { + for (var i = value.length - 1; i >= 0; i--) + { + if ( !isValue( value[ i ] ) ) + { + value.splice( i, 1 ); + } + } + } + else if ( isObject( value ) ) + { + for (var prop in value) + { + if ( !isValue( value[ prop ] ) ) + { + delete value[ prop ]; + } + } + } + + return value; + }; +}; diff --git a/src/lib/transforms/floor.js b/src/lib/transforms/floor.js new file mode 100644 index 0000000..a978d21 --- /dev/null +++ b/src/lib/transforms/floor.js @@ -0,0 +1,14 @@ +Validation.Rules.floor = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + value = tryParseFloat( value ); + + if ( isNumber( value ) ) + { + value = Math.floor( value ); + } + + return value; + }; +}; diff --git a/src/lib/transforms/mod.js b/src/lib/transforms/mod.js new file mode 100644 index 0000000..19d7d76 --- /dev/null +++ b/src/lib/transforms/mod.js @@ -0,0 +1,21 @@ +Validation.Rules.mod = function(field, params, database, alias, message) +{ + var number = tryParseFloat( params ); + + if ( !isNumber( number ) ) + { + throw '"' + number + '" is not a valid number for the mod rule.'; + } + + return function(value, model, setMessage) + { + value = tryParseFloat( value ); + + if ( isNumber( value ) ) + { + value = value % number; + } + + return value; + }; +}; diff --git a/src/lib/transforms/null.js b/src/lib/transforms/null.js new file mode 100644 index 0000000..16e86fd --- /dev/null +++ b/src/lib/transforms/null.js @@ -0,0 +1,9 @@ +Validation.Rules.null = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + model.$set( field, null ); + + return null; + }; +}; diff --git a/src/lib/transforms/round.js b/src/lib/transforms/round.js new file mode 100644 index 0000000..b5ecf56 --- /dev/null +++ b/src/lib/transforms/round.js @@ -0,0 +1,14 @@ +Validation.Rules.round = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + value = tryParseFloat( value ); + + if ( isNumber( value ) ) + { + value = Math.round( value ); + } + + return value; + }; +}; diff --git a/src/lib/transforms/startOfDay.js b/src/lib/transforms/startOfDay.js new file mode 100644 index 0000000..560ff61 --- /dev/null +++ b/src/lib/transforms/startOfDay.js @@ -0,0 +1,7 @@ +Validation.Rules.startOfDay = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + return startOfDay( value ); + }; +}; diff --git a/src/lib/transforms/trim.js b/src/lib/transforms/trim.js new file mode 100644 index 0000000..f689c99 --- /dev/null +++ b/src/lib/transforms/trim.js @@ -0,0 +1,23 @@ +Validation.Rules.trim = function(field, params, database, alias, message) +{ + // String.trim polyfill + if ( !String.prototype.trim ) + { + var regex = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; + + String.prototype.trim = function() + { + return this.replace( regex, '' ); + }; + } + + return function(value, model, setMessage) + { + if ( isString( value ) ) + { + value = value.trim(); + } + + return value; + }; +}; diff --git a/src/lib/transforms/unbase64.js b/src/lib/transforms/unbase64.js new file mode 100644 index 0000000..cd28603 --- /dev/null +++ b/src/lib/transforms/unbase64.js @@ -0,0 +1,12 @@ +Validation.Rules.unbase64 = function(field, params, database, alias, message) +{ + return function(value, model, setMessage) + { + if ( global.atob ) + { + value = global.atob( value ); + } + + return value; + }; +}; diff --git a/src/lib/util.js b/src/lib/util.js new file mode 100644 index 0000000..07a1da1 --- /dev/null +++ b/src/lib/util.js @@ -0,0 +1,151 @@ +function tryParseFloat(x) +{ + var parsed = parseFloat( x ); + + if ( !isNaN( parsed ) ) + { + x = parsed; + } + + return x; +} + +function tryParseInt(x) +{ + var parsed = parseInt( x ); + + if ( !isNaN( parsed ) ) + { + x = parsed; + } + + return x; +} + +function startOfDay(d) +{ + if ( isDate( d ) ) + { + d.setHours( 0, 0, 0, 0 ); + } + else if ( isNumber( d ) ) + { + d = d - (d % 86400000); + } + + return d; +} + +function endOfDay(d) +{ + if ( isDate( d ) ) + { + d.setHours( 23, 59, 59, 999 ); + } + else if ( isNumber( d ) ) + { + d = d - (d % 86400000) + 86400000 - 1; + } + + return d; +} + +function ruleGenerator(ruleName, defaultMessage, isInvalid) +{ + Validation.Rules[ ruleName ] = function(field, params, database, getAlias, message) + { + checkNoParams( ruleName, field, params ); + + var messageTemplate = determineMessage( ruleName, message ); + + return function(value, model, setMessage) + { + function setValue( newValue ) + { + value = newValue; + } + + if ( isInvalid( value, model, setValue ) ) + { + setMessage( generateMessage( field, getAlias( field ), value, model, messageTemplate ) ); + } + + return value; + }; + }; + + Validation.Rules[ ruleName ].message = defaultMessage; +} + +function determineMessage(ruleName, message) +{ + return message || Validation.Rules[ ruleName ].message; +} + +function joinFriendly(arr, lastSeparator, itemSeparator, getAlias) +{ + var copy = arr.slice(); + + if ( getAlias ) + { + for (var i = 0; i < copy.length; i++) + { + copy[ i ] = getAlias( copy[ i ] ); + } + } + + var last = copy.pop(); + var lastSeparator = lastSeparator || 'and'; + var itemSeparator = itemSeparator || ', '; + + switch (copy.length) { + case 0: + return last; + case 1: + return copy[ 0 ] + ' ' + lastSeparator + ' ' + last; + default: + return copy.join( itemSeparator ) + itemSeparator + lastSeparator + ' ' + last; + } +} + +function mapFromArray(arr, value) +{ + var map = {}; + + for (var i = 0; i < arr.length; i++) + { + map[ arr[ i ] ] = value; + } + + return map; +} + +function checkNoParams(ruleName, field, params) +{ + if ( params ) + { + throw 'the rule ' + ruleName + ' for field ' + field + ' has no arguments'; + } +} + +function generateMessage(field, alias, value, model, message, extra) +{ + if ( isFunction( message ) ) + { + message = message( field, alias, value, model, extra ); + } + + var base = {}; + base.$field = field; + base.$alias = alias; + base.$value = value; + + transfer( model, base ); + + if ( isObject( extra ) ) + { + transfer( extra, base ); + } + + return format( message, base ); +} diff --git a/src/lib/validation.js b/src/lib/validation.js new file mode 100644 index 0000000..8c381f1 --- /dev/null +++ b/src/lib/validation.js @@ -0,0 +1,212 @@ +Rekord.on( Rekord.Events.Plugins, function(model, db, options) +{ + var validation = options.validation || Database.Defaults.validation; + + if ( isEmpty( validation ) ) + { + return; + } + + var rules = validation.rules || {}; + var messages = validation.messages || {}; + var aliases = validation.aliases || {}; + var required = !!validation.required; + + function getAlias(field) + { + return aliases[ field ] || field; + } + + db.validations = {}; + + for ( var field in rules ) + { + db.validations[ field ] = Validation.parseRules( rules[ field ], field, db, getAlias, messages[ field ] ) + } + + addMethod( model.prototype, '$validate', function() + { + var $this = this; + + this.$trigger( Model.Events.PreValidate, [this] ); + + this.$valid = true; + this.$validations = {}; + this.$validationMessages.length = 0; + + for (var field in db.validations) + { + var chain = db.validations[ field ]; + var value = this.$get( field ); + var fieldValid = true; + + var setMessage = function(message) + { + // Only accept for the first valid message + if ( message && fieldValid ) + { + fieldValid = false; + + $this.$validations[ field ] = message; + $this.$validationMessages.push( message ); + $this.$valid = false; + } + }; + + for (var i = 0; i < chain.length && fieldValid && value !== Validation.Stop; i++) + { + value = chain[ i ]( value, this, setMessage ); + } + } + + this.$trigger( this.$valid ? Model.Events.ValidatePass : Model.Events.ValidateFail, [this] ); + + return this.$valid; + }); + + replaceMethod( model.prototype, '$init', function($init) + { + return function() + { + this.$valid = undefined; + this.$validations = {}; + this.$validationMessages = []; + + return $init.apply( this, arguments ); + }; + }); + + if ( required ) + { + replaceMethod( model.prototype, '$save', function($save) + { + return function() + { + if ( this.$isDeleted() ) + { + Rekord.debug( Rekord.Debugs.SAVE_DELETED, this.$db, this ); + + return Promise.resolve( this ); + } + + if ( !this.$validate() ) + { + return Promise.resolve( this ); + } + + return $save.apply( this, arguments ); + }; + }); + } +}); + +Model.Events.PreValidate = 'pre-validate'; + +Model.Events.ValidatePass = 'validate-pass'; + +Model.Events.ValidateFail = 'validate-fail'; + +var Validation = +{ + Rules: {}, + Expression: {}, + Expressions: [], + Delimiter: /([|])/, + Escape: '\\', + RuleSeparator: ':', + Stop: {}, + + parseRules: function(rules, field, database, getAlias, message) + { + var validators = []; + + if ( isString( rules ) ) + { + rules = split( rules, this.Delimiter, this.Escape ); + } + + if ( isArray( rules ) ) + { + for (var i = 0; i < rules.length; i++) + { + var rule = rules[ i ]; + var validator = this.parseRule( rule, field, database, getAlias, message ); + + validators.push( validator ); + } + } + else if ( isObject( rules ) ) + { + for (var rule in rules) + { + var ruleMessageOrData = rules[ rule ]; + + var ruleMessage = isObject( ruleMessageOrData ) ? ruleMessageOrData.message : + ( isString( ruleMessageOrData ) ? ruleMessageOrData : undefined ); + + var ruleInput = isObject( ruleMessageOrData ) && ruleMessageOrData.message ? ruleMessageOrData.input : + ( isString( ruleMessageOrData ) ? undefined : ruleMessageOrData ); + + var validator = this.parseRule( rule, field, database, getAlias, ruleMessage || message, ruleInput ); + + validators.push( validator ); + } + } + + return validators; + }, + + parseRule: function(rule, field, database, getAlias, message, input) + { + var colon = rule.indexOf( this.RuleSeparator ); + var ruleName = colon === -1 ? rule : rule.substring( 0, colon ); + + if ( ruleName.charAt( 0 ) === '$' ) + { + return this.customValidator( ruleName, field, database, getAlias, message ); + } + + var ruleParams = colon === -1 ? input : rule.substring( colon + 1 ); + var validatorFactory = Validation.Rules[ ruleName ]; + + if ( !validatorFactory ) + { + throw ruleName + ' is not a valid rule'; + } + + return validatorFactory( field, ruleParams, database, getAlias, message ); + }, + + parseExpression: function(expr, database) + { + var parsers = Validation.Expressions; + + for (var i = 0; i < parsers.length; i++) + { + var parser = parsers[ i ]; + var expressionFunction = parser( expr, database ); + + if ( isFunction( expressionFunction ) ) + { + return expressionFunction; // (value, model) + } + } + + return noop; + }, + + customValidator: function(functionName, field, database, getAlias, message) + { + return function(value, model, setMessage) + { + var result = model[ functionName ]( value, getAlias, message ); + + if ( isString( result ) ) + { + setMessage( result ); + } + + return value; + }; + } +}; diff --git a/test/base.js b/test/base.js new file mode 100644 index 0000000..d2267db --- /dev/null +++ b/test/base.js @@ -0,0 +1,521 @@ + +// Configuration + +Rekord.autoload = true; +Rekord.Database.Defaults.load = Rekord.Load.All; + +QUnit.config.reorder = false; + +// Extra Assertions + +function isInstance(model, Class, message) +{ + ok( model instanceof Class, message ); +} + +function isType(value, type, message) +{ + strictEqual( typeof value, type, message ); +} + +function hasModel(rekord, key, model, message) +{ + strictEqual( rekord.get( key ), model, message ); +} + +function pushChanges(target, changes) +{ + var previous = {}; + + for (var prop in changes) + { + previous[ prop ] = target[ prop ]; + target[ prop ] = changes[ prop ]; + } + + return function() + { + Rekord.transfer( previous, target ); + }; +}; + +function currentTime() +{ + var counter = 1; + + return function newDefaultTime() + { + return counter++; + }; +} + +function currentDate() +{ + var start = new Date(); + + return function newDefaultDate() + { + start.setDate( start.getDate() + 1 ); + + return new Date( start.getTime() ); + }; +} + +// Extending Assert + +QUnit.assert.timer = function() +{ + return QUnit.assert.currentTimer = new TestTimer(); +}; + +function TestTimer() +{ + this.callbacks = []; + this.time = 0; +} + +TestTimer.prototype = +{ + wait: function(millis, func) + { + var callbacks = this.callbacks; + var at = millis + this.time; + + if ( callbacks[ at ] ) + { + callbacks[ at ].push( func ); + } + else + { + callbacks[ at ] = [ func ]; + } + }, + run: function() + { + var callbacks = this.callbacks; + + for (var i = 0; i < callbacks.length; i++) + { + var calls = callbacks[ i ]; + + this.time = i; + + if ( calls ) + { + for (var k = 0; k < calls.length; k++) + { + calls[ k ](); + } + } + } + } +}; + +function wait(millis, func) +{ + QUnit.assert.currentTimer.wait( millis, func ); +} + +// Mock File + +function TestFile(name, size, type, contents) +{ + this.name = name; + this.size = size; + this.type = type; + this.contents = contents; +} + +function TestFileReader() +{ + this.onload = Rekord.noop; +} + +TestFileReader.prototype = +{ + readAsText: function(file) + { + if ( TestFileReader.IMMEDIATE ) + { + this.readAsTextNow( file ); + } + else + { + TestFileReader.pending.push( [this, 'readAsTextNow', file] ); + } + }, + readAsTextNow: function(file) + { + this.onload( {target:{result: file.contents}} ); + }, + readAsDataURL: function(file) + { + if ( TestFileReader.IMMEDIATE ) + { + this.readAsDataURLNow( file ); + } + else + { + TestFileReader.pending.push( [this, 'readAsDataURLNow', file] ); + } + }, + readAsDataURLNow: function(file) + { + this.onload( {target:{result: 'data:text/plain;base64,' + btoa(file.contents)}} ); + } +}; + +TestFileReader.IMMEDIATE = true; +TestFileReader.pending = []; + +TestFileReader.executePending = function() +{ + for (var i = 0; i < TestFileReader.pending.length; i++) + { + var pair = TestFileReader.pending[ i ]; + + pair[ 0 ][ pair[ 1 ] ]( pair[ 2 ] ); + } + + TestFileReader.pending.length = 0; +}; + +window.File = TestFile; +window.FileList = Array; +window.FileReader = TestFileReader; + +// Utility Methods + +function offline() +{ + Rekord.setOffline(); + Rekord.forceOffline = true; +} + +function online() +{ + Rekord.forceOffline = false; + Rekord.setOnline(); +} + +function noline() +{ + Rekord.off( 'online offline' ); + online(); +} + +function restart() +{ + Rekord.promises = {}; + noline(); + online(); +} + +// Rekord.store."database name".(put|remove|all) + +Rekord.store = function(database) +{ + var store = Rekord.store[ database.name ]; + + if ( !store ) + { + store = Rekord.store[ database.name ] = new TestStore(); + } + + return store; +}; + +function TestStore() +{ + this.map = new Rekord.Map(); + this.valid = true; + this.delay = 0; + this.lastKey = this.lastRecord = this.lastOperation = null; +} + +TestStore.prototype = +{ + finishDelayed: function(success, failure, arg0, arg1) + { + var store = this; + + if ( store.delay > 0 ) + { + wait( store.delay, function() + { + store.finish( success, failure, arg0, arg1 ); + + }); + } + else + { + store.finish( success, failure, arg0, arg1 ); + } + }, + finish: function(success, failure, arg0, arg1) + { + if ( this.valid ) + { + if ( success ) success( arg0, arg1 ); + } + else + { + if ( failure ) failure( arg0, arg1 ); + } + }, + get: function(key, success, failure) + { + this.lastKey = key; + this.lastOperation = 'get'; + + var map = this.map; + function onGet() + { + var model = map.get( key ); + if ( model ) { + success.call( this, key, model ); + } else { + failure.apply( this, 404 ); + } + } + + this.finishDelayed( onGet, failure ); + }, + save: function(model, success, failure) + { + this.put( model.$key(), model, success, failure ); + }, + put: function(key, record, success, failure) + { + this.lastKey = key; + this.lastRecord = record; + this.lastOperation = 'put'; + + var map = this.map; + function onPut() + { + map.put( key, record ); + success.apply( this, arguments ); + } + + this.finishDelayed( onPut, failure, key, record ); + }, + remove: function(key, success, failure) + { + this.lastKey = key; + this.lastOperation = 'remove'; + + var map = this.map; + var removed = map.get( key ); + function onRemove() + { + map.remove( key ); + success.apply( this, arguments ); + } + + this.finishDelayed( onRemove, failure, key, removed ); + }, + all: function(success, failure) + { + this.lastOperation = 'all'; + this.finishDelayed( success, failure, this.map.values, this.map.keys ); + } +}; + + +// Rekord.live."database name".(save|remove) + +Rekord.live = function(database) +{ + var live = Rekord.live[ database.name ]; + + if ( !live ) + { + live = Rekord.live[ database.name ] = new TestLive( database ); + } + + return live; +}; + +function TestLive(database) +{ + this.database = database; + this.onHandleMessage = null; + this.lastMessage = null; +} + +TestLive.prototype = +{ + save: function(model, data) + { + this.lastMessage = { + op: 'SAVE', + key: model.$key(), + model: data + }; + + if ( this.onHandleMessage ) + { + this.onHandleMessage( this.lastMessage ); + } + }, + remove: function(model) + { + this.lastMessage = { + op: 'REMOVE', + key: model.$key() + }; + + if ( this.onHandleMessage ) + { + this.onHandleMessage( this.lastMessage ); + } + }, + liveSave: function(data) + { + var key = this.database.buildKeyFromInput( data ); + + this.database.liveSave( key, data ); + }, + liveRemove: function(input) + { + var key = this.database.buildKeyFromInput( input ); + + this.database.liveRemove( key ); + } +}; + +// Rekord.rest."database name".(all|create|update|remove) + +Rekord.rest = function(database) +{ + var rest = Rekord.rest[ database.name ]; + + if ( !rest ) + { + rest = Rekord.rest[ database.name ] = new TestRest(); + } + + return rest; +}; + +function TestRest() +{ + this.map = new Rekord.Map(); + this.queries = new Rekord.Map(); + this.status = 200; + this.returnValue = false; + this.delay = 0; + this.lastModel = this.lastRecord = this.lastOperation = null; +} + +TestRest.prototype = +{ + finishDelayed: function(success, failure, returnValue) + { + var rest = this; + + if ( rest.delay > 0 ) + { + wait( rest.delay, function() + { + rest.finish( success, failure, returnValue ); + + }); + } + else + { + rest.finish( success, failure, returnValue ); + } + }, + finish: function(success, failure, returnValue) + { + var offline = !Rekord.online || Rekord.forceOffline; + var status = offline ? 0 : this.status; + var successful = status >= 200 && status < 300; + var returnedValue = this.returnValue || returnValue; + + if ( successful ) + { + if ( success ) success( returnedValue, status ); + } + else + { + if ( failure ) failure( returnedValue, status ); + } + }, + get: function(model, success, failure) + { + this.lastOperation = 'get'; + this.lastModel = model; + + var map = this.map; + function onGet() + { + var cached = map.get( model.$key() ); + if ( cached ) { + success.call( this, cached ); + } else { + failure.call( this, null, 404 ); + } + } + + this.finishDelayed( onGet, failure, null ); + }, + create: function(model, encoded, success, failure) + { + this.lastOperation = 'create'; + this.lastModel = model; + this.lastRecord = encoded; + + var map = this.map; + function onCreate() + { + map.put( model.$key(), encoded ); + success.apply( this, arguments ); + } + + this.finishDelayed( onCreate, failure, {} ); + }, + update: function(model, encoded, success, failure) + { + this.lastOperation = 'update'; + this.lastModel = model; + this.lastRecord = encoded; + + var map = this.map; + function onUpdate() + { + var existing = map.get( model.$key() ); + Rekord.transfer( encoded, existing ); + success.apply( this, arguments ); + } + + this.finishDelayed( onUpdate, failure, {} ); + }, + remove: function(model, success, failure) + { + this.lastOperation = 'remove'; + this.lastModel = model; + + var map = this.map; + function onRemove() + { + map.remove( model.$key() ); + success.apply( this, arguments ); + } + + this.finishDelayed( onRemove, failure, {} ); + }, + all: function(success, failure) + { + this.lastOperation = 'all'; + this.finishDelayed( success, failure, this.map.values ); + }, + query: function(url, data, success, failure) + { + this.lastOperation = 'query'; + this.lastRecord = data; + this.finishDelayed( success, failure, this.queries.get( url ) ); + } +}; diff --git a/test/cases/rekord-validation.js b/test/cases/rekord-validation.js new file mode 100644 index 0000000..16f6df1 --- /dev/null +++ b/test/cases/rekord-validation.js @@ -0,0 +1,6765 @@ +module( 'Rekord Validation' ); + +test( 'rule accepted custom message', function(assert) +{ + var prefix = 'rule_accepted_custom_message_'; + + var agreedMessage = 'You must agree to the terms and conditions!'; + + var Agreement = Rekord({ + name: prefix + 'agreement', + fields: ['agreed', 'name'], + validation: { + rules: { + agreed: 'accepted' + }, + messages: { + agreed: agreedMessage + } + } + }); + + var a = Agreement.create({name: 'Terms & Conditions', agreed: false}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {agreed: agreedMessage} ); + deepEqual( a.$validationMessages, [agreedMessage] ); + + a.agreed = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule accepted default message', function(assert) +{ + var prefix = 'rule_accepted_default_message_'; + + var agreedMessage = 'agreed has not been accepted.'; + + var Agreement = Rekord({ + name: prefix + 'agreement', + fields: ['agreed', 'name'], + validation: { + rules: { + agreed: 'accepted' + } + } + }); + + var a = Agreement.create({name: 'Terms & Conditions', agreed: false}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {agreed: agreedMessage} ); + deepEqual( a.$validationMessages, [agreedMessage] ); + + a.agreed = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule accepted aliased message', function(assert) +{ + var prefix = 'rule_accepted_aliased_message_'; + + var agreedMessage = 'The Terms & Conditions has not been accepted.'; + + var Agreement = Rekord({ + name: prefix + 'agreement', + fields: ['agreed', 'name'], + validation: { + rules: { + agreed: 'accepted' + }, + aliases: { + agreed: 'The Terms & Conditions' + } + } + }); + + var a = Agreement.create({name: 'Terms & Conditions', agreed: false}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {agreed: agreedMessage} ); + deepEqual( a.$validationMessages, [agreedMessage] ); + + a.agreed = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule accepted override message', function(assert) +{ + var pop = pushChanges( Rekord.Validation.Rules.accepted, { + message: 'You must accept {$alias}.' + }); + + var prefix = 'rule_accepted_override_message_'; + + var agreedMessage = 'You must accept the agreement.'; + + var Agreement = Rekord({ + name: prefix + 'agreement', + fields: ['agreed', 'name'], + validation: { + rules: { + agreed: 'accepted' + }, + aliases: { + agreed: 'the agreement' + } + } + }); + + var a = Agreement.create({name: 'Terms & Conditions', agreed: false}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {agreed: agreedMessage} ); + deepEqual( a.$validationMessages, [agreedMessage] ); + + a.agreed = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + pop(); +}); + +test( 'rule accepted add acceptable', function(assert) +{ + var pop = pushChanges( Rekord.Validation.Rules.accepted.acceptable, { + 'yup': true, + '1': false + }); + + var prefix = 'rule_accepted_add_acceptable_'; + + var agreedMessage = 'The agreement has not been accepted.'; + + var Agreement = Rekord({ + name: prefix + 'agreement', + fields: ['agreed', 'name'], + validation: { + rules: { + agreed: 'accepted' + }, + aliases: { + agreed: 'The agreement' + } + } + }); + + var a = Agreement.create({name: 'Terms & Conditions', agreed: 1}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {agreed: agreedMessage} ); + deepEqual( a.$validationMessages, [agreedMessage] ); + + a.agreed = 'yup'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + pop(); +}); + +// rule accepted invalid arguments + +test( 'rule after custom message', function(assert) +{ + var prefix = 'rule_after_custom_message_'; + + var expectedMessage = 'The due date must be after yesterday!'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'after:yesterday' + }, + messages: { + due_date: expectedMessage + } + } + }); + + var a = Task.create({name: 'a', due_date: now - msInDay * 2 }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule after default message', function(assert) +{ + var prefix = 'rule_after_default_message_'; + + var expectedMessage = 'due_date must be after yesterday.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'after:yesterday' + } + } + }); + + var a = Task.create({name: 'a', due_date: now - msInDay * 2 }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule after aliased message', function(assert) +{ + var prefix = 'rule_after_alaised_message_'; + + var expectedMessage = 'The due date must be after yesterday.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'after:yesterday' + }, + aliases: { + due_date: 'The due date' + } + } + }); + + var a = Task.create({name: 'a', due_date: now - msInDay * 2 }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule after override message', function(assert) +{ + var pop = pushChanges( Rekord.Validation.Rules.after, { + message: 'The {$alias} when given must be after {$date}.' + }); + + var prefix = 'rule_after_override_message_'; + + var expectedMessage = 'The due date when given must be after yesterday.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'after:yesterday' + }, + aliases: { + due_date: 'due date' + } + } + }); + + var a = Task.create({name: 'a', due_date: now - msInDay * 2 }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + pop(); +}); + +test( 'rule after raw params', function(assert) +{ + var prefix = 'rule_after_raw_params_'; + + var msInDay = 86400000; + var now = Date.now(); + var yesterday = new Date( now - msInDay ); + var expectedMessage = 'due_date must be after yesterday.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: { + after: yesterday + } + }, + messages: { + due_date: expectedMessage + } + } + }); + + var a = Task.create({name: 'a', due_date: now - msInDay * 2 }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule after missing date', function(assert) +{ + var prefix = 'rule_after_missing_date_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['due_date'], + validation: { + rules: { + due_date: 'after' + } + } + }); + }, 'after validation rule requires a date expression argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['due_date'], + validation: { + rules: { + due_date: 'after:' + } + } + }); + }, 'after validation rule requires a date expression argument'); +}); + +test( 'rule after invalid date', function(assert) +{ + var prefix = 'rule_after_invalid_date_'; + + throws(function() { + Rekord({ + name: prefix, + fields: ['due_date'], + validation: { + rules: { + due_date: 'after:NOTDATE' + } + } + }); + }, 'NOTDATE is not a valid date expression for the after rule'); +}); + +test( 'rule after_on custom message', function(assert) +{ + var prefix = 'rule_after_on_custom_message_'; + + var expectedMessage = 'The due date must be after or equal to today!'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'after_on:today' + }, + messages: { + due_date: expectedMessage + } + } + }); + + var a = Task.create({name: 'a', due_date: now - msInDay }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule after_on default message', function(assert) +{ + var prefix = 'rule_after_on_default_message_'; + + var expectedMessage = 'due_date must be after or equal to today.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'after_on:today' + } + } + }); + + var a = Task.create({name: 'a', due_date: now - msInDay }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule after_on aliased message', function(assert) +{ + var prefix = 'rule_after_on_alaised_message_'; + + var expectedMessage = 'The due date must be after or equal to today.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'after_on:today' + }, + aliases: { + due_date: 'The due date' + } + } + }); + + var a = Task.create({name: 'a', due_date: now - msInDay }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule after_on override message', function(assert) +{ + var pop = pushChanges( Rekord.Validation.Rules.after_on, { + message: 'The {$alias} when given must be on or after {$date}.' + }); + + var prefix = 'rule_after_on_override_message_'; + + var expectedMessage = 'The due date when given must be on or after today.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'after_on:today' + }, + aliases: { + due_date: 'due date' + } + } + }); + + var a = Task.create({name: 'a', due_date: now - msInDay }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + pop(); +}); + +test( 'rule after_on raw params', function(assert) +{ + var prefix = 'rule_after_on_raw_params_'; + + var msInDay = 86400000; + var now = Date.now(); + var today = now; + var expectedMessage = 'due_date must be after or equal to today.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: { + after_on: today + } + }, + messages: { + due_date: expectedMessage + } + } + }); + + var a = Task.create({name: 'a', due_date: now - msInDay }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule after_on missing date', function(assert) +{ + var prefix = 'rule_after_on_missing_date_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['due_date'], + validation: { + rules: { + due_date: 'after_on' + } + } + }); + }, 'after_on validation rule requires a date expression argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['due_date'], + validation: { + rules: { + due_date: 'after_on:' + } + } + }); + }, 'after_on validation rule requires a date expression argument'); +}); + +test( 'rule after_on invalid date', function(assert) +{ + var prefix = 'rule_after_on_invalid_date_'; + + throws(function() { + Rekord({ + name: prefix, + fields: ['due_date'], + validation: { + rules: { + due_date: 'after_on:NOTDATE' + } + } + }); + }, 'NOTDATE is not a valid date expression for the after_on rule'); +}); + +test( 'rule before custom message', function(assert) +{ + var prefix = 'rule_before_custom_message_'; + + var expectedMessage = 'The due date must be before tomorrow!'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'before:tomorrow' + }, + messages: { + due_date: expectedMessage + } + } + }); + + var a = Task.create({name: 'a', due_date: now + msInDay }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule before default message', function(assert) +{ + var prefix = 'rule_before_default_message_'; + + var expectedMessage = 'due_date must be before tomorrow.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'before:tomorrow' + } + } + }); + + var a = Task.create({name: 'a', due_date: now + msInDay }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule before aliased message', function(assert) +{ + var prefix = 'rule_before_alaised_message_'; + + var expectedMessage = 'The due date must be before tomorrow.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'before:tomorrow' + }, + aliases: { + due_date: 'The due date' + } + } + }); + + var a = Task.create({name: 'a', due_date: now + msInDay }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule before override message', function(assert) +{ + var pop = pushChanges( Rekord.Validation.Rules.before, { + message: 'The {$alias} when given must be before {$date}.' + }); + + var prefix = 'rule_before_override_message_'; + + var expectedMessage = 'The due date when given must be before tomorrow.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'before:tomorrow' + }, + aliases: { + due_date: 'due date' + } + } + }); + + var a = Task.create({name: 'a', due_date: now + msInDay }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + pop(); +}); + +test( 'rule before raw params', function(assert) +{ + var prefix = 'rule_before_raw_params_'; + + var msInDay = 86400000; + var now = Date.now(); + var tomorrow = now + msInDay - 1; + var expectedMessage = 'due_date must be before tomorrow.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: { + before: tomorrow + } + }, + messages: { + due_date: expectedMessage + } + } + }); + + var a = Task.create({name: 'a', due_date: now + msInDay }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule before missing date', function(assert) +{ + var prefix = 'rule_before_missing_date_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['due_date'], + validation: { + rules: { + due_date: 'before' + } + } + }); + }, 'before validation rule requires a date expression argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['due_date'], + validation: { + rules: { + due_date: 'before:' + } + } + }); + }, 'before validation rule requires a date expression argument'); +}); + +test( 'rule before invalid date', function(assert) +{ + var prefix = 'rule_before_invalid_date_'; + + throws(function() { + Rekord({ + name: prefix, + fields: ['due_date'], + validation: { + rules: { + due_date: 'before:NOTDATE' + } + } + }); + }, 'NOTDATE is not a valid date expression for the before rule'); +}); + +test( 'rule before_on custom message', function(assert) +{ + var prefix = 'rule_before_on_custom_message_'; + + var expectedMessage = 'The due date must be before or equal to tomorrow!'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'before_on:tomorrow' + }, + messages: { + due_date: expectedMessage + } + } + }); + + var a = Task.create({name: 'a', due_date: now + msInDay * 2 }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule before_on default message', function(assert) +{ + var prefix = 'rule_before_on_default_message_'; + + var expectedMessage = 'due_date must be before or equal to tomorrow.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'before_on:tomorrow' + } + } + }); + + var a = Task.create({name: 'a', due_date: now + msInDay * 2 }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule before_on aliased message', function(assert) +{ + var prefix = 'rule_before_on_alaised_message_'; + + var expectedMessage = 'The due date must be before or equal to tomorrow.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'before_on:tomorrow' + }, + aliases: { + due_date: 'The due date' + } + } + }); + + var a = Task.create({name: 'a', due_date: now + msInDay * 2 }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule before_on override message', function(assert) +{ + var pop = pushChanges( Rekord.Validation.Rules.before_on, { + message: 'The {$alias} when given must be before or equal to {$date}.' + }); + + var prefix = 'rule_before_on_override_message_'; + + var expectedMessage = 'The due date when given must be before or equal to tomorrow.'; + var msInDay = 86400000; + var now = Date.now(); + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: 'before_on:tomorrow' + }, + aliases: { + due_date: 'due date' + } + } + }); + + var a = Task.create({name: 'a', due_date: now + msInDay * 2 }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + pop(); +}); + +test( 'rule before_on raw params', function(assert) +{ + var prefix = 'rule_before_on_raw_params_'; + + var msInDay = 86400000; + var now = Date.now(); + var tomorrow = now + msInDay; + var expectedMessage = 'due_date must be before or equal to tomorrow.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'], + validation: { + rules: { + due_date: { + before_on: tomorrow + } + }, + messages: { + due_date: expectedMessage + } + } + }); + + var a = Task.create({name: 'a', due_date: now + msInDay * 2 }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = now; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule before_on missing date', function(assert) +{ + var prefix = 'rule_before_on_missing_date_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['due_date'], + validation: { + rules: { + due_date: 'before_on' + } + } + }); + }, 'before_on validation rule requires a date expression argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['due_date'], + validation: { + rules: { + due_date: 'before_on:' + } + } + }); + }, 'before_on validation rule requires a date expression argument'); +}); + +test( 'rule before_on invalid date', function(assert) +{ + var prefix = 'rule_before_on_invalid_date_'; + + throws(function() { + Rekord({ + name: prefix, + fields: ['due_date'], + validation: { + rules: { + due_date: 'before_on:NOTDATE' + } + } + }); + }, 'NOTDATE is not a valid date expression for the before_on rule'); +}); + +test( 'rule required custom message', function(assert) +{ + var prefix = 'rule_required_custom_message_'; + + var expectedMessage = 'Task name is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'required' + }, + messages: { + name: expectedMessage + } + } + }); + + var a = Task.create({name: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 'a'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule required default message', function(assert) +{ + var prefix = 'rule_required_default_message_'; + + var expectedMessage = 'name is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'required' + } + } + }); + + var a = Task.create({name: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 'a'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule required aliased message', function(assert) +{ + var prefix = 'rule_required_aliased_message_'; + + var expectedMessage = 'Task name is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'required' + }, + aliases: { + name: 'Task name' + } + } + }); + + var a = Task.create({name: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 'a'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule required override message', function(assert) +{ + var pop = pushChanges( Rekord.Validation.Rules.required, { + message: '{$alias} is a required field.' + }); + + var prefix = 'rule_required_override_message_'; + + var expectedMessage = 'name is a required field.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'required' + } + } + }); + + var a = Task.create({name: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 'a'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + pop(); +}); + +// rule required invalid arguments + +test( 'rule min string', function(assert) +{ + var prefix = 'rule_min_string_'; + + var expectedMessage = 'name must have a minimum of 3 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'min:3' + } + } + }); + + var a = Task.create({name: '22'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '333'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule min number', function(assert) +{ + var prefix = 'rule_min_number_'; + + var expectedMessage = 'count must be at least 3.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['count'], + validation: { + rules: { + count: 'min:3' + } + } + }); + + var a = Task.create({count: 2}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 3; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule min array', function(assert) +{ + var prefix = 'rule_min_array_'; + + var expectedMessage = 'options must have at least 3 items.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['options'], + validation: { + rules: { + options: 'min:3' + } + } + }); + + var a = Task.create({options: [1, 2]}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {options: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.options = [1, 2, 3]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule min raw params', function(assert) +{ + var prefix = 'rule_min_raw_params_'; + + var expectedMessage = 'name must have a minimum of 3 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: {min: 3} + } + } + }); + + var a = Task.create({name: '22'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '333'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule min invalid arguments', function(assert) +{ + var prefix = 'rule_min_invalid_arguments_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['count'], + validation: { + rules: { + count: 'min' + } + } + }); + }, 'min validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['count'], + validation: { + rules: { + count: 'min:' + } + } + }); + }, 'min validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'nan', + fields: ['count'], + validation: { + rules: { + count: 'min:X' + } + } + }); + }, 'X is not a valid number for the min rule'); +}); + +// rule min custom message +// rule min aliased message +// rule min override message + +test( 'rule greater_than string', function(assert) +{ + var prefix = 'rule_greater_than_string_'; + + var expectedMessage = 'name must have more than 3 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'greater_than:3' + } + } + }); + + var a = Task.create({name: '333'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '4444'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule greater_than number', function(assert) +{ + var prefix = 'rule_greater_than_number_'; + + var expectedMessage = 'count must be greater than 3.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['count'], + validation: { + rules: { + count: 'greater_than:3' + } + } + }); + + var a = Task.create({count: 3}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule greater_than array', function(assert) +{ + var prefix = 'rule_greater_than_array_'; + + var expectedMessage = 'options must have more than 3 items.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['options'], + validation: { + rules: { + options: 'greater_than:3' + } + } + }); + + var a = Task.create({options: [1, 2, 3]}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {options: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.options = [1, 2, 3, 4]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule greater_than invalid arguments', function(assert) +{ + var prefix = 'rule_greater_than_invalid_arguments_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['count'], + validation: { + rules: { + count: 'greater_than' + } + } + }); + }, 'greater_than validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['count'], + validation: { + rules: { + count: 'greater_than:' + } + } + }); + }, 'greater_than validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'nan', + fields: ['count'], + validation: { + rules: { + count: 'greater_than:X' + } + } + }); + }, 'X is not a valid number for the greater_than rule'); +}); + +// rule greater_than custom message +// rule greater_than aliased message +// rule greater_than override message +// rule greater_than raw params + +test( 'rule max string', function(assert) +{ + var prefix = 'rule_max_string_'; + + var expectedMessage = 'name must have no more than 3 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'max:3' + } + } + }); + + var a = Task.create({name: '4444'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '333'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule max number', function(assert) +{ + var prefix = 'rule_max_number_'; + + var expectedMessage = 'count must be no more than 3.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['count'], + validation: { + rules: { + count: 'max:3' + } + } + }); + + var a = Task.create({count: 4}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 3; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule max array', function(assert) +{ + var prefix = 'rule_max_array_'; + + var expectedMessage = 'options must have no more than 3 items.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['options'], + validation: { + rules: { + options: 'max:3' + } + } + }); + + var a = Task.create({options: [1, 2, 3, 4]}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {options: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.options = [1, 2, 3]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule max invalid arguments', function(assert) +{ + var prefix = 'rule_max_invalid_arguments_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['count'], + validation: { + rules: { + count: 'max' + } + } + }); + }, 'max validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['count'], + validation: { + rules: { + count: 'max:' + } + } + }); + }, 'max validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'nan', + fields: ['count'], + validation: { + rules: { + count: 'max:X' + } + } + }); + }, 'X is not a valid number for the max rule'); +}); + +// rule max custom message +// rule max aliased message +// rule max override message +// rule max raw params + +test( 'rule less_than string', function(assert) +{ + var prefix = 'rule_less_than_string_'; + + var expectedMessage = 'name must have less than 3 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'less_than:3' + } + } + }); + + var a = Task.create({name: '333'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '22'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule less_than number', function(assert) +{ + var prefix = 'rule_less_than_number_'; + + var expectedMessage = 'count must be less than 3.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['count'], + validation: { + rules: { + count: 'less_than:3' + } + } + }); + + var a = Task.create({count: 3}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 2; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule less_than array', function(assert) +{ + var prefix = 'rule_less_than_array_'; + + var expectedMessage = 'options must have less than 3 items.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['options'], + validation: { + rules: { + options: 'less_than:3' + } + } + }); + + var a = Task.create({options: [1, 2, 3]}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {options: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.options = [1, 2]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule less_than invalid arguments', function(assert) +{ + var prefix = 'rule_less_than_invalid_arguments_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['count'], + validation: { + rules: { + count: 'less_than' + } + } + }); + }, 'less_than validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['count'], + validation: { + rules: { + count: 'less_than:' + } + } + }); + }, 'less_than validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'nan', + fields: ['count'], + validation: { + rules: { + count: 'less_than:X' + } + } + }); + }, 'X is not a valid number for the less_than rule'); +}); + +// rule less_than custom message +// rule less_than aliased message +// rule less_than override message +// rule less_than raw params + +test( 'rule equal string', function(assert) +{ + var prefix = 'rule_equal_string_'; + + var expectedMessage = 'name must have 3 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'equal:3' + } + } + }); + + var a = Task.create({name: '22'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '333'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule equal number', function(assert) +{ + var prefix = 'rule_equal_number_'; + + var expectedMessage = 'count must equal 3.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['count'], + validation: { + rules: { + count: 'equal:3' + } + } + }); + + var a = Task.create({count: 4}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 3; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule equal array', function(assert) +{ + var prefix = 'rule_equal_array_'; + + var expectedMessage = 'options must have 3 items.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['options'], + validation: { + rules: { + options: 'equal:3' + } + } + }); + + var a = Task.create({options: [1, 2, 3, 4]}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {options: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.options = [1, 2, 3]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule equal invalid arguments', function(assert) +{ + var prefix = 'rule_equal_invalid_arguments_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['count'], + validation: { + rules: { + count: 'equal' + } + } + }); + }, 'equal validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['count'], + validation: { + rules: { + count: 'equal:' + } + } + }); + }, 'equal validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'nan', + fields: ['count'], + validation: { + rules: { + count: 'equal:X' + } + } + }); + }, 'X is not a valid number for the equal rule'); +}); + +// rule equal custom message +// rule equal aliased message +// rule equal override message +// rule equal raw params + +test( 'rule not_equal string', function(assert) +{ + var prefix = 'rule_not_equal_string_'; + + var expectedMessage = 'name must not have 3 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'not_equal:3' + } + } + }); + + var a = Task.create({name: '333'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '22'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_equal number', function(assert) +{ + var prefix = 'rule_not_equal_number_'; + + var expectedMessage = 'count must not equal 3.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['count'], + validation: { + rules: { + count: 'not_equal:3' + } + } + }); + + var a = Task.create({count: 3}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_equal array', function(assert) +{ + var prefix = 'rule_not_equal_array_'; + + var expectedMessage = 'options must not have 3 items.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['options'], + validation: { + rules: { + options: 'not_equal:3' + } + } + }); + + var a = Task.create({options: [1, 2, 3]}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {options: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.options = [1, 2]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_equal invalid arguments', function(assert) +{ + var prefix = 'rule_not_equal_invalid_arguments_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['count'], + validation: { + rules: { + count: 'not_equal' + } + } + }); + }, 'not_equal validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['count'], + validation: { + rules: { + count: 'not_equal:' + } + } + }); + }, 'not_equal validation rule requires a number argument'); + + throws(function() { + Rekord({ + name: prefix + 'nan', + fields: ['count'], + validation: { + rules: { + count: 'not_equal:X' + } + } + }); + }, 'X is not a valid number for the not_equal rule'); +}); + +// rule not_equal custom message +// rule not_equal aliased message +// rule not_equal override message +// rule not_equal raw params + +test( 'rule array custom message', function(assert) +{ + var prefix = 'rule_array_custom_message_'; + + var expectedMessage = 'Task name must be an array.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'array' + }, + messages: { + name: expectedMessage + } + } + }); + + var a = Task.create({name: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = []; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule array default message', function(assert) +{ + var prefix = 'rule_array_default_message_'; + + var expectedMessage = 'name must be an array.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'array' + } + } + }); + + var a = Task.create({name: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = []; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule array aliased message', function(assert) +{ + var prefix = 'rule_array_aliased_message_'; + + var expectedMessage = 'Task name must be an array.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'array' + }, + aliases: { + name: 'Task name' + } + } + }); + + var a = Task.create({name: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = []; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule array override message', function(assert) +{ + var pop = pushChanges( Rekord.Validation.Rules.array, { + message: '{$alias} is an array field.' + }); + + var prefix = 'rule_array_override_message_'; + + var expectedMessage = 'name is an array field.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'array' + } + } + }); + + var a = Task.create({name: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = []; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + pop(); +}); + +// rule array invalid arguments + +test( 'rule string default message', function(assert) +{ + var prefix = 'rule_string_default_message_'; + + var expectedMessage = 'name must be a string.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'string' + } + } + }); + + var a = Task.create({name: 4}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 'what'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule string custom message +// rule string aliased message +// rule string override message +// rule string invalid arguments + +test( 'rule number default message', function(assert) +{ + var prefix = 'rule_number_default_message_'; + + var expectedMessage = 'name must be a number.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'number' + } + } + }); + + var a = Task.create({name: '4'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule number custom message +// rule number aliased message +// rule number override message +// rule number invalid arguments + +test( 'rule object default message', function(assert) +{ + var prefix = 'rule_object_default_message_'; + + var expectedMessage = 'name must be an object.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'object' + } + } + }); + + var a = Task.create({name: '4'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = {butt: ['what']}; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule object custom message +// rule object aliased message +// rule object override message +// rule object invalid arguments + +test( 'rule boolean default message', function(assert) +{ + var prefix = 'rule_boolean_default_message_'; + + var expectedMessage = 'name must be a true or false.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'boolean' + } + } + }); + + var a = Task.create({name: '4'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule boolean custom message +// rule boolean aliased message +// rule boolean override message +// rule boolean invalid arguments + +test( 'rule model default message', function(assert) +{ + var prefix = 'rule_model_default_message_'; + + var expectedMessage = 'name must have a value.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'model' + } + } + }); + + var b = Task.create(); + var a = Task.create({name: null}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = b; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule model custom message +// rule model aliased message +// rule model override message +// rule model invalid arguments + +test( 'rule between string', function(assert) +{ + var prefix = 'rule_between_string_'; + + var expectedMessage = 'name must have between 3 to 8 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'between:3,8' + } + } + }); + + var a = Task.create({name: '22'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '999999999'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '333'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.name = '88888888'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule between number', function(assert) +{ + var prefix = 'rule_between_number_'; + + var expectedMessage = 'count must be between 3 and 8.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['count'], + validation: { + rules: { + count: 'between:3,8' + } + } + }); + + var a = Task.create({count: 2}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 9; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 3; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.count = 8; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule between array', function(assert) +{ + var prefix = 'rule_between_array_'; + + var expectedMessage = 'options must have between 3 to 8 items.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['options'], + validation: { + rules: { + options: 'between:3,8' + } + } + }); + + var a = Task.create({options: [1, 2]}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {options: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.options = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {options: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.options = [1, 2, 3]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.options = [1, 2, 3, 4, 5, 6, 7, 8]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule between raw params', function(assert) +{ + var prefix = 'rule_between_raw_params_'; + + var expectedMessage = 'count must be between 3 and 8.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['count'], + validation: { + rules: { + count: { + between: [3, 8] + } + } + } + }); + + var a = Task.create({count: 2}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 9; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 3; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.count = 8; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule between raw params object', function(assert) +{ + var prefix = 'rule_between_raw_params_object_'; + + var expectedMessage = 'count must be between 3 and 8.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['count'], + validation: { + rules: { + count: { + between: {start: 3, end: 8} + } + } + } + }); + + var a = Task.create({count: 2}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 9; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 3; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.count = 8; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule between invalid arguments', function(assert) +{ + var prefix = 'rule_between_invalid_arguments_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['count'], + validation: { + rules: { + count: 'between' + } + } + }); + }, 'between validation rule requires a range argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['count'], + validation: { + rules: { + count: 'between:' + } + } + }); + }, 'between validation rule requires a range argument'); + + throws(function() { + Rekord({ + name: prefix + 'nan,', + fields: ['count'], + validation: { + rules: { + count: 'between:X' + } + } + }); + }, 'X is not a valid range of numbers for the between rule'); + + throws(function() { + Rekord({ + name: prefix + 'nan,num', + fields: ['count'], + validation: { + rules: { + count: 'between:X,3' + } + } + }); + }, 'X,3 is not a valid range of numbers for the between rule'); + + throws(function() { + Rekord({ + name: prefix + 'num,nan', + fields: ['count'], + validation: { + rules: { + count: 'between:4,' + } + } + }); + }, '4, is not a valid range of numbers for the between rule'); +}); + +// rule between custom message +// rule between aliased message +// rule between override message + +test( 'rule not_between string', function(assert) +{ + var prefix = 'rule_not_between_string_'; + + var expectedMessage = 'name must not have between 3 to 8 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'not_between:3,8' + } + } + }); + + var a = Task.create({name: '333'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '88888888'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '22'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.name = '999999999'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_between number', function(assert) +{ + var prefix = 'rule_not_between_number_'; + + var expectedMessage = 'count must not be between 3 and 8.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['count'], + validation: { + rules: { + count: 'not_between:3,8' + } + } + }); + + var a = Task.create({count: 3}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 8; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {count: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.count = 2; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.count = 9; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_between array', function(assert) +{ + var prefix = 'rule_not_between_array_'; + + var expectedMessage = 'options must not have between 3 to 8 items.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['options'], + validation: { + rules: { + options: 'not_between:3,8' + } + } + }); + + var a = Task.create({options: [1, 2, 3]}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {options: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.options = [1, 2, 3, 4, 5, 6, 7, 8]; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {options: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.options = [1, 2]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.options = [1, 2, 3, 4, 5, 6, 7, 8, 9]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_between raw params', function(assert) +{ + var prefix = 'rule_not_between_raw_params_'; + + var expectedMessage = 'name must not have between 3 to 8 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: { + not_between: [3, 8] + } + } + } + }); + + var a = Task.create({name: '333'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '88888888'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '22'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.name = '999999999'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_between raw params object', function(assert) +{ + var prefix = 'rule_not_between_raw_params_object_'; + + var expectedMessage = 'name must not have between 3 to 8 characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: { + not_between: {start: 3, end: 8} + } + } + } + }); + + var a = Task.create({name: '333'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '88888888'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '22'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.name = '999999999'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_between invalid arguments', function(assert) +{ + var prefix = 'rule_not_between_invalid_arguments_'; + + throws(function() { + Rekord({ + name: prefix + 'no_colon', + fields: ['count'], + validation: { + rules: { + count: 'not_between' + } + } + }); + }, 'not_between validation rule requires a range argument'); + + throws(function() { + Rekord({ + name: prefix + 'colon', + fields: ['count'], + validation: { + rules: { + count: 'not_between:' + } + } + }); + }, 'not_between validation rule requires a range argument'); + + throws(function() { + Rekord({ + name: prefix + 'nan,', + fields: ['count'], + validation: { + rules: { + count: 'not_between:X' + } + } + }); + }, 'X is not a valid range of numbers for the not_between rule'); + + throws(function() { + Rekord({ + name: prefix + 'nan,num', + fields: ['count'], + validation: { + rules: { + count: 'not_between:X,3' + } + } + }); + }, 'X,3 is not a valid range of numbers for the not_between rule'); + + throws(function() { + Rekord({ + name: prefix + 'num,nan', + fields: ['count'], + validation: { + rules: { + count: 'not_between:4,' + } + } + }); + }, '4, is not a valid range of numbers for the not_between rule'); +}); + +// rule not_between custom message +// rule not_between aliased message +// rule not_between override message + +test( 'rule whole custom message', function(assert) +{ + var prefix = 'rule_whole_custom_message_'; + + var expectedMessage = 'Task name must be a whole number.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'whole' + }, + messages: { + name: expectedMessage + } + } + }); + + var a = Task.create({name: 4.1}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '4'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule whole default message', function(assert) +{ + var prefix = 'rule_whole_default_message_'; + + var expectedMessage = 'name must be a whole number.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'whole' + } + } + }); + + var a = Task.create({name: 2.3}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '2'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule whole aliased message', function(assert) +{ + var prefix = 'rule_whole_aliased_message_'; + + var expectedMessage = 'Task name must be a whole number.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'whole' + }, + aliases: { + name: 'Task name' + } + } + }); + + var a = Task.create({name: 4.5}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '4'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule whole override message', function(assert) +{ + var pop = pushChanges( Rekord.Validation.Rules.whole, { + message: '{$alias} must be an integer.' + }); + + var prefix = 'rule_whole_override_message_'; + + var expectedMessage = 'name must be an integer.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'whole' + } + } + }); + + var a = Task.create({name: 6.6}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '6'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + pop(); +}); + +// rule whole invalid arguments + +test( 'rule numeric custom message', function(assert) +{ + var prefix = 'rule_numeric_custom_message_'; + + var expectedMessage = 'Task name must be numeric.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'numeric' + }, + messages: { + name: expectedMessage + } + } + }); + + var a = Task.create({name: 'taco tuesday'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 23.4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule numeric default message', function(assert) +{ + var prefix = 'rule_numeric_default_message_'; + + var expectedMessage = 'name must be numeric.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'numeric' + } + } + }); + + var a = Task.create({name: null}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '2.5'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule numeric aliased message', function(assert) +{ + var prefix = 'rule_numeric_aliased_message_'; + + var expectedMessage = 'Task name must be numeric.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'numeric' + }, + aliases: { + name: 'Task name' + } + } + }); + + var a = Task.create({name: 'y4.5'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 4.1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule numeric override message', function(assert) +{ + var pop = pushChanges( Rekord.Validation.Rules.numeric, { + message: '{$alias} must be a decimal.' + }); + + var prefix = 'rule_numeric_override_message_'; + + var expectedMessage = 'name must be a decimal.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'numeric' + } + } + }); + + var a = Task.create({name: true}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = '6.6'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + pop(); +}); + +// rule numeric invalid arguments + +test( 'rule alpha default message', function(assert) +{ + var prefix = 'rule_alpha_default_message_'; + + var expectedMessage = 'text should only contain alphabetic characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['text'], + validation: { + rules: { + text: 'alpha' + } + } + }); + + var a = Task.create({text: 'ayo2'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'w hat'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'okay'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.text = 'sure'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule alpha custom message +// rule alpha aliased message +// rule alpha override message +// rule alpha invalid arguments + +test( 'rule alpha_dash default message', function(assert) +{ + var prefix = 'rule_alpha_dash_default_message_'; + + var expectedMessage = 'text should only contain alpha-numeric characters, dashes, and underscores.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['text'], + validation: { + rules: { + text: 'alpha_dash' + } + } + }); + + var a = Task.create({text: 'ayo2 '}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'what?'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'o_kay34'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.text = 's-ure'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule alpha_dash custom message +// rule alpha_dash aliased message +// rule alpha_dash override message +// rule alpha_dash invalid arguments + +test( 'rule alpha_num default message', function(assert) +{ + var prefix = 'rule_alpha_num_default_message_'; + + var expectedMessage = 'text should only contain alpha-numeric characters.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['text'], + validation: { + rules: { + text: 'alpha_num' + } + } + }); + + var a = Task.create({text: 'ayo2 '}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'what_'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'okay34'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.text = 'sure'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule alpha_num custom message +// rule alpha_num aliased message +// rule alpha_num override message +// rule alpha_num invalid arguments + +test( 'rule email default message', function(assert) +{ + var prefix = 'rule_email_default_message_'; + + var expectedMessage = 'text is not a valid email.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['text'], + validation: { + rules: { + text: 'email' + } + } + }); + + var a = Task.create({text: '@meow.com'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'what@taco'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'pdiffenderfer@gmail.com'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.text = 'rekord.js+extra@gmail-site.com'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule email custom message +// rule email aliased message +// rule email override message +// rule email invalid arguments + +test( 'rule url default message', function(assert) +{ + var prefix = 'rule_url_default_message_'; + + var expectedMessage = 'text is not a valid URL.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['text'], + validation: { + rules: { + text: 'url' + } + } + }); + + var a = Task.create({text: '@meow'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'ftp://website.com'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'http://www.google.com/maps?q=Your+Mom'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.text = 'google.com'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule url custom message +// rule url aliased message +// rule url override message +// rule url invalid arguments + +test( 'rule uri default message', function(assert) +{ + var prefix = 'rule_uri_default_message_'; + + var expectedMessage = 'text is not a valid URI.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['text'], + validation: { + rules: { + text: 'uri' + } + } + }); + + var a = Task.create({text: '@meow'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = '192.168.2.1'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'http://www.google.com/maps?q=Your+Mom'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.text = 'ftp://website.domain'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule uri custom message +// rule uri aliased message +// rule uri override message +// rule uri invalid arguments + +test( 'rule phone default message', function(assert) +{ + var prefix = 'rule_phone_default_message_'; + + var expectedMessage = 'text is not a valid phone number.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['text'], + validation: { + rules: { + text: 'phone' + } + } + }); + + var a = Task.create({text: '666666'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = '1(717)341-567'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = '1(717)341-5675'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.text = '717 3415678'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule phone custom message +// rule phone aliased message +// rule phone override message +// rule phone invalid arguments + +test( 'rule regex default message', function(assert) +{ + var prefix = 'rule_regex_default_message_'; + + var expectedMessage = 'text is not a valid value.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['text'], + validation: { + rules: { + text: 'regex:/^[a-z][0-9][_-]$/i' + } + } + }); + + var a = Task.create({text: 'z0'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'r1.'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'Q4_'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.text = 't3-'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule regex raw params', function(assert) +{ + var prefix = 'rule_regex_raw_params_'; + + var expectedMessage = 'text is not a valid value.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['text'], + validation: { + rules: { + text: { + regex: /^[a-z][0-9][_-]$/i + } + } + } + }); + + var a = Task.create({text: 'z0'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'r1.'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {text: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.text = 'Q4_'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.text = 't3-'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule regex custom message +// rule regex aliased message +// rule regex override message +// rule regex invalid arguments + +test( 'rule date_like default message', function(assert) +{ + var prefix = 'rule_date_like_default_message_'; + + var expectedMessage = 'value must be a valid date.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'date_like' + } + } + }); + + var a = Task.create({value: '01/2016'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'not date either'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 67867567; // ms + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = '01/03/1989'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = new Date(); + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule date_like custom message +// rule date_like aliased message +// rule date_like override message +// rule date_like invalid arguments + +test( 'rule confirmed default message', function(assert) +{ + var prefix = 'rule_confirmed_default_message_'; + + var expectedMessage = 'value must match value_again.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value', 'value_again'], + validation: { + rules: { + value: 'confirmed:value_again' + } + } + }); + + var a = Task.create({value: 'taco', value_again: 'not taco'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'not taco either'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'not taco'; // ms + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 42; + a.value_again = 42; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule confirmed multiple fields +// rule confirmed custom message +// rule confirmed aliased message +// rule confirmed override message +// rule confirmed invalid arguments + +test( 'rule different default message', function(assert) +{ + var prefix = 'rule_different_default_message_'; + + var expectedMessage = 'value must not match value_again.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value', 'value_again'], + validation: { + rules: { + value: 'different:value_again' + } + } + }); + + var a = Task.create({value: 'taco', value_again: 'taco'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 1; + a.value_again = 1; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 1; + a.value_again = '1'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 42; + a.value_again = 43; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule different multiple fields +// rule different custom message +// rule different aliased message +// rule different override message +// rule different invalid arguments + +test( 'rule if_valid default message', function(assert) +{ + var prefix = 'rule_if_valid_default_message_'; + + var expectedMessage1 = 'name is required.'; + var expectedMessage2 = 'done must be a yes or no.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done'], + validation: { + rules: { + name: 'required', + done: 'if_valid:name|yesno' + } + } + }); + + var a = Task.create({name: '', done: 2}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage1} ); + deepEqual( a.$validationMessages, [expectedMessage1] ); + + a.name = null; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage1} ); + deepEqual( a.$validationMessages, [expectedMessage1] ); + + a.name = 'taco'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {done: expectedMessage2} ); + deepEqual( a.$validationMessages, [expectedMessage2] ); + + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule if_valid multiple fields +// rule if_valid custom message +// rule if_valid aliased message +// rule if_valid override message +// rule if_valid invalid arguments + +test( 'rule exists custom message', function(assert) +{ + var prefix = 'rule_exists_custom_message_'; + + var expectedMessage = 'Value must match an existing value.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'exists' + }, + messages: { + value: expectedMessage + } + } + }); + + Task.create({value: 'exists'}); + Task.create({value: 'also exists'}); + + var a = Task.create({value: 'not exists'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'does not exist either'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'exists'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 'also exists'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule exists raw params', function(assert) +{ + var prefix = 'rule_exists_raw_params'; + + var expectedMessage = 'Value must match an existing value.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: { + exists: {field: 'value'} + } + }, + messages: { + value: expectedMessage + } + } + }); + + Task.create({value: 'exists'}); + Task.create({value: 'also exists'}); + + var a = Task.create({value: 'not exists'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'does not exist either'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'exists'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 'also exists'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule exists foreign class +// rule exists different field +// rule exists foreign class field +// rule exists multiple fields +// rule exists default message +// rule exists aliased message +// rule exists override message +// rule exists raw params model class +// rule exists raw params models array +// rule exists invalid arguments + +test( 'rule unique custom message', function(assert) +{ + var prefix = 'rule_unique_custom_message_'; + + var expectedMessage = 'Value must be unique.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'unique' + }, + messages: { + value: expectedMessage + } + } + }); + + Task.create({value: 'unique1'}); + Task.create({value: 'unique2'}); + + var a = Task.create({value: 'unique1'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'unique2'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'unique3'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = ''; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule unique raw params', function(assert) +{ + var prefix = 'rule_unique_raw_params_'; + + var expectedMessage = 'Value must be unique.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: { + unique: {field: 'value'} + } + }, + messages: { + value: expectedMessage + } + } + }); + + Task.create({value: 'unique1'}); + Task.create({value: 'unique2'}); + + var a = Task.create({value: 'unique1'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'unique2'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'unique3'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = ''; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule unique foreign class +// rule unique different field +// rule unique foreign class field +// rule unique multiple fields +// rule unique default message +// rule unique aliased message +// rule unique override message +// rule unique raw params model class +// rule unique raw params models array +// rule unique invalid arguments + +test( 'rule contains default message', function(assert) +{ + var prefix = 'rule_contains_default_message_'; + + var expectedMessage = 'tasks does not contain an item whose done equals true.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'task_list', + fields: ['name'], + validation: { + rules: { + tasks: 'contains:done,true' + } + }, + hasMany: { + tasks: { + model: Task, + foreign: 'list_id' + } + } + }); + + var t0 = Task.create({name: 't0', done: false}); + var t1 = Task.create({name: 't1'}); + var a = TaskList.create({ + name: 'list1', + tasks: [ t0, t1 ] + }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t0.done = 1; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t1.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + t0.done = true; + t1.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule contains raw params', function(assert) +{ + var prefix = 'rule_contains_raw_params_'; + + var expectedMessage = 'tasks does not contain an item whose done equals true.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'task_list', + fields: ['name'], + validation: { + rules: { + tasks: { + contains: { + field: 'done', + value: true, + equals: Rekord.equals + } + } + } + }, + hasMany: { + tasks: { + model: Task, + foreign: 'list_id' + } + } + }); + + var t0 = Task.create({name: 't0', done: false}); + var t1 = Task.create({name: 't1'}); + var a = TaskList.create({ + name: 'list1', + tasks: [ t0, t1 ] + }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t0.done = 1; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t1.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + t0.done = true; + t1.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule contains raw params array', function(assert) +{ + var prefix = 'rule_contains_raw_params_array_'; + + var expectedMessage = 'tasks does not contain an item whose done equals true.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'task_list', + fields: ['name'], + validation: { + rules: { + tasks: { + contains: ['done', true, Rekord.equals] + } + } + }, + hasMany: { + tasks: { + model: Task, + foreign: 'list_id' + } + } + }); + + var t0 = Task.create({name: 't0', done: false}); + var t1 = Task.create({name: 't1'}); + var a = TaskList.create({ + name: 'list1', + tasks: [ t0, t1 ] + }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t0.done = 1; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t1.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + t0.done = true; + t1.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule contains custom message +// rule contains aliased message +// rule contains override message +// rule contains invalid arguments + +test( 'rule not_contains default message', function(assert) +{ + var prefix = 'rule_not_contains_default_message_'; + + var expectedMessage = 'tasks contains an item whose done equals true.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'task_list', + fields: ['name'], + validation: { + rules: { + tasks: 'not_contains:done,true' + } + }, + hasMany: { + tasks: { + model: Task, + foreign: 'list_id' + } + } + }); + + var t0 = Task.create({name: 't0', done: true}); + var t1 = Task.create({name: 't1'}); + var a = TaskList.create({ + name: 'list1', + tasks: [ t0, t1 ] + }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t0.done = false; + t1.done = true; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t1.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + t0.done = null; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_contains raw params', function(assert) +{ + var prefix = 'rule_not_contains_raw_params_'; + + var expectedMessage = 'tasks contains an item whose done equals true.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'task_list', + fields: ['name'], + validation: { + rules: { + tasks: { + not_contains: { + field: 'done', + value: true, + equals: Rekord.equals + } + } + } + }, + hasMany: { + tasks: { + model: Task, + foreign: 'list_id' + } + } + }); + + var t0 = Task.create({name: 't0', done: true}); + var t1 = Task.create({name: 't1'}); + var a = TaskList.create({ + name: 'list1', + tasks: [ t0, t1 ] + }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t0.done = false; + t1.done = true; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t1.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + t0.done = null; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_contains raw params array', function(assert) +{ + var prefix = 'rule_not_contains_raw_params_array_'; + + var expectedMessage = 'tasks contains an item whose done equals true.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'done', 'list_id'] + }); + + var TaskList = Rekord({ + name: prefix + 'task_list', + fields: ['name'], + validation: { + rules: { + tasks: { + not_contains: ['done', true, Rekord.equals] + } + } + }, + hasMany: { + tasks: { + model: Task, + foreign: 'list_id' + } + } + }); + + var t0 = Task.create({name: 't0', done: true}); + var t1 = Task.create({name: 't1'}); + var a = TaskList.create({ + name: 'list1', + tasks: [ t0, t1 ] + }); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t0.done = false; + t1.done = true; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t1.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + t0.done = null; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule not_contains custom message +// rule not_contains aliased message +// rule not_contains override message +// rule not_contains invalid arguments + +test( 'rule in default message', function(assert) +{ + var prefix = 'rule_in_default_message_'; + + var expectedMessage = 'value must be one of A, B, C, or true.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'in:A,B,C,true' + } + } + }); + + var a = Task.create({value: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'D'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'A'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 'B'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule in raw params', function(assert) +{ + var prefix = 'rule_in_raw_params_'; + + var expectedMessage = 'value must be one of A, B, C, or true.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: { + in: ['A', 'B', 'C', true] + } + } + } + }); + + var a = Task.create({value: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'D'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'A'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 'B'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule in custom message +// rule in aliased message +// rule in override message +// rule in invalid arguments +// rule in raw params function + +test( 'rule not_in default message', function(assert) +{ + var prefix = 'rule_not_in_default_message_'; + + var expectedMessage = 'value must not be one of A, B, C, or true.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'not_in:A,B,C,true' + } + } + }); + + var a = Task.create({value: 'A'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = true; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'D'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = null; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule not_in raw params', function(assert) +{ + var prefix = 'rule_not_in_raw_params_'; + + var expectedMessage = 'value must not be one of A, B, C, or true.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: { + not_in: ['A', 'B', 'C', true] + } + } + } + }); + + var a = Task.create({value: 'A'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = true; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'D'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = null; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule not_in custom message +// rule not_in aliased message +// rule not_in override message +// rule not_in invalid arguments +// rule not_in raw params function + +test( 'rule yesno default message', function(assert) +{ + var prefix = 'rule_yesno_default_message_'; + + var expectedMessage = 'value must be a yes or no.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'yesno' + } + } + }); + + var a = Task.create({value: 'A'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = null; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 'f'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule yesno custom yesno +// rule yesno custom message +// rule yesno aliased message +// rule yesno override message +// rule yesno invalid arguments + +test( 'rule required_if default message', function(assert) +{ + var prefix = 'rule_required_if_default_message_'; + + var expectedMessage = 'due_date is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['due_date', 'done'], + validation: { + rules: { + due_date: 'required_if:done,true,1' + } + } + }); + + var a = Task.create({due_date: '', done: true}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = null; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = '01/01/2000'; + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.done = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.due_date = ''; + a.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule required_if raw params', function(assert) +{ + var prefix = 'rule_required_if_raw_params_'; + + var expectedMessage = 'due_date is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['due_date', 'done'], + validation: { + rules: { + due_date: { + required_if: { + field: 'done', + values: [true, 1] + } + } + } + } + }); + + var a = Task.create({due_date: '', done: true}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = null; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = '01/01/2000'; + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.done = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.due_date = ''; + a.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule required_if raw params array', function(assert) +{ + var prefix = 'rule_required_if_raw_params_array_'; + + var expectedMessage = 'due_date is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['due_date', 'done'], + validation: { + rules: { + due_date: { + required_if: ['done', true, 1] + } + } + } + }); + + var a = Task.create({due_date: '', done: true}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = null; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = '01/01/2000'; + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.done = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.due_date = ''; + a.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule required_if custom message +// rule required_if aliased message +// rule required_if override message +// rule required_if invalid arguments + +test( 'rule required_unless default message', function(assert) +{ + var prefix = 'rule_required_unless_default_message_'; + + var expectedMessage = 'due_date is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['due_date', 'done'], + validation: { + rules: { + due_date: 'required_unless:done,false,0' + } + } + }); + + var a = Task.create({due_date: '', done: true}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = null; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = '01/01/2000'; + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.done = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.due_date = ''; + a.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule required_unless raw params', function(assert) +{ + var prefix = 'rule_required_unless_raw_params_'; + + var expectedMessage = 'due_date is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['due_date', 'done'], + validation: { + rules: { + due_date: { + required_unless: { + field: 'done', + values: [false, 0] + } + } + } + } + }); + + var a = Task.create({due_date: '', done: true}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = null; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = '01/01/2000'; + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.done = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.due_date = ''; + a.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule required_unless raw params array', function(assert) +{ + var prefix = 'rule_required_unless_raw_params_array_'; + + var expectedMessage = 'due_date is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['due_date', 'done'], + validation: { + rules: { + due_date: { + required_unless: ['done', false, 0] + } + } + } + }); + + var a = Task.create({due_date: '', done: true}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = null; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = '01/01/2000'; + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.done = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.due_date = ''; + a.done = false; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule required_unless custom message +// rule required_unless aliased message +// rule required_unless override message +// rule required_unless invalid arguments + +test( 'rule required_with default message', function(assert) +{ + var prefix = 'rule_required_with_default_message_'; + + var expectedMessage = 'due_date is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['due_date', 'done', 'summary'], + validation: { + rules: { + due_date: 'required_with:done,summary' + } + } + }); + + var a = Task.create({due_date: '', done: true, summary: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.done = null; + a.summary = 'summary'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = '01/01/2000'; + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.done = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.due_date = ''; + a.done = null; + a.summary = ''; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule required_with custom message +// rule required_with aliased message +// rule required_with override message +// rule required_with invalid arguments + +test( 'rule required_with_all default message', function(assert) +{ + var prefix = 'rule_required_with_all_default_message_'; + + var expectedMessage = 'due_date is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['due_date', 'done', 'summary'], + validation: { + rules: { + due_date: 'required_with_all:done,summary' + } + } + }); + + var a = Task.create({due_date: '', done: true, summary: 'summary'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.done = 'y'; + a.summary = 'summary'; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = '01/01/2000'; + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.done = null; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.due_date = ''; + a.done = null; + a.summary = ''; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule required_with_all custom message +// rule required_with_all aliased message +// rule required_with_all override message +// rule required_with_all invalid arguments + +test( 'rule required_without default message', function(assert) +{ + var prefix = 'rule_required_without_default_message_'; + + var expectedMessage = 'due_date is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['due_date', 'done', 'summary'], + validation: { + rules: { + due_date: 'required_without:done,summary' + } + } + }); + + var a = Task.create({due_date: '', done: null, summary: 'summary'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.done = 'y'; + a.summary = ''; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = '01/01/2000'; + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.done = null; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.due_date = 'taco'; + a.done = null; + a.summary = ''; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule required_without custom message +// rule required_without aliased message +// rule required_without override message +// rule required_without invalid arguments + +test( 'rule required_without_all default message', function(assert) +{ + var prefix = 'rule_required_without_all_default_message_'; + + var expectedMessage = 'due_date is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['due_date', 'done', 'summary'], + validation: { + rules: { + due_date: 'required_without_all:done,summary' + } + } + }); + + var a = Task.create({due_date: '', done: null, summary: ''}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.summary = null; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {due_date: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.due_date = '01/01/2000'; + a.summary = ''; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.due_date = null; + a.done = true; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.done = null; + a.summary = 'summary'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule required_without_all custom message +// rule required_without_all aliased message +// rule required_without_all override message +// rule required_without_all invalid arguments + +test( 'rule if default message', function(assert) +{ + var prefix = 'rule_if_default_message_'; + + var expectedMessage = 'value must be odd when over 6.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'if:value:min:6|regex:/[13579]$/' // odd + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 8}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 10; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 7; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 21; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule if raw params', function(assert) +{ + var prefix = 'rule_if_raw_params_'; + + var expectedMessage = 'value must be odd when over 6.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: { + if: { + field: 'value', + rules: { + min: 6 + } + }, + regex: /[13579]$/ // odd + } + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 8}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 10; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 7; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 21; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule if all', function(assert) +{ + var prefix = 'rule_if_all_'; + + var expectedMessage = 'value must be odd when over 6 and under 12.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'if:value:min:6\\|max:12|regex:/[13579]$/' // odd + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 8}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 10; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 11; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 7; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 14; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule if all raw params', function(assert) +{ + var prefix = 'rule_if_all_raw_params_'; + + var expectedMessage = 'value must be odd when over 6 and under 12.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: { + if: { + rules: { + min: 6, + max: 12 + } + }, + regex: /[13579]$/ // odd + } + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 8}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 10; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 11; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 7; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 14; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule if custom message +// rule if aliased message +// rule if override message +// rule if invalid arguments +// rule if raw params array +// rule if default field + +test( 'rule if_any default message', function(assert) +{ + var prefix = 'rule_if_any_default_message_'; + + var expectedMessage = 'value must be odd when over 6 or an array.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'if_any:value:min:6\\|array|regex:/[13579]$/' // odd + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 8}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = [1,2,3,4,5,6,7,8]; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 11; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = [1,2,3,4,5,6,7]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule if_any raw params', function(assert) +{ + var prefix = 'rule_if_any_raw_params_'; + + var expectedMessage = 'value must be odd when over 6 or an array.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: { + if_any: { + rules: 'min:6|array' + }, + regex: /[13579]$/ // odd + } + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 8}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = [1,2,3,4,5,6,7,8]; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 11; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = [1,2,3,4,5,6,7]; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule if_any custom message +// rule if_any aliased message +// rule if_any override message +// rule if_any invalid arguments +// rule if_any raw params array +// rule if_any default field + +test( 'rule if_not default message', function(assert) +{ + var prefix = 'rule_if_not_default_message_'; + + var expectedMessage = 'value must be odd when under 6.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'if_not:value:min:6|regex:/[13579]$/' // odd + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 4}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 2; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 3; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 7; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule if_not raw params', function(assert) +{ + var prefix = 'rule_if_not_raw_params_'; + + var expectedMessage = 'value must be odd when under 6.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: { + if_not: { + rules: { + min: 6 + } + }, + regex: /[13579]$/ // odd + } + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 4}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 2; + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 3; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 7; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); + + a.value = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +// rule if_not custom message +// rule if_not aliased message +// rule if_not override message +// rule if_not invalid arguments +// rule if_not raw params array +// rule if_not default field + +test( 'rule validate message', function(assert) +{ + var prefix = 'rule_validate_message_'; + + var expectedMessage = 'Tasks are required to have names.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'list_id'], + validation: { + rules: { + name: 'required' + } + } + }); + + var TaskList = Rekord({ + name: prefix + 'task_list', + fields: ['name'], + validation: { + rules: { + tasks: 'validate:message' + }, + messages: { + tasks: expectedMessage + } + }, + hasMany: { + tasks: { + model: Task, + foreign: 'list_id' + } + } + }); + + var a = new TaskList({ + name: 'a', + tasks: [ + {name: 't0'}, + {name: null} + ] + }); + + var t0 = a.tasks[0]; + var t1 = a.tasks[1]; + + strictEqual( t0.list_id, a.id ); + strictEqual( t1.list_id, a.id ); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t1.name = 't1'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule validate models', function(assert) +{ + var prefix = 'rule_validate_models_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'list_id'], + validation: { + rules: { + name: 'required' + } + } + }); + + var TaskList = Rekord({ + name: prefix + 'task_list', + fields: ['name'], + validation: { + rules: { + tasks: 'validate:models' + } + }, + hasMany: { + tasks: { + model: Task, + foreign: 'list_id' + } + } + }); + + var a = new TaskList({ + name: 'a', + tasks: [ + {name: 't0'}, + {name: null} + ] + }); + + var t0 = a.tasks[0]; + var t1 = a.tasks[1]; + var expectedMessage = Rekord.collect( t1 ); + + strictEqual( t0.list_id, a.id ); + strictEqual( t1.list_id, a.id ); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t1.name = 't1'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'rule validate validations', function(assert) +{ + var prefix = 'rule_validate_validations_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'list_id'], + validation: { + rules: { + name: 'required' + } + } + }); + + var TaskList = Rekord({ + name: prefix + 'task_list', + fields: ['name'], + validation: { + rules: { + tasks: 'validate:validations' + } + }, + hasMany: { + tasks: { + model: Task, + foreign: 'list_id' + } + } + }); + + var a = new TaskList({ + name: 'a', + tasks: [ + {name: 't0'}, + {name: null} + ] + }); + + var t0 = a.tasks[0]; + var t1 = a.tasks[1]; + + var expectedMessage = {} + expectedMessage[ t1.id ] = { + name: 'name is required.' + }; + + strictEqual( t0.list_id, a.id ); + strictEqual( t1.list_id, a.id ); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {tasks: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + t1.name = 't1'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'custom validation', function(assert) +{ + var prefix = 'custom_validation_'; + + var expectedMessage = 'Name must start with a two character code followed by a colon followed by a description.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: '$isValidName' + } + }, + methods: { + $isValidName: function(nm) { + if (nm && !/^\w\w\:.*$/.test(nm)) { + return expectedMessage; + } + } + } + }); + + var a = Task.create({name: 'not valid'}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 'TK: meow'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'validation required', function(assert) +{ + var prefix = 'validation_required_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: 'required' + }, + required: true + } + }); + + var a = new Task(); + + strictEqual( a.$valid, void 0 ); + notOk( a.$isSaved() ); + + a.$save(); + + strictEqual( a.$valid, false ); + notOk( a.$isSaved() ); + + a.name = 'Hello World!'; + a.$save(); + + strictEqual( a.$valid, true ); + ok( a.$isSaved() ); +}); + +test( 'rule specific message', function(assert) +{ + var prefix = 'rule_specific_message_'; + + var expectedMessage = 'Task name is required.' + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name'], + validation: { + rules: { + name: { + 'required': expectedMessage + } + } + } + }); + + var a = new Task(); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 't1'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'alias multiple message', function(assert) +{ + var prefix = 'alias_multiple_message_'; + + var expectedMessage = 'Task name must match task password.' + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'password'], + validation: { + rules: { + name: 'confirmed:password' + }, + aliases: { + name: 'Task name', + password: 'task password' + } + } + }); + + var a = new Task({name: 0, password: 1}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {name: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.name = 1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'expression date', function(assert) +{ + var prefix = 'expression_date_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'] + }); + + var expr = Rekord.Validation.parseExpression( '10/03/2014', Task.Database ); + + ok( Rekord.isFunction( expr ) ); + + var millis = expr(); + + ok( Rekord.isNumber( millis ) ); + + var date = new Date( millis ); + + strictEqual( 9, date.getMonth() ); + strictEqual( 3, date.getDate() ); + strictEqual( 2014, date.getFullYear() ); +}); + +test( 'expression field', function(assert) +{ + var prefix = 'expression_field_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'] + }); + + var expr = Rekord.Validation.parseExpression( 'name', Task.Database ); + + ok( Rekord.isFunction( expr ) ); + + var model = new Task({name: 'taskName'}); + + var name = expr( null, model ); + + strictEqual( name, 'taskName' ); +}); + +test( 'expression relative', function(assert) +{ + var prefix = 'expression_relative_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'] + }); + + var expr = Rekord.Validation.parseExpression( '+1hr', Task.Database ); + + ok( Rekord.isFunction( expr ) ); + + var hourlater = expr(); + var expected = Date.now() + 60 * 60 * 1000; + + ok( Math.abs( hourlater - expected ) < 10 ); +}); + +test( 'expression today', function(assert) +{ + var prefix = 'expression_today_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'] + }); + + var expr = Rekord.Validation.parseExpression( 'today', Task.Database ); + + ok( Rekord.isFunction( expr ) ); + + var exprTime = expr(); + var expected = Rekord.startOfDay( new Date() ).getTime() + 0 * (24 * 60 * 60 * 1000); + + ok( Math.abs( exprTime - expected ) < 10 ); +}); + +test( 'expression tomorrow', function(assert) +{ + var prefix = 'expression_tomorrow_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'] + }); + + var expr = Rekord.Validation.parseExpression( 'tomorrow', Task.Database ); + + ok( Rekord.isFunction( expr ) ); + + var exprTime = expr(); + var expected = Rekord.startOfDay( new Date() ).getTime() + 1 * (24 * 60 * 60 * 1000); + + ok( Math.abs( exprTime - expected ) < 10 ); +}); + +test( 'expression yesterday', function(assert) +{ + var prefix = 'expression_yesterday_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['name', 'due_date'] + }); + + var expr = Rekord.Validation.parseExpression( 'yesterday', Task.Database ); + + ok( Rekord.isFunction( expr ) ); + + var exprTime = expr(); + var expected = Rekord.startOfDay( new Date() ).getTime() - 1 * (24 * 60 * 60 * 1000); + + ok( Math.abs( exprTime - expected ) < 10 ); +}); + +test( 'transform ceil', function(assert) +{ + var prefix = 'transform_ceil_'; + + var expectedMessage = 'value must be over 3.5'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'ceil|min:3.5' + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 3}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 3.1; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'transform floor', function(assert) +{ + var prefix = 'transform_floor_'; + + var expectedMessage = 'value must be over 3.5'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'floor|min:3.5' + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 3.9}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'transform trim', function(assert) +{ + var prefix = 'transform_trim_'; + + var expectedMessage = 'value is required.'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'trim|required' + } + } + }); + + var a = Task.create({value: ' '}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 'a'; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'transform round', function(assert) +{ + var prefix = 'transform_round_'; + + var expectedMessage = 'value must be over 3.5'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'round|min:3.5' + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 3.4}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'transform mod', function(assert) +{ + var prefix = 'transform_mod_'; + + var expectedMessage = 'value must be even'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'mod:2|equal:0' + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: 3}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'transform abs', function(assert) +{ + var prefix = 'transform_abs_'; + + var expectedMessage = 'value must be over 3.5'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'abs|floor|min:3.5' + }, + messages: { + value: expectedMessage + } + } + }); + + var a = Task.create({value: -3.9}); + + notOk( a.$validate() ); + notOk( a.$valid ); + deepEqual( a.$validations, {value: expectedMessage} ); + deepEqual( a.$validationMessages, [expectedMessage] ); + + a.value = 4; + + ok( a.$validate() ); + ok( a.$valid ); + deepEqual( a.$validations, {} ); + deepEqual( a.$validationMessages, [] ); +}); + +test( 'transform apply', function(assert) +{ + var prefix = 'transform_apply_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'abs|floor|apply' + } + } + }); + + var a = Task.create({value: '-3.9'}); + + ok( a.$validate() ); + + strictEqual( a.value, 3 ); +}); + +test( 'transform filter', function(assert) +{ + var prefix = 'transform_filter_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'filter|apply' + } + } + }); + + var a = Task.create({value: [null, 1, 2, null, 3, 4, null]}); + + ok( a.$validate() ); + + deepEqual( a.value, [1, 2, 3, 4] ); +}); + +test( 'transform startOfDay', function(assert) +{ + var prefix = 'transform_startOfDay_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'startOfDay' + } + } + }); + + var d = new Date(); + + var a = Task.create({value: d}); + + ok( a.$validate() ); + + strictEqual( a.value.getHours(), 0 ); +}); + +test( 'transform endOfDay', function(assert) +{ + var prefix = 'transform_endOfDay_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'endOfDay' + } + } + }); + + var d = new Date(); + + var a = Task.create({value: d}); + + ok( a.$validate() ); + + strictEqual( a.value.getHours(), 23 ); +}); + +test( 'transform base64', function(assert) +{ + var prefix = 'transform_base64_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'base64|apply' + } + } + }); + + var a = Task.create({value: 'Hello World'}); + + ok( a.$validate() ); + + strictEqual( a.value, 'SGVsbG8gV29ybGQ=' ); +}); + +test( 'transform unbase64', function(assert) +{ + var prefix = 'transform_unbase64_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'unbase64|apply' + } + } + }); + + var a = Task.create({value: 'SGVsbG8gV29ybGQ='}); + + ok( a.$validate() ); + + strictEqual( a.value, 'Hello World' ); +}); + +test( 'transform null', function(assert) +{ + var prefix = 'transform_null_'; + + var Task = Rekord({ + name: prefix + 'task', + fields: ['value'], + validation: { + rules: { + value: 'if::equal:0|null' + } + } + }); + + var a = Task.create({value: 'notnull'}); + + ok( a.$validate() ); + + strictEqual( a.value, 'notnull' ); + + a.value = 0; + + ok( a.$validate() ); + + strictEqual( a.value, null ); +}); + + +// transform null diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..3ba9637 --- /dev/null +++ b/test/index.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + +

Rekord Validation

+

+
+

+
    +
    +
    +
    + + +