From f0a24355491204c91b691e59c556bd5af4a408b9 Mon Sep 17 00:00:00 2001 From: Blake West Date: Sun, 18 Jan 2015 20:09:06 -0800 Subject: [PATCH] Added regex functionality to routes. - Also now require callback functions on custom routes, and require them to return a valid response. - Bumped bower version. - closes #8, #9, #10. --- README.md | 12 +++++--- bower.json | 2 +- dist/facade.js | 72 ++++++++++++++++++++++++++++++++-------------- dist/facade.min.js | 4 +-- src/facade.js | 72 ++++++++++++++++++++++++++++++++-------------- test/test.js | 43 ++++++++++++++++++++++++++- 6 files changed, 153 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index ef6f6b5..225094c 100644 --- a/README.md +++ b/README.md @@ -154,22 +154,26 @@ patientResource.addRoute({ method:"POST", route:"/verify", - // This callback is called everytime the route is run. It is not the response. - // Currently the response for custom routes is just the item, or collection, depending on // the route. - callback: function(data, item ) { + callback: function(data, item, headers) { item.verified = true; // If the onItem flag was false, this function would be passed the collection, // and not the item. + return [200, item, {}, 'OK']; }, // this flag adds the route for every item in the db. eg. '/patients/1/verify' onItem: true }); ``` **notes about the addRoute options hash** + `route`: Can be either a string or regex. If it's a regex, it can't be set to "onItem". + `onItem`: If this flag is omitted, or set to false, it will create the route on the collection. eg. '/patients/verify'; - `callback`: This is meant to let you "perform the action" of the route. Very similar to whatever your real backend might do for this route. The callback is passed the request data, and then the appropriate database object. Which is the item if it's an item route (eg. patients/3/verify), or the collection if it's a colleciton route (eg. patients/verify) + `callback`: This is **required** and is meant to let you "perform the action" of the route. Very similar to whatever your real backend might do for this route. It also returns the response of the route. The response must be in standard angular form which is + `[status, data, headers, status_text]`. + The callback is passed the request data, the appropriate database object (which is the item if it's an item route (eg. patients/3/verify), or the collection if it's a colleciton route (eg. patients/verify)). It's also passed the request headers. + **Creating Expectations!** Facade provides convenience around HTTP expectations. Specifically, you can do, diff --git a/bower.json b/bower.json index b91694b..f9c99bf 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "facade", - "version": "0.3.0", + "version": "0.4.0", "authors": [ "Blake West " ], diff --git a/dist/facade.js b/dist/facade.js index 224a947..c38e7fd 100644 --- a/dist/facade.js +++ b/dist/facade.js @@ -80,7 +80,7 @@ var Y=s();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(G. this.db[opts.name] = buildTable(opts.name); // Create a slot for the resources routes in the master route list; - facadeRoutes[opts.name] = []; + facadeRoutes[opts.name] = {}; // Add resource to master list return this.resources[opts.name] = buildResource(opts); @@ -124,12 +124,20 @@ var Y=s();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(G. var routeObj; var fullRoute = [method, url].join(' '); var exists = _.any(facadeRoutes, function(resourceRoutes) { - return Boolean(resourceRoutes[fullRoute]) && (routeObj = resourceRoutes[fullRoute]); + routeObj = resourceRoutes[fullRoute]; + if (routeObj) { + return routeObj; + } + return routeObj = _.chain(resourceRoutes) + .filter('regExp') + .where({method: method}) + .find(function(route) { + return route.regExp.test(url); + }).value(); }); if (!exists) { throw new Error("The route " + fullRoute + " does not exist"); } - return routeObj; } @@ -281,37 +289,33 @@ var Y=s();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(G. function createCustomRouteForItem(opts) { + throwIfRegex(opts.route); var fullUrl = opts.resource.url + '/' + opts.item.id + opts.route; Facade.backend.when(opts.method, fullUrl).respond(function(method, url, requestData, headers) { requestData = requestData || {}; var route = Facade.findRoute(method, url); - - if (!route.hasSpecialResponse()) { - _.isFunction(opts.callback) && opts.callback(requestData, opts.item); - } + var item = getTable(opts.resource).find(opts.item.id); var response = route.getSpecialResponseOr(function() { - return [200, JSON.stringify(opts.item), {}, 'OK']; + return opts.callback(requestData, item, headers); }); + checkForValidResponse(response); return response; }); } function createCustomRouteForCollection(opts) { - var fullUrl = opts.resource.url + opts.route; + var fullUrl = _.isRegExp(opts.route) ? opts.route : opts.resource.url + opts.route Facade.backend.when(opts.method, fullUrl).respond(function(method, url, requestData, headers) { requestData = requestData || {}; var collection = getTable(opts.resource).getAll(); var route = Facade.findRoute(method, url); - if (!route.hasSpecialResponse()) { - _.isFunction(opts.callback) && opts.callback(requestData, collection); - } - var response = route.getSpecialResponseOr(function() { - return [200, JSON.stringify(collection), {}, 'OK']; + return opts.callback(requestData, collection); }); + checkForValidResponse(response); return response; }); @@ -339,10 +343,16 @@ var Y=s();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(G. function storeRoute(opts) { opts.route = opts.route || ''; - var prefix = opts.resource.url; - var fullUrl = opts.item ? prefix + '/' + opts.item.id + opts.route : prefix + opts.route; - var fullRoute = [opts.method, fullUrl].join(' ') - facadeRoutes[opts.resource.name][fullRoute] = buildRoute(fullRoute); + if (_.isRegExp(opts.route)) { + opts.regExp = opts.route; + }else { + var prefix = opts.resource.url; + var fullUrl = opts.item ? prefix + '/' + opts.item.id + opts.route : prefix + opts.route; + var fullRoute = [opts.method, fullUrl].join(' ') + + opts.fullRoute = fullRoute; + } + facadeRoutes[opts.resource.name][(opts.regExp || opts.fullRoute)] = buildRoute(opts); } function storeRouteOpts(opts) { @@ -437,10 +447,12 @@ var Y=s();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(G. }; } - function buildRoute(fullRoute) { + function buildRoute(opts) { var specialResponses = []; return { - fullRoute: fullRoute, + fullRoute: opts.fullRoute, + regExp: opts.regExp, + method: opts.method, nextResponse: function(status, data) { specialResponses.push({status: status, data: data}); }, @@ -593,11 +605,27 @@ var Y=s();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(G. } } + function throwIfRegex(route) { + if (_.isRegExp(route)) { + throw new Error("Regex routes can't be used for item routes. Either make 'onItem' false" + + " or make the route a string") + } + } + function checkForRequiredRouteArgs(opts) { checkForValidMethod(opts.method); + if (!_.isString(opts.route) && !_.isRegExp(opts.route)) { + throw new Error("You must supply a route (eg: '/my_route') as either a string or regex"); + } + if (!opts.callback) { + throw new Error("You must supply a response callback for custom routes."); + } + } - if (!_.isString(opts.route)) { - throw new Error("You must supply a route. eg: '/my_route' "); + function checkForValidResponse(response) { + if (!_.isArray(response)) { throw new Error("Response must be an array");} + if (response.length !== 4) { + throw new Error("Response does not appear to be in the form of [status, data, headers, status_text]" ) } } diff --git a/dist/facade.min.js b/dist/facade.min.js index cdcca07..2ce1b82 100644 --- a/dist/facade.min.js +++ b/dist/facade.min.js @@ -1,2 +1,2 @@ -(function(){function n(n,e,t){t=(t||0)-1;for(var r=n?n.length:0;++ta||"undefined"==typeof i)return 1;if(a>i||"undefined"==typeof a)return-1}}return n.n-e.n}function o(n){var e=-1,r=n.length,u=n[0],o=n[r/2|0],i=n[r-1];if(u&&"object"==typeof u&&o&&"object"==typeof o&&i&&"object"==typeof i)return!1;for(u=c(),u["false"]=u["null"]=u["true"]=u.undefined=!1,o=c(),o.k=n,o.l=u,o.push=t;++et?0:t);++r3&&"function"==typeof i[c-2])var f=ee(i[--c-1],i[c--],2);else c>2&&"function"==typeof i[c-1]&&(f=i[--c]);for(;++a=y&&i===n,f=[];if(c){var s=o(r);s?(i=e,r=s):c=!1}for(;++ui(r,s)&&f.push(s);return c&&l(r),f}function ue(n,e,t,r){r=(r||0)-1;for(var u=n?n.length:0,o=[];++r=y&&c===n,d=u||h?a():p;for(h&&(d=o(d),c=e);++ic(d,g))&&((u||h)&&d.push(g),p.push(v))}return h?(f(d.k),l(d)):u&&f(d),p}function fe(n){return function(e,t,r){var u={};t=M.createCallback(t,r,3),r=-1;var o=e?e.length:0;if("number"==typeof o)for(;++rt?xt(0,o+t):t)||0,Dt(n)?i=-1o&&(o=a)}}else e=null==e&&Oe(n)?r:M.createCallback(e,t,3),Re(n,function(n,t,r){t=e(n,t,r),t>u&&(u=t,o=n)});return o}function Ae(n,e,t,r){if(!n)return t;var u=3>arguments.length;e=M.createCallback(e,r,4);var o=-1,i=n.length;if("number"==typeof i)for(u&&(t=n[++o]);++oarguments.length;return e=M.createCallback(e,r,4),Te(n,function(n,r,o){t=u?(u=!1,n):e(t,n,r,o)}),t}function De(n){var e=-1,t=n?n.length:0,r=Qe("number"==typeof t?t:0);return Re(n,function(n){var t=ae(0,++e);r[e]=r[t],r[t]=n}),r}function Je(n,e,t){var r;e=M.createCallback(e,t,3),t=-1;var u=n?n.length:0;if("number"==typeof u)for(;++tr?xt(0,u+r):r||0}else if(r)return r=We(e,t),e[r]===t?r:-1;return n(e,t,r)}function Be(n,e,t){if("number"!=typeof e&&null!=e){var r=0,u=-1,o=n?n.length:0;for(e=M.createCallback(e,t,3);++uu;)r=u+o>>>1,t(n[r])t?0:t);++e0?l=bt(u,t):(i&&ht(i),t=s,i=l=s=h,t&&(p=Ut(),a=n.apply(f,o),l||i||(o=f=null)))}var o,i,a,c,f,l,s,p=0,d=!1,v=!0;if(!_e(n))throw new it;if(e=xt(0,e)||0,!0===t)var g=!0,v=!1;else we(t)&&(g=t.leading,d="maxWait"in t&&(xt(e,t.maxWait)||0),v="trailing"in t?t.trailing:v);return function(){if(o=arguments,c=Ut(),f=this,s=v&&(l||!g),!1===d)var t=g&&!l;else{i||g||(p=c);var h=d-(c-p),m=0>=h;m?(i&&(i=ht(i)),p=c,a=n.apply(f,o)):i||(i=bt(r,h))}return m&&l?l=ht(l):l||e===d||(l=bt(u,e)),t&&(m=!0,a=n.apply(f,o)),!m||l||i||(o=f=null),a}}function Ue(n){return n}function He(n,e,t){var r=!0,u=e&&ye(e);e&&(t||u.length)||(null==t&&(t=e),o=V,e=n,n=M,u=ye(e)),!1===t?r=!1:we(t)&&"chain"in t&&(r=t.chain);var o=n,i=_e(o);Re(u,function(t){var u=n[t]=e[t];i&&(o.prototype[t]=function(){var e=this.__chain__,t=this.__wrapped__,i=[t];if(yt.apply(i,arguments),i=u.apply(n,i),r||e){if(t===i&&we(i))return this;i=new o(i),i.__chain__=e}return i})})}function Ye(){}function Me(n){return function(e){return e[n]}}function Ve(){return this.__wrapped__}t=t?X.defaults(H.Object(),t,X.pick(H,N)):H;var Qe=t.Array,Xe=t.Boolean,Ze=t.Date,nt=t.Function,et=t.Math,tt=t.Number,rt=t.Object,ut=t.RegExp,ot=t.String,it=t.TypeError,at=[],ct=rt.prototype,ft=t._,lt=ct.toString,st=ut("^"+ot(lt).replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/toString| for [^\]]+/g,".*?")+"$"),pt=et.ceil,ht=t.clearTimeout,dt=et.floor,vt=nt.prototype.toString,gt=he(gt=rt.getPrototypeOf)&>,mt=ct.hasOwnProperty,yt=at.push,bt=t.setTimeout,_t=at.splice,wt=at.unshift,kt=function(){try{var n={},e=he(e=rt.defineProperty)&&e,t=e(n,n,n)&&e}catch(r){}return t}(),Ot=he(Ot=rt.create)&&Ot,Ft=he(Ft=Qe.isArray)&&Ft,St=t.isFinite,jt=t.isNaN,Et=he(Et=rt.keys)&&Et,xt=et.max,Rt=et.min,Tt=t.parseInt,Ct=et.random,Nt={};Nt[I]=Qe,Nt[D]=Xe,Nt[J]=Ze,Nt[P]=nt,Nt[B]=rt,Nt[K]=tt,Nt[W]=ut,Nt[$]=ot,V.prototype=M.prototype;var At=M.support={};At.funcDecomp=!he(t.a)&&T.test(p),At.funcNames="string"==typeof nt.name,M.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:E,variable:"",imports:{_:M}},Ot||(ne=function(){function n(){}return function(e){if(we(e)){n.prototype=e;var r=new n;n.prototype=null}return r||t.Object()}}());var It=kt?function(n,e){G.value=e,kt(n,"__bindData__",G)}:Ye,Dt=Ft||function(n){return n&&"object"==typeof n&&"number"==typeof n.length&<.call(n)==I||!1},Jt=Et?function(n){return we(n)?Et(n):[]}:Y,Pt={"&":"&","<":"<",">":">",'"':""","'":"'"},Kt=be(Pt),Bt=ut("("+Jt(Kt).join("|")+")","g"),Wt=ut("["+Jt(Pt).join("")+"]","g"),$t=gt?function(n){if(!n||lt.call(n)!=B)return!1;var e=n.valueOf,t=he(e)&&(t=gt(e))&>(t);return t?n==t||gt(n)==t:de(n)}:de,zt=fe(function(n,e,t){mt.call(n,t)?n[t]++:n[t]=1}),qt=fe(function(n,e,t){(mt.call(n,t)?n[t]:n[t]=[]).push(e)}),Gt=fe(function(n,e,t){n[t]=e}),Lt=Ce,Ut=he(Ut=Ze.now)&&Ut||function(){return(new Ze).getTime()},Ht=8==Tt(_+"08")?Tt:function(n,e){return Tt(Oe(n)?n.replace(x,""):n,e||0)};return M.after=function(n,e){if(!_e(e))throw new it;return function(){return 1>--n?e.apply(this,arguments):void 0}},M.assign=U,M.at=function(n){for(var e=arguments,t=-1,r=ue(e,!0,!1,1),e=e[2]&&e[2][e[1]]===n?1:r.length,u=Qe(e);++t=y&&o(r?t[r]:p)))}var s=t[0],d=-1,v=s?s.length:0,g=[];n:for(;++d(m?e(m,h):c(p,h))){for(r=u,(m||p).push(h);--r;)if(m=i[r],0>(m?e(m,h):c(t[r],h)))continue n;g.push(h)}}for(;u--;)(m=i[u])&&l(m);return f(i),f(p),g},M.invert=be,M.invoke=function(n,e){var t=s(arguments,2),r=-1,u="function"==typeof e,o=n?n.length:0,i=Qe("number"==typeof o?o:0);return Re(n,function(n){i[++r]=(u?e:n[e]).apply(n,t)}),i},M.keys=Jt,M.map=Ce,M.mapValues=function(n,e,t){var r={};return e=M.createCallback(e,t,3),d(n,function(n,t,u){r[t]=e(n,t,u)}),r},M.max=Ne,M.memoize=function(n,e){function t(){var r=t.cache,u=e?e.apply(this,arguments):m+arguments[0];return mt.call(r,u)?r[u]:r[u]=n.apply(this,arguments)}if(!_e(n))throw new it;return t.cache={},t},M.merge=function(n){var e=arguments,t=2;if(!we(n))return n;if("number"!=typeof e[2]&&(t=e.length),t>3&&"function"==typeof e[t-2])var r=ee(e[--t-1],e[t--],2);else t>2&&"function"==typeof e[t-1]&&(r=e[--t]);for(var e=s(arguments,1,t),u=-1,o=a(),i=a();++ua&&(o=a)}}else e=null==e&&Oe(n)?r:M.createCallback(e,t,3),Re(n,function(n,t,r){t=e(n,t,r),u>t&&(u=t,o=n)});return o},M.omit=function(n,e,t){var r={};if("function"!=typeof e){var u=[];v(n,function(n,e){u.push(e)});for(var u=re(u,ue(arguments,!0,!1,1)),o=-1,i=u.length;++ot?xt(0,r+t):Rt(t,r-1))+1);r--;)if(n[r]===e)return r;return-1},M.mixin=He,M.noConflict=function(){return t._=ft,this},M.noop=Ye,M.now=Ut,M.parseInt=Ht,M.random=function(n,e,t){var r=null==n,u=null==e;return null==t&&("boolean"==typeof n&&u?(t=n,n=1):u||"boolean"!=typeof e||(t=e,u=!0)),r&&u&&(e=1),n=+n||0,u?(e=n,n=0):e=+e||0,t||n%1||e%1?(t=Ct(),Rt(n+t*(e-n+parseFloat("1e-"+((t+"").length-1))),e)):ae(n,e)},M.reduce=Ae,M.reduceRight=Ie,M.result=function(n,e){if(n){var t=n[e];return _e(t)?n[e]():t}},M.runInContext=p,M.size=function(n){var e=n?n.length:0;return"number"==typeof e?e:Jt(n).length},M.some=Je,M.sortedIndex=We,M.template=function(n,e,t){var r=M.templateSettings;n=ot(n||""),t=b({},t,r);var u,o=b({},t.imports,r.imports),r=Jt(o),o=Fe(o),a=0,c=t.interpolate||R,f="__p+='",c=ut((t.escape||R).source+"|"+c.source+"|"+(c===E?F:R).source+"|"+(t.evaluate||R).source+"|$","g");n.replace(c,function(e,t,r,o,c,l){return r||(r=o),f+=n.slice(a,l).replace(C,i),t&&(f+="'+__e("+t+")+'"),c&&(u=!0,f+="';"+c+";\n__p+='"),r&&(f+="'+((__t=("+r+"))==null?'':__t)+'"),a=l+e.length,e}),f+="';",c=t=t.variable,c||(t="obj",f="with("+t+"){"+f+"}"),f=(u?f.replace(w,""):f).replace(k,"$1").replace(O,"$1;"),f="function("+t+"){"+(c?"":t+"||("+t+"={});")+"var __t,__p='',__e=_.escape"+(u?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+f+"return __p}";try{var l=nt(r,"return "+f).apply(h,o)}catch(s){throw s.source=f,s}return e?l(e):(l.source=f,l)},M.unescape=function(n){return null==n?"":ot(n).replace(Bt,ve)},M.uniqueId=function(n){var e=++g;return ot(null==n?"":n)+e},M.all=je,M.any=Je,M.detect=xe,M.findWhere=xe,M.foldl=Ae,M.foldr=Ie,M.include=Se,M.inject=Ae,He(function(){var n={};return d(M,function(e,t){M.prototype[t]||(n[t]=e)}),n}(),!1),M.first=Pe,M.last=function(n,e,t){var r=0,u=n?n.length:0;if("number"!=typeof e&&null!=e){var o=u;for(e=M.createCallback(e,t,3);o--&&e(n[o],o,n);)r++}else if(r=e,null==r||t)return n?n[u-1]:h;return s(n,xt(0,u-r))},M.sample=function(n,e,t){return n&&"number"!=typeof n.length&&(n=Fe(n)),null==e||t?n?n[ae(0,n.length-1)]:h:(n=De(n),n.length=Rt(xt(0,e),n.length),n)},M.take=Pe,M.head=Pe,d(M,function(n,e){var t="sample"!==e;M.prototype[e]||(M.prototype[e]=function(e,r){var u=this.__chain__,o=n(this.__wrapped__,e,r);return u||null!=e&&(!r||t&&"function"==typeof e)?new V(o,u):o})}),M.VERSION="2.4.1",M.prototype.chain=function(){return this.__chain__=!0,this},M.prototype.toString=function(){return ot(this.__wrapped__)},M.prototype.value=Ve,M.prototype.valueOf=Ve,Re(["join","pop","shift"],function(n){var e=at[n];M.prototype[n]=function(){var n=this.__chain__,t=e.apply(this.__wrapped__,arguments);return n?new V(t,n):t}}),Re(["push","reverse","sort","unshift"],function(n){var e=at[n];M.prototype[n]=function(){return e.apply(this.__wrapped__,arguments),this}}),Re(["concat","slice","splice"],function(n){var e=at[n];M.prototype[n]=function(){return new V(e.apply(this.__wrapped__,arguments),this.__chain__)}}),M}var h,d=[],v=[],g=0,m=+new Date+"",y=75,b=40,_=" \f \n\r\u2028\u2029 ᠎              ",w=/\b__p\+='';/g,k=/\b(__p\+=)''\+/g,O=/(__e\(.*?\)|\b__t\))\+'';/g,F=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,S=/\w*$/,j=/^\s*function[ \n\r\t]+\w/,E=/<%=([\s\S]+?)%>/g,x=RegExp("^["+_+"]*0+(?=.$)"),R=/($^)/,T=/\bthis\b/,C=/['\n\r\t\u2028\u2029\\]/g,N="Array Boolean Date Function Math Number Object RegExp String _ attachEvent clearTimeout isFinite isNaN parseInt setTimeout".split(" "),A="[object Arguments]",I="[object Array]",D="[object Boolean]",J="[object Date]",P="[object Function]",K="[object Number]",B="[object Object]",W="[object RegExp]",$="[object String]",z={};z[P]=!1,z[A]=z[I]=z[D]=z[J]=z[K]=z[B]=z[W]=z[$]=!0;var q={leading:!1,maxWait:0,trailing:!1},G={configurable:!1,enumerable:!1,value:null,writable:!1},L={"boolean":!1,"function":!0,object:!0,number:!1,string:!1,undefined:!1},U={"\\":"\\","'":"'","\n":"n","\r":"r"," ":"t","\u2028":"u2028","\u2029":"u2029"},H=L[typeof window]&&window||this,Y=L[typeof exports]&&exports&&!exports.nodeType&&exports,M=L[typeof module]&&module&&!module.nodeType&&module,V=M&&M.exports===Y&&Y,Q=L[typeof global]&&global;!Q||Q.global!==Q&&Q.window!==Q||(H=Q);var X=p();"function"==typeof define&&"object"==typeof define.amd&&define.amd?(H._=X,define(function(){return X})):Y&&M?V?(M.exports=X)._=X:Y._=X:H._=X}).call(this),function(){"use strict";function n(){P=_.clone(Facade.resources,!0),K=_.clone(Facade.db,!0),B=_.clone(J,!0)}function e(n){t(n),r(n)}function t(n){_.each(s(),function(e){var t={resource:n,method:e.method};e.createWith(t),v(t)})}function r(n){var e=O(n);_.each(e,function(e){u(n,e)})}function u(n,e){_.each(l(),function(t){var r={resource:n,item:e,method:t.method};t.createWith(r),v(r)}),_.each(W,function(n){n.item=e,p(n),v(n)})}function o(n){Facade.backend.whenGET(n.resource.url+"/"+n.item.id).respond(function(e,t){var r=Facade.findRoute(e,t),u=r.getSpecialResponseOr(function(){var e=F(n.resource,n.item.id);return[200,JSON.stringify(e),{},"OK"]});return u})}function i(n){Facade.backend.whenPUT(n.resource.url+"/"+n.item.id).respond(function(e,t,r,u){r=r||{};var o=Facade.findRoute(e,t),i=o.getSpecialResponseOr(function(){var e=F(n.resource,n.item.id);return _.assign(e,JSON.parse(r)),[200,JSON.stringify(e),u,"OK"]});return i})}function a(n){Facade.backend.whenDELETE(n.resource.url+"/"+n.item.id).respond(function(e,t,r){r=r||{};var u=Facade.findRoute(e,t),o=u.getSpecialResponseOr(function(){var e=k(n.resource).delete(n.item.id);return[200,JSON.stringify(e),{},"OK"]});return o})}function c(n){Facade.backend.whenPOST(n.resource.url).respond(function(e,t,r){r=r||{},r=JSON.parse(r);var u=Facade.findRoute(e,t),o=u.getSpecialResponseOr(function(){var e=_.isFunction(n.resource.createDefault)&&n.resource.createDefault(r);return e=e||r,n.resource.addItem(e),[200,JSON.stringify(e),{},"OK"]});return o})}function f(n){Facade.backend.whenGET(n.resource.url).respond(function(e,t){var r=Facade.findRoute(e,t),u=r.getSpecialResponseOr(function(){return[200,O(n.resource),{},"OK"]});return u})}function l(){return[{createWith:o,method:"GET"},{createWith:i,method:"PUT"},{createWith:a,method:"DELETE"}]}function s(){return[{createWith:c,method:"POST"},{createWith:f,method:"GET"}]}function p(n){var e=n.resource.url+"/"+n.item.id+n.route;Facade.backend.when(n.method,e).respond(function(e,t,r){r=r||{};var u=Facade.findRoute(e,t);u.hasSpecialResponse()||_.isFunction(n.callback)&&n.callback(r,n.item);var o=u.getSpecialResponseOr(function(){return[200,JSON.stringify(n.item),{},"OK"]});return o})}function h(n){var e=n.resource.url+n.route;Facade.backend.when(n.method,e).respond(function(e,t,r){r=r||{};var u=k(n.resource).getAll(),o=Facade.findRoute(e,t);o.hasSpecialResponse()||_.isFunction(n.callback)&&n.callback(r,u);var i=o.getSpecialResponseOr(function(){return[200,JSON.stringify(u),{},"OK"]});return i})}function d(n){var e=n.resource.url+n.route;Facade.backend.expect(n.method,e,m(n.expected)).respond(function(e,t,r){r=r||{};var u=k(n.resource).getAll(),o=Facade.findRoute(e,t);o.hasSpecialResponse()||_.isFunction(n.callback)&&n.callback(r,u);var i=o.getSpecialResponseOr(function(){return[200,JSON.stringify(u),{},"OK"]});return i})}function v(n){n.route=n.route||"";var e=n.resource.url,t=n.item?e+"/"+n.item.id+n.route:e+n.route,r=[n.method,t].join(" ");J[n.resource.name][r]=b(r)}function g(n){W.push(n)}function m(n){function e(t,r,u){if(t[u])return t[u]===r?!0:(console.log("Expected",u,"to equal",r,"in",t,"but it was",t[u]),!1);var o=_.filter(t,function(n){return _.isObject(n)}),i=t[u]===val;return o||i?_.any(o,function(n){return e(n,r,u)}):(console.log("Missing expectedKey",u,"in",nestedData,"should include",n),!1)}return function(t){var r=JSON.parse(t);return r?_.all(n,function(n,t){return e(r,n,t)}):(console.log("Unable to parse to JSON:",t),!1)}}function y(n){return n=n||{},n.url=this&&this.url?this.url+n.url:n.url,{url:n.url,name:n.name,addItem:function(n){return x(n),k(this).create(n),D&&u(this,n),n},addItems:function(n){T(n),_.each(n,function(n){this.addItem(n)},this)},resource:y,addRoute:function(n){if(n=n||{},A(n),n.resource=this,g(n),n.onItem){var e=k(this).getAll();_.each(e,function(e){n.item=e,p(n),v(n)})}else h(n),v(n)},expect:function(e,t){C(e),t=t||"";var r=this;return{"with":function(u){n={method:e,route:t,expected:u,resource:r},d(n)}}}}}function b(n){var e=[];return{fullRoute:n,nextResponse:function(n,t){e.push({status:n,data:t})},getSpecialResponse:function(){return e.shift()},hasSpecialResponse:function(){return Boolean(e.length)},getSpecialResponseOr:function(n){if(this.hasSpecialResponse()){var e=this.getSpecialResponse();return[e.status,JSON.stringify(e.data),{},"OK"]}return _.isFunction(n)&&n()}}}function w(n){var e={};return{getAll:function(){return _.map(e)},create:function(n){x(n); -var t=n.id;e[JSON.stringify(t)]=n},find:function(t,r){R(t),r=r||{};var u=e[JSON.stringify(t)];if(!u)throw new Error("No item found in "+n+" table with id of "+t);return r.wrap?S(u,j(n)):u},"delete":function(t){R(t);var t=JSON.stringify(t),r=e[t];if(!r)throw new Error("No item found in "+n+" table with id of "+t+". So can't delete it.");return e[t]=null,r}}}function k(n){var e=Facade.db[n.name];if(!e)throw new Error("There doesnt appear to be a table called "+n.name);return e}function O(n){return k(n).getAll()}function F(n,e,t){return t=t||{},k(n).find(e,t)}function S(n,e){return n=_.extend({},n),n.showUrl=function(){return e.url+"/"+n.id},n}function j(n){var e=Facade.resources[n];if(!e)throw new Error("There doesnt appear to be a resource called "+n);return e}function E(n){if(!_.isString(n.name))throw new Error("You must provide a name for the resource");if(Facade.resources[n.name])throw new Error("A resource named "+n.name+" already exists. Please choose a different name.");if(!_.isString(n.url))throw new Error("You must provide a url for the "+n.name+" resource");var e=_.pluck(Facade.resources,"url");if(_.find(e,function(e){return e===n.url}))throw new Error("The url "+n.url+" is already taken. Please change one")}function x(n){if(!n.id)throw new Error("The resource must have an id property.")}function R(n){if(!n)throw new Error("You must pass in an id to find a record")}function T(n){if(!_.isArray(n))throw new Error("addItems must take an array")}function C(n){if(!_.isString(n))throw new Error("No HTTP method was provided");var e=["GET","POST","PATCH","PUT","HEAD","DELETE"];if(!_.contains(e,n))throw new Error(n+" is not a valid HTTP method.")}function N(n){if(n=n||{},Facade.backend=Facade.backend||n.backend||{},!_.isFunction(Facade.backend.whenGET))throw new Error("$httpBackend not detected. Either add it as an option when initializing, or set the attribute directly on Facade via Facade.backend = $httpBackend")}function A(n){if(C(n.method),!_.isString(n.route))throw new Error("You must supply a route. eg: '/my_route' ")}window.Facade={};var I,D=!1;Facade.resources={},Facade.db={};var J={},P={},K={},B={},W=[];Facade.resource=function(n){return n=n||{},E(n),this.db[n.name]=w(n.name),J[n.name]=[],this.resources[n.name]=y(n)},Facade.initialize=function(t){N(t),D=!0,_.isFunction(I)&&I(),_.each(this.resources,function(n){e(n)}),n()},Facade.reset=function(){Facade.resources=_.clone(P,!0),Facade.db=_.clone(K,!0),J=_.clone(B,!0),D=!1},Facade.define=function(n){I=n},Facade.undefine=function(){I=void 0},Facade.clear=function(){this.resources={},this.db={},J={},W=[],this.backend=void 0,D=!1},Facade.findRoute=function(n,e){var t,r=[n,e].join(" "),u=_.any(J,function(n){return Boolean(n[r])&&(t=n[r])});if(!u)throw new Error("The route "+r+" does not exist");return t}}.call(this); \ No newline at end of file +(function(){function n(n,e,t){t=(t||0)-1;for(var r=n?n.length:0;++ta||"undefined"==typeof i)return 1;if(a>i||"undefined"==typeof a)return-1}}return n.n-e.n}function o(n){var e=-1,r=n.length,u=n[0],o=n[r/2|0],i=n[r-1];if(u&&"object"==typeof u&&o&&"object"==typeof o&&i&&"object"==typeof i)return!1;for(u=c(),u["false"]=u["null"]=u["true"]=u.undefined=!1,o=c(),o.k=n,o.l=u,o.push=t;++et?0:t);++r3&&"function"==typeof i[c-2])var f=ee(i[--c-1],i[c--],2);else c>2&&"function"==typeof i[c-1]&&(f=i[--c]);for(;++a=y&&i===n,f=[];if(c){var s=o(r);s?(i=e,r=s):c=!1}for(;++ui(r,s)&&f.push(s);return c&&l(r),f}function ue(n,e,t,r){r=(r||0)-1;for(var u=n?n.length:0,o=[];++r=y&&c===n,d=u||h?a():p;for(h&&(d=o(d),c=e);++ic(d,v))&&((u||h)&&d.push(v),p.push(g))}return h?(f(d.k),l(d)):u&&f(d),p}function fe(n){return function(e,t,r){var u={};t=M.createCallback(t,r,3),r=-1;var o=e?e.length:0;if("number"==typeof o)for(;++rt?St(0,o+t):t)||0,Dt(n)?i=-1o&&(o=a)}}else e=null==e&&Ee(n)?r:M.createCallback(e,t,3),Re(n,function(n,t,r){t=e(n,t,r),t>u&&(u=t,o=n)});return o}function Ae(n,e,t,r){if(!n)return t;var u=3>arguments.length;e=M.createCallback(e,r,4);var o=-1,i=n.length;if("number"==typeof i)for(u&&(t=n[++o]);++oarguments.length;return e=M.createCallback(e,r,4),Te(n,function(n,r,o){t=u?(u=!1,n):e(t,n,r,o)}),t}function De(n){var e=-1,t=n?n.length:0,r=Qe("number"==typeof t?t:0);return Re(n,function(n){var t=ae(0,++e);r[e]=r[t],r[t]=n}),r}function Pe(n,e,t){var r;e=M.createCallback(e,t,3),t=-1;var u=n?n.length:0;if("number"==typeof u)for(;++tr?St(0,u+r):r||0}else if(r)return r=Be(e,t),e[r]===t?r:-1;return n(e,t,r)}function $e(n,e,t){if("number"!=typeof e&&null!=e){var r=0,u=-1,o=n?n.length:0;for(e=M.createCallback(e,t,3);++uu;)r=u+o>>>1,t(n[r])t?0:t);++e0?l=bt(u,t):(i&&ht(i),t=s,i=l=s=h,t&&(p=Ut(),a=n.apply(f,o),l||i||(o=f=null)))}var o,i,a,c,f,l,s,p=0,d=!1,g=!0;if(!_e(n))throw new it;if(e=St(0,e)||0,!0===t)var v=!0,g=!1;else we(t)&&(v=t.leading,d="maxWait"in t&&(St(e,t.maxWait)||0),g="trailing"in t?t.trailing:g);return function(){if(o=arguments,c=Ut(),f=this,s=g&&(l||!v),!1===d)var t=v&&!l;else{i||v||(p=c);var h=d-(c-p),m=0>=h;m?(i&&(i=ht(i)),p=c,a=n.apply(f,o)):i||(i=bt(r,h))}return m&&l?l=ht(l):l||e===d||(l=bt(u,e)),t&&(m=!0,a=n.apply(f,o)),!m||l||i||(o=f=null),a}}function Ue(n){return n}function Ye(n,e,t){var r=!0,u=e&&ye(e);e&&(t||u.length)||(null==t&&(t=e),o=V,e=n,n=M,u=ye(e)),!1===t?r=!1:we(t)&&"chain"in t&&(r=t.chain);var o=n,i=_e(o);Re(u,function(t){var u=n[t]=e[t];i&&(o.prototype[t]=function(){var e=this.__chain__,t=this.__wrapped__,i=[t];if(yt.apply(i,arguments),i=u.apply(n,i),r||e){if(t===i&&we(i))return this;i=new o(i),i.__chain__=e}return i})})}function He(){}function Me(n){return function(e){return e[n]}}function Ve(){return this.__wrapped__}t=t?X.defaults(Y.Object(),t,X.pick(Y,N)):Y;var Qe=t.Array,Xe=t.Boolean,Ze=t.Date,nt=t.Function,et=t.Math,tt=t.Number,rt=t.Object,ut=t.RegExp,ot=t.String,it=t.TypeError,at=[],ct=rt.prototype,ft=t._,lt=ct.toString,st=ut("^"+ot(lt).replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/toString| for [^\]]+/g,".*?")+"$"),pt=et.ceil,ht=t.clearTimeout,dt=et.floor,gt=nt.prototype.toString,vt=he(vt=rt.getPrototypeOf)&&vt,mt=ct.hasOwnProperty,yt=at.push,bt=t.setTimeout,_t=at.splice,wt=at.unshift,kt=function(){try{var n={},e=he(e=rt.defineProperty)&&e,t=e(n,n,n)&&e}catch(r){}return t}(),Et=he(Et=rt.create)&&Et,xt=he(xt=Qe.isArray)&&xt,Ft=t.isFinite,Ot=t.isNaN,jt=he(jt=rt.keys)&&jt,St=et.max,Rt=et.min,Tt=t.parseInt,Ct=et.random,Nt={};Nt[I]=Qe,Nt[D]=Xe,Nt[P]=Ze,Nt[J]=nt,Nt[$]=rt,Nt[W]=tt,Nt[B]=ut,Nt[K]=ot,V.prototype=M.prototype;var At=M.support={};At.funcDecomp=!he(t.a)&&T.test(p),At.funcNames="string"==typeof nt.name,M.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:j,variable:"",imports:{_:M}},Et||(ne=function(){function n(){}return function(e){if(we(e)){n.prototype=e;var r=new n;n.prototype=null}return r||t.Object()}}());var It=kt?function(n,e){G.value=e,kt(n,"__bindData__",G)}:He,Dt=xt||function(n){return n&&"object"==typeof n&&"number"==typeof n.length&<.call(n)==I||!1},Pt=jt?function(n){return we(n)?jt(n):[]}:H,Jt={"&":"&","<":"<",">":">",'"':""","'":"'"},Wt=be(Jt),$t=ut("("+Pt(Wt).join("|")+")","g"),Bt=ut("["+Pt(Jt).join("")+"]","g"),Kt=vt?function(n){if(!n||lt.call(n)!=$)return!1;var e=n.valueOf,t=he(e)&&(t=vt(e))&&vt(t);return t?n==t||vt(n)==t:de(n)}:de,zt=fe(function(n,e,t){mt.call(n,t)?n[t]++:n[t]=1}),qt=fe(function(n,e,t){(mt.call(n,t)?n[t]:n[t]=[]).push(e)}),Gt=fe(function(n,e,t){n[t]=e}),Lt=Ce,Ut=he(Ut=Ze.now)&&Ut||function(){return(new Ze).getTime()},Yt=8==Tt(_+"08")?Tt:function(n,e){return Tt(Ee(n)?n.replace(S,""):n,e||0)};return M.after=function(n,e){if(!_e(e))throw new it;return function(){return 1>--n?e.apply(this,arguments):void 0}},M.assign=U,M.at=function(n){for(var e=arguments,t=-1,r=ue(e,!0,!1,1),e=e[2]&&e[2][e[1]]===n?1:r.length,u=Qe(e);++t=y&&o(r?t[r]:p)))}var s=t[0],d=-1,g=s?s.length:0,v=[];n:for(;++d(m?e(m,h):c(p,h))){for(r=u,(m||p).push(h);--r;)if(m=i[r],0>(m?e(m,h):c(t[r],h)))continue n;v.push(h)}}for(;u--;)(m=i[u])&&l(m);return f(i),f(p),v},M.invert=be,M.invoke=function(n,e){var t=s(arguments,2),r=-1,u="function"==typeof e,o=n?n.length:0,i=Qe("number"==typeof o?o:0);return Re(n,function(n){i[++r]=(u?e:n[e]).apply(n,t)}),i},M.keys=Pt,M.map=Ce,M.mapValues=function(n,e,t){var r={};return e=M.createCallback(e,t,3),d(n,function(n,t,u){r[t]=e(n,t,u)}),r},M.max=Ne,M.memoize=function(n,e){function t(){var r=t.cache,u=e?e.apply(this,arguments):m+arguments[0];return mt.call(r,u)?r[u]:r[u]=n.apply(this,arguments)}if(!_e(n))throw new it;return t.cache={},t},M.merge=function(n){var e=arguments,t=2;if(!we(n))return n;if("number"!=typeof e[2]&&(t=e.length),t>3&&"function"==typeof e[t-2])var r=ee(e[--t-1],e[t--],2);else t>2&&"function"==typeof e[t-1]&&(r=e[--t]);for(var e=s(arguments,1,t),u=-1,o=a(),i=a();++ua&&(o=a)}}else e=null==e&&Ee(n)?r:M.createCallback(e,t,3),Re(n,function(n,t,r){t=e(n,t,r),u>t&&(u=t,o=n)});return o},M.omit=function(n,e,t){var r={};if("function"!=typeof e){var u=[];g(n,function(n,e){u.push(e)});for(var u=re(u,ue(arguments,!0,!1,1)),o=-1,i=u.length;++ot?St(0,r+t):Rt(t,r-1))+1);r--;)if(n[r]===e)return r;return-1},M.mixin=Ye,M.noConflict=function(){return t._=ft,this},M.noop=He,M.now=Ut,M.parseInt=Yt,M.random=function(n,e,t){var r=null==n,u=null==e;return null==t&&("boolean"==typeof n&&u?(t=n,n=1):u||"boolean"!=typeof e||(t=e,u=!0)),r&&u&&(e=1),n=+n||0,u?(e=n,n=0):e=+e||0,t||n%1||e%1?(t=Ct(),Rt(n+t*(e-n+parseFloat("1e-"+((t+"").length-1))),e)):ae(n,e)},M.reduce=Ae,M.reduceRight=Ie,M.result=function(n,e){if(n){var t=n[e];return _e(t)?n[e]():t}},M.runInContext=p,M.size=function(n){var e=n?n.length:0;return"number"==typeof e?e:Pt(n).length},M.some=Pe,M.sortedIndex=Be,M.template=function(n,e,t){var r=M.templateSettings;n=ot(n||""),t=b({},t,r);var u,o=b({},t.imports,r.imports),r=Pt(o),o=xe(o),a=0,c=t.interpolate||R,f="__p+='",c=ut((t.escape||R).source+"|"+c.source+"|"+(c===j?x:R).source+"|"+(t.evaluate||R).source+"|$","g");n.replace(c,function(e,t,r,o,c,l){return r||(r=o),f+=n.slice(a,l).replace(C,i),t&&(f+="'+__e("+t+")+'"),c&&(u=!0,f+="';"+c+";\n__p+='"),r&&(f+="'+((__t=("+r+"))==null?'':__t)+'"),a=l+e.length,e}),f+="';",c=t=t.variable,c||(t="obj",f="with("+t+"){"+f+"}"),f=(u?f.replace(w,""):f).replace(k,"$1").replace(E,"$1;"),f="function("+t+"){"+(c?"":t+"||("+t+"={});")+"var __t,__p='',__e=_.escape"+(u?",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,'')}":";")+f+"return __p}";try{var l=nt(r,"return "+f).apply(h,o)}catch(s){throw s.source=f,s}return e?l(e):(l.source=f,l)},M.unescape=function(n){return null==n?"":ot(n).replace($t,ge)},M.uniqueId=function(n){var e=++v;return ot(null==n?"":n)+e},M.all=Oe,M.any=Pe,M.detect=Se,M.findWhere=Se,M.foldl=Ae,M.foldr=Ie,M.include=Fe,M.inject=Ae,Ye(function(){var n={};return d(M,function(e,t){M.prototype[t]||(n[t]=e)}),n}(),!1),M.first=Je,M.last=function(n,e,t){var r=0,u=n?n.length:0;if("number"!=typeof e&&null!=e){var o=u;for(e=M.createCallback(e,t,3);o--&&e(n[o],o,n);)r++}else if(r=e,null==r||t)return n?n[u-1]:h;return s(n,St(0,u-r))},M.sample=function(n,e,t){return n&&"number"!=typeof n.length&&(n=xe(n)),null==e||t?n?n[ae(0,n.length-1)]:h:(n=De(n),n.length=Rt(St(0,e),n.length),n)},M.take=Je,M.head=Je,d(M,function(n,e){var t="sample"!==e;M.prototype[e]||(M.prototype[e]=function(e,r){var u=this.__chain__,o=n(this.__wrapped__,e,r);return u||null!=e&&(!r||t&&"function"==typeof e)?new V(o,u):o})}),M.VERSION="2.4.1",M.prototype.chain=function(){return this.__chain__=!0,this},M.prototype.toString=function(){return ot(this.__wrapped__)},M.prototype.value=Ve,M.prototype.valueOf=Ve,Re(["join","pop","shift"],function(n){var e=at[n];M.prototype[n]=function(){var n=this.__chain__,t=e.apply(this.__wrapped__,arguments);return n?new V(t,n):t}}),Re(["push","reverse","sort","unshift"],function(n){var e=at[n];M.prototype[n]=function(){return e.apply(this.__wrapped__,arguments),this}}),Re(["concat","slice","splice"],function(n){var e=at[n];M.prototype[n]=function(){return new V(e.apply(this.__wrapped__,arguments),this.__chain__)}}),M}var h,d=[],g=[],v=0,m=+new Date+"",y=75,b=40,_=" \f \n\r\u2028\u2029 ᠎              ",w=/\b__p\+='';/g,k=/\b(__p\+=)''\+/g,E=/(__e\(.*?\)|\b__t\))\+'';/g,x=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,F=/\w*$/,O=/^\s*function[ \n\r\t]+\w/,j=/<%=([\s\S]+?)%>/g,S=RegExp("^["+_+"]*0+(?=.$)"),R=/($^)/,T=/\bthis\b/,C=/['\n\r\t\u2028\u2029\\]/g,N="Array Boolean Date Function Math Number Object RegExp String _ attachEvent clearTimeout isFinite isNaN parseInt setTimeout".split(" "),A="[object Arguments]",I="[object Array]",D="[object Boolean]",P="[object Date]",J="[object Function]",W="[object Number]",$="[object Object]",B="[object RegExp]",K="[object String]",z={};z[J]=!1,z[A]=z[I]=z[D]=z[P]=z[W]=z[$]=z[B]=z[K]=!0;var q={leading:!1,maxWait:0,trailing:!1},G={configurable:!1,enumerable:!1,value:null,writable:!1},L={"boolean":!1,"function":!0,object:!0,number:!1,string:!1,undefined:!1},U={"\\":"\\","'":"'","\n":"n","\r":"r"," ":"t","\u2028":"u2028","\u2029":"u2029"},Y=L[typeof window]&&window||this,H=L[typeof exports]&&exports&&!exports.nodeType&&exports,M=L[typeof module]&&module&&!module.nodeType&&module,V=M&&M.exports===H&&H,Q=L[typeof global]&&global;!Q||Q.global!==Q&&Q.window!==Q||(Y=Q);var X=p();"function"==typeof define&&"object"==typeof define.amd&&define.amd?(Y._=X,define(function(){return X})):H&&M?V?(M.exports=X)._=X:H._=X:Y._=X}).call(this),function(){"use strict";function n(){$=_.clone(Facade.resources,!0),B=_.clone(Facade.db,!0),K=_.clone(W,!0)}function e(n){t(n),r(n)}function t(n){_.each(s(),function(e){var t={resource:n,method:e.method};e.createWith(t),g(t)})}function r(n){var e=E(n);_.each(e,function(e){u(n,e)})}function u(n,e){_.each(l(),function(t){var r={resource:n,item:e,method:t.method};t.createWith(r),g(r)}),_.each(z,function(n){n.item=e,p(n),g(n)})}function o(n){Facade.backend.whenGET(n.resource.url+"/"+n.item.id).respond(function(e,t){var r=Facade.findRoute(e,t),u=r.getSpecialResponseOr(function(){var e=x(n.resource,n.item.id);return[200,JSON.stringify(e),{},"OK"]});return u})}function i(n){Facade.backend.whenPUT(n.resource.url+"/"+n.item.id).respond(function(e,t,r,u){r=r||{};var o=Facade.findRoute(e,t),i=o.getSpecialResponseOr(function(){var e=x(n.resource,n.item.id);return _.assign(e,JSON.parse(r)),[200,JSON.stringify(e),u,"OK"]});return i})}function a(n){Facade.backend.whenDELETE(n.resource.url+"/"+n.item.id).respond(function(e,t,r){r=r||{};var u=Facade.findRoute(e,t),o=u.getSpecialResponseOr(function(){var e=k(n.resource).delete(n.item.id);return[200,JSON.stringify(e),{},"OK"]});return o})}function c(n){Facade.backend.whenPOST(n.resource.url).respond(function(e,t,r){r=r||{},r=JSON.parse(r);var u=Facade.findRoute(e,t),o=u.getSpecialResponseOr(function(){var e=_.isFunction(n.resource.createDefault)&&n.resource.createDefault(r);return e=e||r,n.resource.addItem(e),[200,JSON.stringify(e),{},"OK"]});return o})}function f(n){Facade.backend.whenGET(n.resource.url).respond(function(e,t){var r=Facade.findRoute(e,t),u=r.getSpecialResponseOr(function(){return[200,E(n.resource),{},"OK"]});return u})}function l(){return[{createWith:o,method:"GET"},{createWith:i,method:"PUT"},{createWith:a,method:"DELETE"}]}function s(){return[{createWith:c,method:"POST"},{createWith:f,method:"GET"}]}function p(n){A(n.route);var e=n.resource.url+"/"+n.item.id+n.route;Facade.backend.when(n.method,e).respond(function(e,t,r,u){r=r||{};var o=Facade.findRoute(e,t),i=k(n.resource).find(n.item.id),a=o.getSpecialResponseOr(function(){return n.callback(r,i,u)});return D(a),a})}function h(n){var e=_.isRegExp(n.route)?n.route:n.resource.url+n.route;Facade.backend.when(n.method,e).respond(function(e,t,r){r=r||{};var u=k(n.resource).getAll(),o=Facade.findRoute(e,t),i=o.getSpecialResponseOr(function(){return n.callback(r,u)});return D(i),i})}function d(n){var e=n.resource.url+n.route;Facade.backend.expect(n.method,e,m(n.expected)).respond(function(e,t,r){r=r||{};var u=k(n.resource).getAll(),o=Facade.findRoute(e,t);o.hasSpecialResponse()||_.isFunction(n.callback)&&n.callback(r,u);var i=o.getSpecialResponseOr(function(){return[200,JSON.stringify(u),{},"OK"]});return i})}function g(n){if(n.route=n.route||"",_.isRegExp(n.route))n.regExp=n.route;else{var e=n.resource.url,t=n.item?e+"/"+n.item.id+n.route:e+n.route,r=[n.method,t].join(" ");n.fullRoute=r}W[n.resource.name][n.regExp||n.fullRoute]=b(n)}function v(n){z.push(n)}function m(n){function e(t,r,u){if(t[u])return t[u]===r?!0:(console.log("Expected",u,"to equal",r,"in",t,"but it was",t[u]),!1);var o=_.filter(t,function(n){return _.isObject(n)}),i=t[u]===val;return o||i?_.any(o,function(n){return e(n,r,u)}):(console.log("Missing expectedKey",u,"in",nestedData,"should include",n),!1)}return function(t){var r=JSON.parse(t);return r?_.all(n,function(n,t){return e(r,n,t)}):(console.log("Unable to parse to JSON:",t),!1)}}function y(n){return n=n||{},n.url=this&&this.url?this.url+n.url:n.url,{url:n.url,name:n.name,addItem:function(n){return S(n),k(this).create(n),J&&u(this,n),n},addItems:function(n){T(n),_.each(n,function(n){this.addItem(n)},this)},resource:y,addRoute:function(n){if(n=n||{},I(n),n.resource=this,v(n),n.onItem){var e=k(this).getAll();_.each(e,function(e){n.item=e,p(n),g(n)})}else h(n),g(n)},expect:function(e,t){C(e),t=t||"";var r=this;return{"with":function(u){n={method:e,route:t,expected:u,resource:r},d(n)}}}}}function b(n){var e=[];return{fullRoute:n.fullRoute,regExp:n.regExp,method:n.method,nextResponse:function(n,t){e.push({status:n,data:t})},getSpecialResponse:function(){return e.shift()},hasSpecialResponse:function(){return Boolean(e.length)},getSpecialResponseOr:function(n){if(this.hasSpecialResponse()){var e=this.getSpecialResponse();return[e.status,JSON.stringify(e.data),{},"OK"]}return _.isFunction(n)&&n()}}}function w(n){var e={};return{getAll:function(){return _.map(e) +},create:function(n){S(n);var t=n.id;e[JSON.stringify(t)]=n},find:function(t,r){R(t),r=r||{};var u=e[JSON.stringify(t)];if(!u)throw new Error("No item found in "+n+" table with id of "+t);return r.wrap?F(u,O(n)):u},"delete":function(t){R(t);var t=JSON.stringify(t),r=e[t];if(!r)throw new Error("No item found in "+n+" table with id of "+t+". So can't delete it.");return e[t]=null,r}}}function k(n){var e=Facade.db[n.name];if(!e)throw new Error("There doesnt appear to be a table called "+n.name);return e}function E(n){return k(n).getAll()}function x(n,e,t){return t=t||{},k(n).find(e,t)}function F(n,e){return n=_.extend({},n),n.showUrl=function(){return e.url+"/"+n.id},n}function O(n){var e=Facade.resources[n];if(!e)throw new Error("There doesnt appear to be a resource called "+n);return e}function j(n){if(!_.isString(n.name))throw new Error("You must provide a name for the resource");if(Facade.resources[n.name])throw new Error("A resource named "+n.name+" already exists. Please choose a different name.");if(!_.isString(n.url))throw new Error("You must provide a url for the "+n.name+" resource");var e=_.pluck(Facade.resources,"url");if(_.find(e,function(e){return e===n.url}))throw new Error("The url "+n.url+" is already taken. Please change one")}function S(n){if(!n.id)throw new Error("The resource must have an id property.")}function R(n){if(!n)throw new Error("You must pass in an id to find a record")}function T(n){if(!_.isArray(n))throw new Error("addItems must take an array")}function C(n){if(!_.isString(n))throw new Error("No HTTP method was provided");var e=["GET","POST","PATCH","PUT","HEAD","DELETE"];if(!_.contains(e,n))throw new Error(n+" is not a valid HTTP method.")}function N(n){if(n=n||{},Facade.backend=Facade.backend||n.backend||{},!_.isFunction(Facade.backend.whenGET))throw new Error("$httpBackend not detected. Either add it as an option when initializing, or set the attribute directly on Facade via Facade.backend = $httpBackend")}function A(n){if(_.isRegExp(n))throw new Error("Regex routes can't be used for item routes. Either make 'onItem' false or make the route a string")}function I(n){if(C(n.method),!_.isString(n.route)&&!_.isRegExp(n.route))throw new Error("You must supply a route (eg: '/my_route') as either a string or regex");if(!n.callback)throw new Error("You must supply a response callback for custom routes.")}function D(n){if(!_.isArray(n))throw new Error("Response must be an array");if(4!==n.length)throw new Error("Response does not appear to be in the form of [status, data, headers, status_text]")}window.Facade={};var P,J=!1;Facade.resources={},Facade.db={};var W={},$={},B={},K={},z=[];Facade.resource=function(n){return n=n||{},j(n),this.db[n.name]=w(n.name),W[n.name]={},this.resources[n.name]=y(n)},Facade.initialize=function(t){N(t),J=!0,_.isFunction(P)&&P(),_.each(this.resources,function(n){e(n)}),n()},Facade.reset=function(){Facade.resources=_.clone($,!0),Facade.db=_.clone(B,!0),W=_.clone(K,!0),J=!1},Facade.define=function(n){P=n},Facade.undefine=function(){P=void 0},Facade.clear=function(){this.resources={},this.db={},W={},z=[],this.backend=void 0,J=!1},Facade.findRoute=function(n,e){var t,r=[n,e].join(" "),u=_.any(W,function(u){return t=u[r],t?t:t=_.chain(u).filter("regExp").where({method:n}).find(function(n){return n.regExp.test(e)}).value()});if(!u)throw new Error("The route "+r+" does not exist");return t}}.call(this); \ No newline at end of file diff --git a/src/facade.js b/src/facade.js index 41dfb96..2822a02 100644 --- a/src/facade.js +++ b/src/facade.js @@ -24,7 +24,7 @@ this.db[opts.name] = buildTable(opts.name); // Create a slot for the resources routes in the master route list; - facadeRoutes[opts.name] = []; + facadeRoutes[opts.name] = {}; // Add resource to master list return this.resources[opts.name] = buildResource(opts); @@ -68,12 +68,20 @@ var routeObj; var fullRoute = [method, url].join(' '); var exists = _.any(facadeRoutes, function(resourceRoutes) { - return Boolean(resourceRoutes[fullRoute]) && (routeObj = resourceRoutes[fullRoute]); + routeObj = resourceRoutes[fullRoute]; + if (routeObj) { + return routeObj; + } + return routeObj = _.chain(resourceRoutes) + .filter('regExp') + .where({method: method}) + .find(function(route) { + return route.regExp.test(url); + }).value(); }); if (!exists) { throw new Error("The route " + fullRoute + " does not exist"); } - return routeObj; } @@ -225,37 +233,33 @@ function createCustomRouteForItem(opts) { + throwIfRegex(opts.route); var fullUrl = opts.resource.url + '/' + opts.item.id + opts.route; Facade.backend.when(opts.method, fullUrl).respond(function(method, url, requestData, headers) { requestData = requestData || {}; var route = Facade.findRoute(method, url); - - if (!route.hasSpecialResponse()) { - _.isFunction(opts.callback) && opts.callback(requestData, opts.item); - } + var item = getTable(opts.resource).find(opts.item.id); var response = route.getSpecialResponseOr(function() { - return [200, JSON.stringify(opts.item), {}, 'OK']; + return opts.callback(requestData, item, headers); }); + checkForValidResponse(response); return response; }); } function createCustomRouteForCollection(opts) { - var fullUrl = opts.resource.url + opts.route; + var fullUrl = _.isRegExp(opts.route) ? opts.route : opts.resource.url + opts.route Facade.backend.when(opts.method, fullUrl).respond(function(method, url, requestData, headers) { requestData = requestData || {}; var collection = getTable(opts.resource).getAll(); var route = Facade.findRoute(method, url); - if (!route.hasSpecialResponse()) { - _.isFunction(opts.callback) && opts.callback(requestData, collection); - } - var response = route.getSpecialResponseOr(function() { - return [200, JSON.stringify(collection), {}, 'OK']; + return opts.callback(requestData, collection); }); + checkForValidResponse(response); return response; }); @@ -283,10 +287,16 @@ function storeRoute(opts) { opts.route = opts.route || ''; - var prefix = opts.resource.url; - var fullUrl = opts.item ? prefix + '/' + opts.item.id + opts.route : prefix + opts.route; - var fullRoute = [opts.method, fullUrl].join(' ') - facadeRoutes[opts.resource.name][fullRoute] = buildRoute(fullRoute); + if (_.isRegExp(opts.route)) { + opts.regExp = opts.route; + }else { + var prefix = opts.resource.url; + var fullUrl = opts.item ? prefix + '/' + opts.item.id + opts.route : prefix + opts.route; + var fullRoute = [opts.method, fullUrl].join(' ') + + opts.fullRoute = fullRoute; + } + facadeRoutes[opts.resource.name][(opts.regExp || opts.fullRoute)] = buildRoute(opts); } function storeRouteOpts(opts) { @@ -381,10 +391,12 @@ }; } - function buildRoute(fullRoute) { + function buildRoute(opts) { var specialResponses = []; return { - fullRoute: fullRoute, + fullRoute: opts.fullRoute, + regExp: opts.regExp, + method: opts.method, nextResponse: function(status, data) { specialResponses.push({status: status, data: data}); }, @@ -537,11 +549,27 @@ } } + function throwIfRegex(route) { + if (_.isRegExp(route)) { + throw new Error("Regex routes can't be used for item routes. Either make 'onItem' false" + + " or make the route a string") + } + } + function checkForRequiredRouteArgs(opts) { checkForValidMethod(opts.method); + if (!_.isString(opts.route) && !_.isRegExp(opts.route)) { + throw new Error("You must supply a route (eg: '/my_route') as either a string or regex"); + } + if (!opts.callback) { + throw new Error("You must supply a response callback for custom routes."); + } + } - if (!_.isString(opts.route)) { - throw new Error("You must supply a route. eg: '/my_route' "); + function checkForValidResponse(response) { + if (!_.isArray(response)) { throw new Error("Response must be an array");} + if (response.length !== 4) { + throw new Error("Response does not appear to be in the form of [status, data, headers, status_text]" ) } } diff --git a/test/test.js b/test/test.js index 922bf57..9b515a7 100644 --- a/test/test.js +++ b/test/test.js @@ -306,6 +306,7 @@ route:"/verify", callback: function(data, item) { item.verified = true; + return [200, item, {}, 'OK']; }, onItem: true }); @@ -431,6 +432,7 @@ _.each(collection, function(item) { item.verified = true; }) + return [200, collection, {}, 'OK']; } }); @@ -451,6 +453,7 @@ route:"/verify", callback: function(data, item, headers) { item.verified = true; + return [200, item, {}, 'OK']; }, onItem: true }); @@ -464,9 +467,47 @@ $rootScope.verifiedPatient.verified.should.eql(true); }); - xit("should be able to change the status and response value of custom routes", function() { + it("should be able to take a regex for the route", function() { + patientResource.addRoute({ + method:"POST", + route: /verify/, + callback: function(data, collection, headers) { + _.each(collection, function(item) {item.verified = true}); + return [200, collection[0], {}, 'OK']; + } + }); + $rootScope.getOne("patients", patient1.id); + $httpBackend.flush(); + $rootScope.item.verified.should.eql(false); + + $rootScope.verifyPatient(patient1.id); + $httpBackend.flush(); + $rootScope.verifiedPatient.verified.should.eql(true); }); + it("should throw an error if the custom route returns an invalid response", function() { + patientResource.addRoute({ + method:"POST", + route: /verify/, + callback: function(data, collection, headers) { + _.each(collection, function(item) {item.verified = true}); + return [collection[0], {}, 'OK']; + } + }); + + (function() { + $rootScope.verifyPatient(patient1.id); + $httpBackend.flush(); + }).should.throw(/Response/); + }); + it("should throw an error if the custom route doesn't have a callback", function() { + (function() { + patientResource.addRoute({ + method:"POST", + route: /verify/ + }); + }).should.throw(/supply a response/) + }) xdescribe("short cut route methods", function() { it("should have a shortcut GET method", function() {