function Route(template, defaults) { this.template = template = template + '#'; this.defaults = defaults || {}; var urlParams = this.urlParams = {}; foreach(template.split(/\W/), function(param){ if (param && template.match(new RegExp(":" + param + "\\W"))) { urlParams[param] = true; } }); } Route.prototype = { url: function(params) { var path = []; var self = this; var url = this.template; params = params || {}; foreach(this.urlParams, function(_, urlParam){ var value = params[urlParam] || self.defaults[urlParam] || ""; url = url.replace(new RegExp(":" + urlParam + "(\\W)"), value + "$1"); }); url = url.replace(/\/?#$/, ''); var query = []; foreachSorted(params, function(value, key){ if (!self.urlParams[key]) { query.push(encodeURI(key) + '=' + encodeURI(value)); } }); url = url.replace(/\/*$/, ''); return url + (query.length ? '?' + query.join('&') : ''); } }; function ResourceFactory(xhr) { this.xhr = xhr; } ResourceFactory.DEFAULT_ACTIONS = { 'get': {method:'GET'}, 'save': {method:'POST'}, 'query': {method:'GET', isArray:true}, 'remove': {method:'DELETE'}, 'delete': {method:'DELETE'} }; ResourceFactory.prototype = { route: function(url, paramDefaults, actions){ var self = this; var route = new Route(url); actions = extend({}, ResourceFactory.DEFAULT_ACTIONS, actions); function extractParams(data){ var ids = {}; foreach(paramDefaults || {}, function(value, key){ ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; }); return ids; } function Resource(value){ copy(value || {}, this); } foreach(actions, function(action, name){ var isPostOrPut = action.method == 'POST' || action.method == 'PUT'; Resource[name] = function (a1, a2, a3) { var params = {}; var data; var callback = noop; switch(arguments.length) { case 3: callback = a3; case 2: if (isFunction(a2)) { callback = a2; } else { params = a1; data = a2; break; } case 1: if (isFunction(a1)) callback = a1; else if (isPostOrPut) data = a1; else params = a1; break; case 0: break; default: throw "Expected between 0-3 arguments [params, data, callback], got " + arguments.length + " arguments."; } var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); self.xhr( action.method, route.url(extend({}, action.params || {}, extractParams(data), params)), data, function(status, response, clear) { if (status == 200) { if (action.isArray) { value.length = 0; foreach(response, function(item){ value.push(new Resource(item)); }); } else { copy(response, value); } (callback||noop)(value); } else { throw {status: status, response:response, message: status + ": " + response}; } }, action.verifyCache); return value; }; Resource.bind = function(additionalParamDefaults){ return self.route(url, extend({}, paramDefaults, additionalParamDefaults), actions); }; Resource.prototype['$' + name] = function(a1, a2){ var params = extractParams(this); var callback = noop; switch(arguments.length) { case 2: params = a1; callback = a2; case 1: if (typeof a1 == $function) callback = a1; else params = a1; case 0: break; default: throw "Expected between 1-2 arguments [params, callback], got " + arguments.length + " arguments."; } var data = isPostOrPut ? this : _undefined; Resource[name].call(this, params, data, callback); }; }); return Resource; } };