diff --git a/Gemfile b/Gemfile index bb20fcf..4cf6029 100644 --- a/Gemfile +++ b/Gemfile @@ -6,3 +6,5 @@ gem 'highlight', :require => 'simplabs/highlight' gem 'nokogiri' gem 'rdiscount' gem 'sinatra' +gem 'json' +gem 'pygmentize' diff --git a/Gemfile.lock b/Gemfile.lock index ec717ee..6f29ad4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,9 @@ GEM rack highlight (1.1.2) activesupport (>= 2.0.0) + json (1.5.1) nokogiri (1.4.4) + pygmentize (0.0.2) rack (1.2.1) rdiscount (1.6.8) sinatra (1.1.3) @@ -22,6 +24,8 @@ DEPENDENCIES closure-compiler fewer (~> 0.2.0) highlight + json nokogiri + pygmentize rdiscount sinatra diff --git a/lib/bundler.rb b/lib/bundler.rb index 4aec372..38d4eba 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -41,6 +41,7 @@ def files model_utils model_version model_base + plugin_coerce ) end diff --git a/src/model.js b/src/model.js index 112cdf6..ea67654 100644 --- a/src/model.js +++ b/src/model.js @@ -1,7 +1,7 @@ var Model = function(name, func) { // The model constructor. var model = function(attributes) { - this.attributes = Model.Utils.extend({}, attributes) + this.attributes = this.setAttributeValues(Model.Utils.extend({}, attributes)) this.changes = {}; this.errors = new Model.Errors(this); this.uid = [name, Model.UID.generate()].join("-") diff --git a/src/model_instance_methods.js b/src/model_instance_methods.js index a1f30e7..ccef20c 100644 --- a/src/model_instance_methods.js +++ b/src/model_instance_methods.js @@ -3,6 +3,14 @@ Model.InstanceMethods = { return this.attr() }, + setAttributeValue: function(name, value) { + return value + }, + + setAttributeValues: function(attributes) { + return attributes + }, + attr: function(name, value) { if (arguments.length === 0) { // Combined attributes/changes object. @@ -13,7 +21,7 @@ Model.InstanceMethods = { // Clean up any stale changes. delete this.changes[name]; } else { - this.changes[name] = value; + this.changes[name] = this.setAttributeValue(name, value) } return this; } else if (typeof name === "object") { diff --git a/src/plugin_coerce.js b/src/plugin_coerce.js new file mode 100644 index 0000000..b6c18a6 --- /dev/null +++ b/src/plugin_coerce.js @@ -0,0 +1,91 @@ +if (!(typeof Plugin === 'function')) { + Plugin = function() { + } +} + +Plugin.Coerce = (function() { + + var plugin = function (klass, aTypes) { + var attributeTypes = aTypes; + + var augmentSetAttributeValue = function(orig_fn) { + return function() { + var name = arguments[0] + var value = arguments[1] + return coerceAttribute(name, orig_fn.call(this, name, value)) + } + } + + var augmentSetAttributeValues = function(orig_fn) { + return function() { + return coerceAttributes(orig_fn.apply(this, arguments)) + } + } + + function coerceAttribute(name, value) { + if (!(attributeTypes === undefined || attributeTypes[name] === undefined)) { + return plugin[attributeTypes[name]](value); + } else { + return value + } + } + + function coerceAttributes(attributes) { + for (var name in attributes) { + attributes[name] = coerceAttribute(name, attributes[name]) + } + return attributes + } + + klass.prototype.setAttributeValue = augmentSetAttributeValue(klass.prototype.setAttributeValue) + klass.prototype.setAttributeValues = augmentSetAttributeValues(klass.prototype.setAttributeValues) + } + + plugin.integer = function(value) { + return parseInt(value) + } + + plugin['boolean'] = function(value) { + return (!(value === false || value === "false" || + value === 0 || value === "0")); + } + + plugin['float'] = function(value) { + return parseFloat(value) + } + + plugin.isoDate = function(value) { + //http://zetafleet.com/blog/javascript-dateparse-for-iso-8601 + /** + * Date.parse with progressive enhancement for ISO-8601, version 2 + * © 2010 Colin Snover + * Released under MIT license. + */ + (function () { + var origParse = Date.parse; + Date.parse = function (date) { + var timestamp = origParse(date), minutesOffset = 0, struct; + if (isNaN(timestamp) && (struct = /^(\d{4}|[+\-]\d{6})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{3,}))?)?(?:(Z)|([+\-])(\d{2})(?::?(\d{2}))?))?/.exec(date))) { + if (struct[8] !== 'Z') { + minutesOffset = +struct[10] * 60 + (+struct[11]); + + if (struct[9] === '+') { + minutesOffset = 0 - minutesOffset; + } + } + + timestamp = Date.UTC(+struct[1], +struct[2] - 1, +struct[3], +struct[4], +struct[5] + minutesOffset, +struct[6], +struct[7].substr(0, 3)); + } + + return timestamp; + }; + }()); + + return new Date(Date.parse(value)) + } + + return plugin + +})(); + + diff --git a/test/tests/model_rest_test.js b/test/tests/model_rest_test.js index c7cb109..d1a122e 100644 --- a/test/tests/model_rest_test.js +++ b/test/tests/model_rest_test.js @@ -52,6 +52,7 @@ test("create with named params in resource path", function() { equal(request.url, "/root/3/nested/2/posts"); }); + test("update with named params in resource path", function() { var Post = Model("post", function() { this.persistence(Model.REST, "/root/:root_id/nested/:nested_id/posts") diff --git a/test/tests/plugin_coerce_test.js b/test/tests/plugin_coerce_test.js new file mode 100644 index 0000000..2a493dd --- /dev/null +++ b/test/tests/plugin_coerce_test.js @@ -0,0 +1,139 @@ +module("Plugin.Coerce") + +test("Integer coercion", function() { + equals(Plugin.Coerce.integer("1"), 1) + equals(Plugin.Coerce.integer("1"), parseInt("1")) + equals(Plugin.Coerce.integer("4.5"), parseInt("4")) + equals(Plugin.Coerce.integer("4.5"), parseInt("4")) + same(Plugin.Coerce.integer("abc"), NaN) +}); + +test("Boolean coercion", function() { + equals(Plugin.Coerce.boolean("1"), true) + equals(Plugin.Coerce.boolean("0"), false) + equals(Plugin.Coerce.boolean("true"), true) + equals(Plugin.Coerce.boolean("false"), false) + equals(Plugin.Coerce.boolean(true), true) + equals(Plugin.Coerce.boolean(false), false) +}); + +test("Float coercion", function() { + equals(Plugin.Coerce.float("1.23"), 1.23) + equals(Plugin.Coerce.float("1"), 1.0) + same(Plugin.Coerce.float("abc"), NaN) +}); + +test("IsoDate coercion", function() { + equals(Plugin.Coerce.isoDate("2010-08-21T02:03:44Z"), "Sat Aug 21 2010 04:03:44 GMT+0200 (CEST)") + equals(Plugin.Coerce.isoDate("test"), "Invalid Date") + ok(Plugin.Coerce.isoDate("2011-12-31T00:00:00Z") instanceof Date) + equals(Plugin.Coerce.isoDate("2011-12-31T00:00:00Z"), "Sat Dec 31 2011 01:00:00 GMT+0100 (CET)") +}); + + +test("attribute types with coercion", function() { + var Post = Model("post", function() { + this.use(Plugin.Coerce, + { + category_id: "integer", + lat: "float", + published: "boolean", + pubDate: "isoDate" + }) + }) + + var post = new Post({ + category_id: "1.4", + lat: "12345.6789", + published: "false", + pubDate: "2010-08-21T02:03:44Z" + }) + + equals(post.attr("category_id"), 1) + equals(post.attr("lat"), 12345.6789) + equals(post.attr("published"), false) + equals(post.attr("pubDate"), "Sat Aug 21 2010 04:03:44 GMT+0200 (CEST)") + + post.attr("published", true) + equals(post.attr("published"), true) + + post.attr("pubDate", "2011-12-31T00:00:00Z") + ok(post.attributes.pubDate instanceof Date, "Should be instance of date!") + equals(post.attr("pubDate"), "Sat Dec 31 2011 01:00:00 GMT+0100 (CET)") + + post.attr({pubDate: "2010-01-01T00:00:0Z", category_id: 2, lat: 1.2, published: true}) + equals(post.attr("category_id"), 2) + equals(post.attr("lat"), 1.2) + equals(post.attr("published"), true) + equals(post.attr("pubDate"), "Fri Jan 01 2010 01:00:00 GMT+0100 (CET)") +}); + + +test("create types with coercion should stringify types correctly", function() { + var Post = Model("post", function() { + this.persistence(Model.REST, "/posts"), + this.use(Plugin.Coerce, + { + category_id: "integer", + lat: "float", + published: "boolean", + pubDate: "isoDate" + }) + }) + + var post = new Post({ + category_id: "1.4", + lat: "12345.6789", + published: "false", + pubDate: "2010-08-21T02:03:44Z" + }) + + AjaxSpy.start(); + stop(); + + post.save(function(success) { + ok(success); + start(); + }); + + equals(AjaxSpy.requests.length, 1, "one request should have been made"); + + var request = AjaxSpy.requests.shift(); + + equals(request.type, "POST"); + equals(request.url, "/posts"); + equals(request.data, "{\"post\":{\"category_id\":1,\"lat\":12345.6789,\"published\":false,\"pubDate\":\"2010-08-21T02:03:44.000Z\"}}") + +}); + +test("Custom coercions", function() { + Plugin.Coerce.uppercase = function(value) { + return value.toUpperCase() + } + + var Post = Model("post", function() { + this.use(Plugin.Coerce, + { + title: "uppercase", + category_id: "integer", + lat: "float", + published: "boolean", + pubDate: "isoDate" + }) + }) + + var post = new Post({ + title: "title", + category_id: "1.4", + lat: "12345.6789", + published: "false", + pubDate: "2010-08-21T02:03:44Z" + }) + + equals(post.attr("title"), "TITLE") + equals(post.attr("category_id"), 1) + equals(post.attr("lat"), 12345.6789) + equals(post.attr("published"), false) + equals(post.attr("pubDate"), "Sat Aug 21 2010 04:03:44 GMT+0200 (CEST)") + +}); diff --git a/test/views/index.erb b/test/views/index.erb index a614669..3a4c679 100644 --- a/test/views/index.erb +++ b/test/views/index.erb @@ -29,6 +29,7 @@ +

js-model Tests