mongo_browser 0.1.3 → 0.2.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +3 -3
  3. data/README.md +1 -25
  4. data/Rakefile +0 -1
  5. data/app/assets/images/background.png +0 -0
  6. data/app/assets/javascripts/app/controllers/alerts.js.coffee +12 -0
  7. data/app/assets/javascripts/app/controllers/breadcrumbs.js.coffee +25 -0
  8. data/app/assets/javascripts/app/controllers/collections.js.coffee +40 -0
  9. data/app/assets/javascripts/app/controllers/databases.js.coffee +39 -0
  10. data/app/assets/javascripts/app/controllers/documents.js.coffee +49 -0
  11. data/app/assets/javascripts/app/controllers/main.js.coffee +10 -0
  12. data/app/assets/javascripts/app/controllers/server_info.js.coffee +14 -0
  13. data/app/assets/javascripts/app/controllers.js.coffee +2 -0
  14. data/app/assets/javascripts/app/directives.js.coffee +20 -0
  15. data/app/assets/javascripts/app/filters.js.coffee +48 -0
  16. data/app/assets/javascripts/app/modules/dialogs.js.coffee +29 -0
  17. data/app/assets/javascripts/app/modules/pager.js.coffee +87 -0
  18. data/app/assets/javascripts/app/modules/table_filter.js.coffee +27 -0
  19. data/app/assets/javascripts/app/resources.js.coffee +22 -0
  20. data/app/assets/javascripts/app/services.js.coffee +36 -0
  21. data/app/assets/javascripts/app.js.coffee +8 -0
  22. data/app/assets/javascripts/application.js.coffee +35 -6
  23. data/app/assets/javascripts/templates/.gitkeep +0 -0
  24. data/app/assets/javascripts/templates.js.coffee +1 -0
  25. data/app/assets/javascripts/vendor.js.coffee +5 -0
  26. data/app/assets/stylesheets/application.css.scss +46 -3
  27. data/app/assets/templates/collections.html +53 -0
  28. data/app/assets/templates/databases.html +32 -0
  29. data/app/assets/templates/documents.html +45 -0
  30. data/app/assets/templates/pager.html +13 -0
  31. data/app/{views/server_info.erb → assets/templates/server_info.html} +5 -7
  32. data/app/assets/templates/table_filter.html +10 -0
  33. data/bin/mongo_browser +8 -45
  34. data/config-e2e.ru +20 -0
  35. data/grunt.js +36 -0
  36. data/lib/mongo_browser/application.rb +143 -64
  37. data/lib/mongo_browser/middleware/sprockets_base.rb +11 -0
  38. data/lib/mongo_browser/middleware/sprockets_sinatra.rb +3 -7
  39. data/lib/mongo_browser/middleware/sprockets_specs.rb +18 -0
  40. data/lib/mongo_browser/models/collection.rb +67 -0
  41. data/lib/mongo_browser/models/database.rb +52 -0
  42. data/lib/mongo_browser/models/document.rb +17 -0
  43. data/lib/mongo_browser/models/pager.rb +30 -0
  44. data/lib/mongo_browser/models/server.rb +51 -0
  45. data/lib/mongo_browser/version.rb +1 -1
  46. data/lib/mongo_browser.rb +10 -5
  47. data/mongo_browser.gemspec +4 -11
  48. data/public/index.html +47 -0
  49. data/script/ci_all +21 -0
  50. data/script/ci_e2e +11 -0
  51. data/script/ci_javascripts +4 -0
  52. data/script/ci_rspec +3 -0
  53. data/spec/features/collections_list_spec.rb +6 -49
  54. data/spec/features/documents_list_spec.rb +15 -14
  55. data/spec/features/server_info_spec.rb +3 -3
  56. data/spec/javascripts/app/controllers/alerts_spec.js.coffee +36 -0
  57. data/spec/javascripts/app/controllers/breadcrumbs_spec.js.coffee +28 -0
  58. data/spec/javascripts/app/controllers/collections_spec.js.coffee +78 -0
  59. data/spec/javascripts/app/controllers/databases_spec.js.coffee +55 -0
  60. data/spec/javascripts/app/controllers/documents_spec.js.coffee +62 -0
  61. data/spec/javascripts/app/controllers/main_spec.js.coffee +20 -0
  62. data/spec/javascripts/app/controllers/server_info_spec.js.coffee +21 -0
  63. data/spec/javascripts/app/directives_spec.js.coffee +58 -0
  64. data/spec/javascripts/app/filters_spec.js.coffee +99 -0
  65. data/spec/javascripts/app/modules/dialogs_spec.js.coffee +51 -0
  66. data/spec/javascripts/app/modules/pager_spec.js.coffee +104 -0
  67. data/spec/javascripts/app/modules/table_filter_spec.js.coffee +76 -0
  68. data/spec/javascripts/app/services_spec.js.coffee +83 -0
  69. data/spec/javascripts/config/testacular-e2e.conf.js +19 -0
  70. data/spec/javascripts/config/testacular.conf.js +43 -0
  71. data/spec/javascripts/e2e/collections_scenario.js.coffee +49 -0
  72. data/spec/javascripts/e2e/databases_scenario.js.coffee +74 -0
  73. data/spec/javascripts/e2e/documents_scenario.js.coffee +18 -0
  74. data/spec/javascripts/e2e/server_info_scenario.js.coffee +12 -0
  75. data/spec/javascripts/helpers/matchers.js.coffee +5 -0
  76. data/spec/javascripts/helpers/mocks.js.coffee +7 -0
  77. data/spec/javascripts/helpers_e2e/app_element.js.coffee +6 -0
  78. data/spec/javascripts/lib/angular-mocks.js +1740 -0
  79. data/spec/javascripts/lib/angular-scenario.js +26147 -0
  80. data/spec/javascripts/lib/jasmine-html.js +681 -0
  81. data/spec/javascripts/lib/jasmine.css +82 -0
  82. data/spec/javascripts/lib/jasmine.js +2600 -0
  83. data/spec/javascripts/runner.html +54 -0
  84. data/spec/javascripts/runner_e2e.html +10 -0
  85. data/spec/javascripts/spec.js.coffee +2 -0
  86. data/spec/javascripts/spec_e2e.js.coffee +2 -0
  87. data/spec/lib/models/collection_spec.rb +80 -0
  88. data/spec/lib/models/database_spec.rb +75 -0
  89. data/spec/lib/models/document_spec.rb +17 -0
  90. data/spec/lib/models/pager_spec.rb +64 -0
  91. data/spec/lib/models/server_spec.rb +76 -0
  92. data/spec/lib/mongo_browser_spec.rb +1 -1
  93. data/spec/spec_helper.rb +2 -19
  94. data/spec/support/feature_example_group.rb +8 -3
  95. data/spec/support/fixtures/databases.json +8 -0
  96. data/spec/support/fixtures.rb +20 -3
  97. data/spec/support/matchers/have_flash_message.rb +1 -1
  98. data/spec/support/mongod.rb +37 -21
  99. data/spec/support/mongodb.conf +47 -0
  100. data/vendor/assets/javascripts/angular/angular-bootstrap.js +166 -0
  101. data/vendor/assets/javascripts/angular/angular-resource.js +435 -0
  102. data/vendor/assets/javascripts/angular/angular-sanitize.js +535 -0
  103. data/vendor/assets/javascripts/angular/angular.js +14531 -0
  104. data/vendor/assets/javascripts/underscore.js +1200 -0
  105. metadata +136 -148
  106. data/app/assets/javascripts/app/table_filter.js.coffee +0 -50
  107. data/app/assets/javascripts/ujs.js.coffee +0 -23
  108. data/app/views/collections/index.erb +0 -59
  109. data/app/views/databases/index.erb +0 -29
  110. data/app/views/documents/index.erb +0 -61
  111. data/app/views/layout/_flash_messages.erb +0 -10
  112. data/app/views/layout/_navbar.erb +0 -22
  113. data/app/views/layout.erb +0 -20
  114. data/app/views/shared/_filter.erb +0 -14
  115. data/features/mongo_browser.feature +0 -18
  116. data/features/step_definitions/mongo_browser_steps.rb +0 -1
  117. data/features/support/env.rb +0 -18
  118. data/spec/features/databases_list_spec.rb +0 -65
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @license AngularJS v1.0.3
3
+ * (c) 2010-2012 Google, Inc. http://angularjs.org
4
+ * License: MIT
5
+ */
6
+ (function(window, angular, undefined) {
7
+ 'use strict';
8
+
9
+ var directive = {};
10
+
11
+ directive.dropdownToggle =
12
+ ['$document', '$location', '$window',
13
+ function ($document, $location, $window) {
14
+ var openElement = null, close;
15
+ return {
16
+ restrict: 'C',
17
+ link: function(scope, element, attrs) {
18
+ scope.$watch(function dropdownTogglePathWatch(){return $location.path();}, function dropdownTogglePathWatchAction() {
19
+ close && close();
20
+ });
21
+
22
+ element.parent().bind('click', function(event) {
23
+ close && close();
24
+ });
25
+
26
+ element.bind('click', function(event) {
27
+ event.preventDefault();
28
+ event.stopPropagation();
29
+
30
+ var iWasOpen = false;
31
+
32
+ if (openElement) {
33
+ iWasOpen = openElement === element;
34
+ close();
35
+ }
36
+
37
+ if (!iWasOpen){
38
+ element.parent().addClass('open');
39
+ openElement = element;
40
+
41
+ close = function (event) {
42
+ event && event.preventDefault();
43
+ event && event.stopPropagation();
44
+ $document.unbind('click', close);
45
+ element.parent().removeClass('open');
46
+ close = null;
47
+ openElement = null;
48
+ }
49
+
50
+ $document.bind('click', close);
51
+ }
52
+ });
53
+ }
54
+ };
55
+ }];
56
+
57
+
58
+ directive.tabbable = function() {
59
+ return {
60
+ restrict: 'C',
61
+ compile: function(element) {
62
+ var navTabs = angular.element('<ul class="nav nav-tabs"></ul>'),
63
+ tabContent = angular.element('<div class="tab-content"></div>');
64
+
65
+ tabContent.append(element.contents());
66
+ element.append(navTabs).append(tabContent);
67
+ },
68
+ controller: ['$scope', '$element', function($scope, $element) {
69
+ var navTabs = $element.contents().eq(0),
70
+ ngModel = $element.controller('ngModel') || {},
71
+ tabs = [],
72
+ selectedTab;
73
+
74
+ ngModel.$render = function() {
75
+ var $viewValue = this.$viewValue;
76
+
77
+ if (selectedTab ? (selectedTab.value != $viewValue) : $viewValue) {
78
+ if(selectedTab) {
79
+ selectedTab.paneElement.removeClass('active');
80
+ selectedTab.tabElement.removeClass('active');
81
+ selectedTab = null;
82
+ }
83
+ if($viewValue) {
84
+ for(var i = 0, ii = tabs.length; i < ii; i++) {
85
+ if ($viewValue == tabs[i].value) {
86
+ selectedTab = tabs[i];
87
+ break;
88
+ }
89
+ }
90
+ if (selectedTab) {
91
+ selectedTab.paneElement.addClass('active');
92
+ selectedTab.tabElement.addClass('active');
93
+ }
94
+ }
95
+
96
+ }
97
+ };
98
+
99
+ this.addPane = function(element, attr) {
100
+ var li = angular.element('<li><a href></a></li>'),
101
+ a = li.find('a'),
102
+ tab = {
103
+ paneElement: element,
104
+ paneAttrs: attr,
105
+ tabElement: li
106
+ };
107
+
108
+ tabs.push(tab);
109
+
110
+ attr.$observe('value', update)();
111
+ attr.$observe('title', function(){ update(); a.text(tab.title); })();
112
+
113
+ function update() {
114
+ tab.title = attr.title;
115
+ tab.value = attr.value || attr.title;
116
+ if (!ngModel.$setViewValue && (!ngModel.$viewValue || tab == selectedTab)) {
117
+ // we are not part of angular
118
+ ngModel.$viewValue = tab.value;
119
+ }
120
+ ngModel.$render();
121
+ }
122
+
123
+ navTabs.append(li);
124
+ li.bind('click', function(event) {
125
+ event.preventDefault();
126
+ event.stopPropagation();
127
+ if (ngModel.$setViewValue) {
128
+ $scope.$apply(function() {
129
+ ngModel.$setViewValue(tab.value);
130
+ ngModel.$render();
131
+ });
132
+ } else {
133
+ // we are not part of angular
134
+ ngModel.$viewValue = tab.value;
135
+ ngModel.$render();
136
+ }
137
+ });
138
+
139
+ return function() {
140
+ tab.tabElement.remove();
141
+ for(var i = 0, ii = tabs.length; i < ii; i++ ) {
142
+ if (tab == tabs[i]) {
143
+ tabs.splice(i, 1);
144
+ }
145
+ }
146
+ };
147
+ }
148
+ }]
149
+ };
150
+ };
151
+
152
+
153
+ directive.tabPane = function() {
154
+ return {
155
+ require: '^tabbable',
156
+ restrict: 'C',
157
+ link: function(scope, element, attrs, tabsCtrl) {
158
+ element.bind('$remove', tabsCtrl.addPane(element, attrs));
159
+ }
160
+ };
161
+ };
162
+
163
+
164
+ angular.module('bootstrap', []).directive(directive);
165
+
166
+ })(window, window.angular);
@@ -0,0 +1,435 @@
1
+ /**
2
+ * @license AngularJS v1.0.3
3
+ * (c) 2010-2012 Google, Inc. http://angularjs.org
4
+ * License: MIT
5
+ */
6
+ (function(window, angular, undefined) {
7
+ 'use strict';
8
+
9
+ /**
10
+ * @ngdoc overview
11
+ * @name ngResource
12
+ * @description
13
+ */
14
+
15
+ /**
16
+ * @ngdoc object
17
+ * @name ngResource.$resource
18
+ * @requires $http
19
+ *
20
+ * @description
21
+ * A factory which creates a resource object that lets you interact with
22
+ * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources.
23
+ *
24
+ * The returned resource object has action methods which provide high-level behaviors without
25
+ * the need to interact with the low level {@link ng.$http $http} service.
26
+ *
27
+ * @param {string} url A parameterized URL template with parameters prefixed by `:` as in
28
+ * `/user/:username`.
29
+ *
30
+ * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
31
+ * `actions` methods.
32
+ *
33
+ * Each key value in the parameter object is first bound to url template if present and then any
34
+ * excess keys are appended to the url search query after the `?`.
35
+ *
36
+ * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
37
+ * URL `/path/greet?salutation=Hello`.
38
+ *
39
+ * If the parameter value is prefixed with `@` then the value of that parameter is extracted from
40
+ * the data object (useful for non-GET operations).
41
+ *
42
+ * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the
43
+ * default set of resource actions. The declaration should be created in the following format:
44
+ *
45
+ * {action1: {method:?, params:?, isArray:?},
46
+ * action2: {method:?, params:?, isArray:?},
47
+ * ...}
48
+ *
49
+ * Where:
50
+ *
51
+ * - `action` – {string} – The name of action. This name becomes the name of the method on your
52
+ * resource object.
53
+ * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`,
54
+ * and `JSONP`
55
+ * - `params` – {object=} – Optional set of pre-bound parameters for this action.
56
+ * - isArray – {boolean=} – If true then the returned object for this action is an array, see
57
+ * `returns` section.
58
+ *
59
+ * @returns {Object} A resource "class" object with methods for the default set of resource actions
60
+ * optionally extended with custom `actions`. The default set contains these actions:
61
+ *
62
+ * { 'get': {method:'GET'},
63
+ * 'save': {method:'POST'},
64
+ * 'query': {method:'GET', isArray:true},
65
+ * 'remove': {method:'DELETE'},
66
+ * 'delete': {method:'DELETE'} };
67
+ *
68
+ * Calling these methods invoke an {@link ng.$http} with the specified http method,
69
+ * destination and parameters. When the data is returned from the server then the object is an
70
+ * instance of the resource class `save`, `remove` and `delete` actions are available on it as
71
+ * methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read,
72
+ * update, delete) on server-side data like this:
73
+ * <pre>
74
+ var User = $resource('/user/:userId', {userId:'@id'});
75
+ var user = User.get({userId:123}, function() {
76
+ user.abc = true;
77
+ user.$save();
78
+ });
79
+ </pre>
80
+ *
81
+ * It is important to realize that invoking a $resource object method immediately returns an
82
+ * empty reference (object or array depending on `isArray`). Once the data is returned from the
83
+ * server the existing reference is populated with the actual data. This is a useful trick since
84
+ * usually the resource is assigned to a model which is then rendered by the view. Having an empty
85
+ * object results in no rendering, once the data arrives from the server then the object is
86
+ * populated with the data and the view automatically re-renders itself showing the new data. This
87
+ * means that in most case one never has to write a callback function for the action methods.
88
+ *
89
+ * The action methods on the class object or instance object can be invoked with the following
90
+ * parameters:
91
+ *
92
+ * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])`
93
+ * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])`
94
+ * - non-GET instance actions: `instance.$action([parameters], [success], [error])`
95
+ *
96
+ *
97
+ * @example
98
+ *
99
+ * # Credit card resource
100
+ *
101
+ * <pre>
102
+ // Define CreditCard class
103
+ var CreditCard = $resource('/user/:userId/card/:cardId',
104
+ {userId:123, cardId:'@id'}, {
105
+ charge: {method:'POST', params:{charge:true}}
106
+ });
107
+
108
+ // We can retrieve a collection from the server
109
+ var cards = CreditCard.query(function() {
110
+ // GET: /user/123/card
111
+ // server returns: [ {id:456, number:'1234', name:'Smith'} ];
112
+
113
+ var card = cards[0];
114
+ // each item is an instance of CreditCard
115
+ expect(card instanceof CreditCard).toEqual(true);
116
+ card.name = "J. Smith";
117
+ // non GET methods are mapped onto the instances
118
+ card.$save();
119
+ // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
120
+ // server returns: {id:456, number:'1234', name: 'J. Smith'};
121
+
122
+ // our custom method is mapped as well.
123
+ card.$charge({amount:9.99});
124
+ // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
125
+ });
126
+
127
+ // we can create an instance as well
128
+ var newCard = new CreditCard({number:'0123'});
129
+ newCard.name = "Mike Smith";
130
+ newCard.$save();
131
+ // POST: /user/123/card {number:'0123', name:'Mike Smith'}
132
+ // server returns: {id:789, number:'01234', name: 'Mike Smith'};
133
+ expect(newCard.id).toEqual(789);
134
+ * </pre>
135
+ *
136
+ * The object returned from this function execution is a resource "class" which has "static" method
137
+ * for each action in the definition.
138
+ *
139
+ * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`.
140
+ * When the data is returned from the server then the object is an instance of the resource type and
141
+ * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
142
+ * operations (create, read, update, delete) on server-side data.
143
+
144
+ <pre>
145
+ var User = $resource('/user/:userId', {userId:'@id'});
146
+ var user = User.get({userId:123}, function() {
147
+ user.abc = true;
148
+ user.$save();
149
+ });
150
+ </pre>
151
+ *
152
+ * It's worth noting that the success callback for `get`, `query` and other method gets passed
153
+ * in the response that came from the server as well as $http header getter function, so one
154
+ * could rewrite the above example and get access to http headers as:
155
+ *
156
+ <pre>
157
+ var User = $resource('/user/:userId', {userId:'@id'});
158
+ User.get({userId:123}, function(u, getResponseHeaders){
159
+ u.abc = true;
160
+ u.$save(function(u, putResponseHeaders) {
161
+ //u => saved user object
162
+ //putResponseHeaders => $http header getter
163
+ });
164
+ });
165
+ </pre>
166
+
167
+ * # Buzz client
168
+
169
+ Let's look at what a buzz client created with the `$resource` service looks like:
170
+ <doc:example>
171
+ <doc:source jsfiddle="false">
172
+ <script>
173
+ function BuzzController($resource) {
174
+ this.userId = 'googlebuzz';
175
+ this.Activity = $resource(
176
+ 'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
177
+ {alt:'json', callback:'JSON_CALLBACK'},
178
+ {get:{method:'JSONP', params:{visibility:'@self'}}, replies: {method:'JSONP', params:{visibility:'@self', comments:'@comments'}}}
179
+ );
180
+ }
181
+
182
+ BuzzController.prototype = {
183
+ fetch: function() {
184
+ this.activities = this.Activity.get({userId:this.userId});
185
+ },
186
+ expandReplies: function(activity) {
187
+ activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id});
188
+ }
189
+ };
190
+ BuzzController.$inject = ['$resource'];
191
+ </script>
192
+
193
+ <div ng-controller="BuzzController">
194
+ <input ng-model="userId"/>
195
+ <button ng-click="fetch()">fetch</button>
196
+ <hr/>
197
+ <div ng-repeat="item in activities.data.items">
198
+ <h1 style="font-size: 15px;">
199
+ <img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
200
+ <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a>
201
+ <a href ng-click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a>
202
+ </h1>
203
+ {{item.object.content | html}}
204
+ <div ng-repeat="reply in item.replies.data.items" style="margin-left: 20px;">
205
+ <img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
206
+ <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: {{reply.content | html}}
207
+ </div>
208
+ </div>
209
+ </div>
210
+ </doc:source>
211
+ <doc:scenario>
212
+ </doc:scenario>
213
+ </doc:example>
214
+ */
215
+ angular.module('ngResource', ['ng']).
216
+ factory('$resource', ['$http', '$parse', function($http, $parse) {
217
+ var DEFAULT_ACTIONS = {
218
+ 'get': {method:'GET'},
219
+ 'save': {method:'POST'},
220
+ 'query': {method:'GET', isArray:true},
221
+ 'remove': {method:'DELETE'},
222
+ 'delete': {method:'DELETE'}
223
+ };
224
+ var noop = angular.noop,
225
+ forEach = angular.forEach,
226
+ extend = angular.extend,
227
+ copy = angular.copy,
228
+ isFunction = angular.isFunction,
229
+ getter = function(obj, path) {
230
+ return $parse(path)(obj);
231
+ };
232
+
233
+ /**
234
+ * We need our custom mehtod because encodeURIComponent is too aggressive and doesn't follow
235
+ * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path
236
+ * segments:
237
+ * segment = *pchar
238
+ * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
239
+ * pct-encoded = "%" HEXDIG HEXDIG
240
+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
241
+ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
242
+ * / "*" / "+" / "," / ";" / "="
243
+ */
244
+ function encodeUriSegment(val) {
245
+ return encodeUriQuery(val, true).
246
+ replace(/%26/gi, '&').
247
+ replace(/%3D/gi, '=').
248
+ replace(/%2B/gi, '+');
249
+ }
250
+
251
+
252
+ /**
253
+ * This method is intended for encoding *key* or *value* parts of query component. We need a custom
254
+ * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be
255
+ * encoded per http://tools.ietf.org/html/rfc3986:
256
+ * query = *( pchar / "/" / "?" )
257
+ * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
258
+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
259
+ * pct-encoded = "%" HEXDIG HEXDIG
260
+ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
261
+ * / "*" / "+" / "," / ";" / "="
262
+ */
263
+ function encodeUriQuery(val, pctEncodeSpaces) {
264
+ return encodeURIComponent(val).
265
+ replace(/%40/gi, '@').
266
+ replace(/%3A/gi, ':').
267
+ replace(/%24/g, '$').
268
+ replace(/%2C/gi, ',').
269
+ replace((pctEncodeSpaces ? null : /%20/g), '+');
270
+ }
271
+
272
+ function Route(template, defaults) {
273
+ this.template = template = template + '#';
274
+ this.defaults = defaults || {};
275
+ var urlParams = this.urlParams = {};
276
+ forEach(template.split(/\W/), function(param){
277
+ if (param && template.match(new RegExp("[^\\\\]:" + param + "\\W"))) {
278
+ urlParams[param] = true;
279
+ }
280
+ });
281
+ this.template = template.replace(/\\:/g, ':');
282
+ }
283
+
284
+ Route.prototype = {
285
+ url: function(params) {
286
+ var self = this,
287
+ url = this.template,
288
+ val,
289
+ encodedVal;
290
+
291
+ params = params || {};
292
+ forEach(this.urlParams, function(_, urlParam){
293
+ val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
294
+ if (angular.isDefined(val) && val !== null) {
295
+ encodedVal = encodeUriSegment(val);
296
+ url = url.replace(new RegExp(":" + urlParam + "(\\W)", "g"), encodedVal + "$1");
297
+ } else {
298
+ url = url.replace(new RegExp("/?:" + urlParam + "(\\W)", "g"), '$1');
299
+ }
300
+ });
301
+ url = url.replace(/\/?#$/, '');
302
+ var query = [];
303
+ forEach(params, function(value, key){
304
+ if (!self.urlParams[key]) {
305
+ query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value));
306
+ }
307
+ });
308
+ query.sort();
309
+ url = url.replace(/\/*$/, '');
310
+ return url + (query.length ? '?' + query.join('&') : '');
311
+ }
312
+ };
313
+
314
+
315
+ function ResourceFactory(url, paramDefaults, actions) {
316
+ var route = new Route(url);
317
+
318
+ actions = extend({}, DEFAULT_ACTIONS, actions);
319
+
320
+ function extractParams(data, actionParams){
321
+ var ids = {};
322
+ actionParams = extend({}, paramDefaults, actionParams);
323
+ forEach(actionParams, function(value, key){
324
+ ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value;
325
+ });
326
+ return ids;
327
+ }
328
+
329
+ function Resource(value){
330
+ copy(value || {}, this);
331
+ }
332
+
333
+ forEach(actions, function(action, name) {
334
+ var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH';
335
+ Resource[name] = function(a1, a2, a3, a4) {
336
+ var params = {};
337
+ var data;
338
+ var success = noop;
339
+ var error = null;
340
+ switch(arguments.length) {
341
+ case 4:
342
+ error = a4;
343
+ success = a3;
344
+ //fallthrough
345
+ case 3:
346
+ case 2:
347
+ if (isFunction(a2)) {
348
+ if (isFunction(a1)) {
349
+ success = a1;
350
+ error = a2;
351
+ break;
352
+ }
353
+
354
+ success = a2;
355
+ error = a3;
356
+ //fallthrough
357
+ } else {
358
+ params = a1;
359
+ data = a2;
360
+ success = a3;
361
+ break;
362
+ }
363
+ case 1:
364
+ if (isFunction(a1)) success = a1;
365
+ else if (hasBody) data = a1;
366
+ else params = a1;
367
+ break;
368
+ case 0: break;
369
+ default:
370
+ throw "Expected between 0-4 arguments [params, data, success, error], got " +
371
+ arguments.length + " arguments.";
372
+ }
373
+
374
+ var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
375
+ $http({
376
+ method: action.method,
377
+ url: route.url(extend({}, extractParams(data, action.params || {}), params)),
378
+ data: data
379
+ }).then(function(response) {
380
+ var data = response.data;
381
+
382
+ if (data) {
383
+ if (action.isArray) {
384
+ value.length = 0;
385
+ forEach(data, function(item) {
386
+ value.push(new Resource(item));
387
+ });
388
+ } else {
389
+ copy(data, value);
390
+ }
391
+ }
392
+ (success||noop)(value, response.headers);
393
+ }, error);
394
+
395
+ return value;
396
+ };
397
+
398
+
399
+ Resource.bind = function(additionalParamDefaults){
400
+ return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);
401
+ };
402
+
403
+
404
+ Resource.prototype['$' + name] = function(a1, a2, a3) {
405
+ var params = extractParams(this),
406
+ success = noop,
407
+ error;
408
+
409
+ switch(arguments.length) {
410
+ case 3: params = a1; success = a2; error = a3; break;
411
+ case 2:
412
+ case 1:
413
+ if (isFunction(a1)) {
414
+ success = a1;
415
+ error = a2;
416
+ } else {
417
+ params = a1;
418
+ success = a2 || noop;
419
+ }
420
+ case 0: break;
421
+ default:
422
+ throw "Expected between 1-3 arguments [params, success, error], got " +
423
+ arguments.length + " arguments.";
424
+ }
425
+ var data = hasBody ? this : undefined;
426
+ Resource[name].call(this, params, data, success, error);
427
+ };
428
+ });
429
+ return Resource;
430
+ }
431
+
432
+ return ResourceFactory;
433
+ }]);
434
+
435
+ })(window, window.angular);