Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Validations #13

Open
wants to merge 15 commits into from

2 participants

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
1  lib/bundler.rb
@@ -37,6 +37,7 @@ def files
model_log
model_rest
model_uid
+ model_validations
model_version
)
end
View
11 src/model.js
@@ -11,9 +11,11 @@ var Model = function(name, class_methods, instance_methods) {
if (jQuery.isFunction(this.initialize)) this.initialize()
};
- // Persistence is special, remove it from class_methods.
+ // Persistence & validations are special, remove them from class_methods.
var persistence = class_methods.persistence
+ var validation_rules = class_methods.validates;
delete class_methods.persistence
+ delete class_methods.validates;
// Apply class methods and extend with any custom class methods. Make sure
// vitals are added last so they can't be overridden.
@@ -30,8 +32,13 @@ var Model = function(name, class_methods, instance_methods) {
// Initialise persistence with a reference to the class.
if (persistence) model.persistence = persistence(model)
+ // Initialise a validator object for this class.
+ if (validation_rules) {
+ model.validator = Model.Validator(validation_rules);
+ };
+
// Add default and custom instance methods.
- jQuery.extend(model.prototype, Model.Callbacks, Model.InstanceMethods,
+ jQuery.extend(model.prototype, Model.Callbacks, Model.InstanceMethods, Model.Validations,
instance_methods);
return model;
View
4 src/model_class_methods.js
@@ -24,6 +24,10 @@ Model.ClassMethods = {
return this.collection;
},
+ any: function () {
+ return this.count() > 0;
+ },
+
count: function() {
return this.collection.length;
},
View
3  src/model_instance_methods.js
@@ -110,6 +110,9 @@ Model.InstanceMethods = {
valid: function() {
this.errors.clear();
this.validate();
+ if (this.constructor.validator) {
+ this.constructor.validator.run.call(this);
+ };
return this.errors.size() === 0;
},
View
123 src/model_validations.js
@@ -0,0 +1,123 @@
+Model.Validator = function (rules) {
+ var rules = rules;
+
+ // http://dl.dropbox.com/u/35146/js/tests/isNumber.html
+ var isNumeric = function (n) {
+ return !isNaN(parseFloat(n)) && isFinite(n);
+ };
+
+ var defaultOptions = {
+ "condition": function () { return true; }
+ };
+
+ var validationMethods = {
+
+ exclusionOf: function (attrName, attrValue, options) {
+ if (options.condition.call(this)) {
+ if (options['in'].indexOf(attrValue) !== -1) {
+ this.errors.add(attrName, options.message || "should not be one of " + options['in'].join(', '))
+ };
+ };
+ },
+
+ inclusionOf: function (attrName, attrValue, options) {
+ if (options.condition.call(this)) {
+ if (options['in'].indexOf(attrValue) === -1) {
+ this.errors.add(attrName, options.message || "should be one of " + options['in'].join(', '))
+ };
+ };
+ },
+
+ presenceOf: function (attrName, attrValue, options) {
+ if (options.condition.call(this)) {
+ if ((attrValue !== undefined && attrValue.length == 0) || (attrValue === undefined)) {
+ this.errors.add(attrName, options.message || "should not be blank");
+ };
+ };
+ },
+
+ // numeric strings will pass validation by default, i.e. "1"
+ numericalityOf: function (attrName, attrValue, options) {
+ var self = this;
+ var addError = function () { self.errors.add(attrName, options.message || "should be numeric"); };
+
+ if (options.condition.call(this)) {
+ if (options.allowNumericStrings) {
+ if (!isNumeric(attrValue)) {
+ addError();
+ };
+ } else if (typeof(attrValue) != "number" || isNaN(attrValue)) {
+ addError();
+ };
+ };
+ },
+
+ lengthOf: function (attrName, attrValue, options) {
+ if (options.condition.call(this)) {
+ if (attrValue.length < options.min || attrValue.length > options.max) {
+ this.errors.add(attrName, options.message || "is too short or too long");
+ };
+ };
+ },
+
+ formatOf: function (attrName, attrValue, options) {
+ if (options.condition.call(this)) {
+ if (!options["with"].test(attrValue)) {
+ this.errors.add(attrName, options.message || "is the wrong format");
+ };
+ };
+ },
+
+ uniquenessOf: function (attrName, attrValue, options) {
+ var instanceToValidat = this
+ if (options.condition.call(this)) {
+ if (this.constructor
+ .select(function () {
+ return this !== instanceToValidat;
+ })
+ .select(function () {
+ return this.attr(attrName) == attrValue;
+ })
+ .any()) {
+ this.errors.add(attrName, options.message || "should be unique");
+ };
+ };
+ }
+ };
+
+ var ruleWithoutOptions = function (ruleName, ruleValue) {
+ for (var i=0; i < ruleValue.length; i++) {
+ validationMethods[ruleName].call(
+ this,
+ ruleValue[i],
+ this.attr(ruleValue[i]),
+ defaultOptions
+ );
+ };
+ };
+
+ var ruleWithOptions = function (ruleName, ruleValue) {
+ for (attributeName in ruleValue) {
+ validationMethods[ruleName].call(
+ this,
+ attributeName,
+ this.attr(attributeName),
+ $.extend({}, defaultOptions, ruleValue[attributeName])
+ );
+ };
+ };
+
+ var run = function () {
+ for (rule in rules) {
+ if ($.isArray(rules[rule])) {
+ ruleWithoutOptions.call(this, rule, rules[rule]);
+ } else {
+ ruleWithOptions.call(this, rule, rules[rule]);
+ };
+ };
+ };
+
+ return {
+ run: run
+ };
+};
View
6 test/tests/model_class_methods.js
@@ -66,15 +66,19 @@ test("maintaining a collection of unique models by object, id and uid", function
equals(Post.count(), 2)
})
-test("detect, select, first, last, count (with chaining)", function() {
+test("detect, select, first, last, count, any (with chaining)", function() {
var Post = Model('post');
+ ok(!Post.any(), "there shouldn't be any posts when none have been added to the colleciton")
+
var post1 = new Post({ id: 1, title: "Foo" });
var post2 = new Post({ id: 2, title: "Bar" });
var post3 = new Post({ id: 3, title: "Bar" });
Post.add(post1, post2, post3);
+ ok(Post.any(), "there should be some posts once the instances have been added to the collection")
+
var indexes = [];
equals(Post.detect(function(i) {
View
195 test/tests/model_validations.js
@@ -0,0 +1,195 @@
+module("Model.Validations");
+
+test("validatesPresenceOf", function () {
+
+ var Post = Model("post", {
+ validates: {
+ presenceOf: ['title']
+ }
+ });
+
+ var validPost = new Post ({ title: "Foo", body: "..." });
+ var invalidPost_noTitle = new Post ({body: "..."});
+ var invalidPost_blankTitle = new Post ({title: "", body: "..."});
+
+ ok(validPost.valid(), "should be valid with a title");
+ ok(!invalidPost_noTitle.valid(), "should be invalid without a title attribute");
+ ok(!invalidPost_blankTitle.valid(), "should be invalid with a blank title attribute");
+});
+
+test("validatesNumericalityOf without options", function () {
+ var Post = Model("post", {
+ validates: {
+ numericalityOf: ['views']
+ }
+ });
+
+ var validPost = new Post({ views: 10 });
+ var validPost_stringReprOfNumber = new Post ({ views: "10"});
+ var invalidPost_notNumeric = new Post({ views: 'not numeric' });
+ var invalidPost_missingAttribute = new Post({ notViews: 1});
+
+ ok(validPost.valid(), "should be valid with a numeric attribute");
+ ok(!validPost_stringReprOfNumber.valid(), "should not be valid with a string representation of a number, by default");
+ ok(!invalidPost_notNumeric.valid(), "should not be valid with a string value");
+ ok(!invalidPost_missingAttribute.valid(), "should not be valid if the attr is missing, by default");
+});
+
+test("validatesNumericalityOf with options", function () {
+
+ var Post = Model("post", {
+ validates: {
+ numericalityOf: {
+ 'views': {
+ allowNumericStrings: true,
+ message: "custom message"
+ }
+ }
+ }
+ });
+
+ var validPost_stringReprOfNumber = new Post ({ views: "10"});
+ var invalidPost = new Post({views: "blah"})
+
+ ok(validPost_stringReprOfNumber.valid(), "should be valid with a string representation of a number");
+ ok(!invalidPost.valid())
+ equal(invalidPost.errors.errors.views[0], "custom message", "should use a custom validation message");
+});
+
+test("validatesLengthOf", function () {
+ var Post = Model("post", {
+ validates: {
+ lengthOf: {
+ 'title': {
+ min: 5,
+ max: 10,
+ message: "custom message"
+ }
+ }
+ }
+ });
+
+ var validPost = new Post ({ title: "just right", body: "..." });
+ var invalidPost_tooShort = new Post ({ title: "1", body: "..." });
+ var invalidPost_tooLong = new Post ({ title: "this is too long!", body: "..." });
+
+ ok(validPost.valid(), "should be valid with the right length title");
+ ok(!invalidPost_tooShort.valid(), "should be invalid with a title that is too short");
+ ok(!invalidPost_tooLong.valid(), "should be invalid with a title that is too long");
+
+ equal(invalidPost_tooLong.errors.errors.title[0], "custom message", "should use a custom validation message");
+});
+
+test("validatesLengthOf with default options", function () {
+ var Post = Model("post", {
+ validates: {
+ lengthOf: {
+ 'title': { max: 10 }
+ }
+ }
+ });
+
+ var validPost = new Post ({ title: "just right", body: "..." });
+ var invalidPost_tooLong = new Post ({ title: "this is too long!", body: "..." });
+
+ ok(validPost.valid(), "should be valid with the right length title");
+ ok(!invalidPost_tooLong.valid(), "should be invalid with a title that is too long");
+});
+
+test("validatesUniqunessOf", function () {
+ var Post = Model("post", {
+ validates: {
+ uniquenessOf: ['title']
+ }
+ });
+
+ Post.add(new Post({title: "foo"}));
+
+ var validPost = new Post({title: "bar"});
+ var invalidPost = new Post({title: "foo"});
+
+ ok(validPost.valid(), "should be valid with a unique title");
+ ok(!invalidPost.valid(), "should be invalid with a duplicate title");
+
+ Post.add(validPost)
+
+ ok(validPost.valid(), "should still be valid when added to the colleciton")
+});
+
+test("validatesUniquenessOf with options", function () {
+ var Post = Model("post", {
+ validates: {
+ uniquenessOf: {
+ 'title': {
+ message: "custom message",
+ condition: function () {
+ return this.attr('doValidation');
+ }
+ }
+ }
+ }
+ });
+
+ Post.add(new Post({title: "foo"}));
+
+ var invalidPost = new Post({title: "foo", doValidation: true});
+ var validPost = new Post({title: "foo", doValidation: false});
+
+ ok(!invalidPost.valid());
+ ok(validPost.valid(), "should only validate if the if condition is true");
+ equal(invalidPost.errors.errors.title[0], "custom message", "should use a custom validation message");
+});
+
+test("validatesFormatOf", function () {
+ var Post = Model("post", {
+ validates: {
+ formatOf: {
+ 'title': {
+ 'with': /^f/
+ }
+ }
+ }
+ });
+
+ var validPost = new Post({ title: "foo" });
+ var invalidPost = new Post({ title: "boo" });
+
+ ok(validPost.valid(), "should be valid when the regex matches the string");
+ ok(!invalidPost.valid(), "should be invalid when the regex doesn't match the string");
+})
+
+test("validatesInclusionOf", function () {
+ var Post = Model("post", {
+ validates: {
+ inclusionOf: {
+ 'tags': {
+ 'in': ['awesome', 'life changing']
+ }
+ }
+ }
+ });
+
+ var validPost = new Post({ tags: 'awesome' });
+ var invalidPost = new Post({ tags: 'boring' });
+
+ ok(validPost.valid(), "should be valid when the attribute value is in the supplied list");
+ ok(!invalidPost.valid(), "should be invalid when the attribute value is not in the supplied list");
+})
+
+test("validatesExclusionOf", function () {
+ var Post = Model("post", {
+ validates: {
+ exclusionOf: {
+ 'name': {
+ 'in': ['bob', 'tom']
+ }
+ }
+ }
+ });
+
+ var validPost = new Post({ name: 'xavier' });
+ var invalidPost = new Post({ name: 'bob' });
+
+ ok(validPost.valid(), "should be valid when the attribute value is not in the supplied list");
+ ok(!invalidPost.valid(), "should be invalid when the attribute value is in the supplied list");
+})
View
1  test/views/index.erb
@@ -24,6 +24,7 @@
<script src="tests/model_rest.js"></script>
<script src="tests/model_uid.js"></script>
<script src="tests/model_local_storage.js"></script>
+<script src="tests/model_validations.js"></script>
</head>
<body>
<h1 id="qunit-header">js-model Tests</h1>
Something went wrong with that request. Please try again.