Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validations #13

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/bundler.rb
Expand Up @@ -37,6 +37,7 @@ def files
model_log
model_rest
model_uid
model_validations
model_version
)
end
Expand Down
11 changes: 9 additions & 2 deletions src/model.js
Expand Up @@ -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.
Expand All @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/model_class_methods.js
Expand Up @@ -24,6 +24,10 @@ Model.ClassMethods = {
return this.collection;
},

any: function () {
return this.count() > 0;
},

count: function() {
return this.collection.length;
},
Expand Down
3 changes: 3 additions & 0 deletions src/model_instance_methods.js
Expand Up @@ -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;
},

Expand Down
123 changes: 123 additions & 0 deletions 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
};
};
6 changes: 5 additions & 1 deletion test/tests/model_class_methods.js
Expand Up @@ -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) {
Expand Down
195 changes: 195 additions & 0 deletions 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");
})