angularjs-rails-resource 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Tommy Odom
1
+ Copyright (c) 2012 - 2013 FineLine Prototyping, Inc.
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -6,8 +6,17 @@ When starting out with AngularJS and Rails we initially were using $resource but
6
6
 
7
7
  1. $resource didn't return promises
8
8
  2. Rails prefers JSON be root wrapped
9
- 3. Our JSON contained snake case keys coming from our database but we didn't want to mix snake case and camel case in our UI
9
+ 3. Our JSON contained snake case (underscored) keys coming from our database but we didn't want to mix snake case and camel case in our UI
10
10
 
11
+ In case you are unfamiliar, the intent of the resource is to behave a bit like a remote model object. One of the nice things about AngularJS
12
+ is that it does not require you to create specific models for all of your data which gives you a lot of freedom for treating model data as basic
13
+ javascript objects. However, on the Rails side when exposing models to a javascript application you are likely going to follow the same pattern for multiple
14
+ models where you will have a controller that has your basic index (query), get, create, update, delete functionality.
15
+
16
+ The resource object created by this factory simplifies access to those models by exposing a mix of "class" methods (query, get) and
17
+ "instance" methods (create, update, delete/remove).
18
+
19
+ This module is being used for applications we are writing and we expect that over time that we will be adding additional functionality but we welcome contributions and suggestions.
11
20
 
12
21
  ## Installation
13
22
 
@@ -26,16 +35,22 @@ Since this is an [AngularJS](http://angularjs.org) module it of course depends o
26
35
  * [$http](http://docs.angularjs.org/api/ng.$http)
27
36
  * [$q](http://docs.angularjs.org/api/ng.$q)
28
37
  * [$injector](http://docs.angularjs.org/api/AUTO.$injector)
38
+ * [$interpolate](http://docs.angularjs.org/api/ng.$interpolate)
29
39
 
30
40
  ## Resource Creation
31
- Creating a resource using this factory is similar to using $resource, you just call the factory with the config options and get back your resource.
41
+ Creating a resource using this factory is similar to using $resource, you just call the factory with the config options and it returns a new resource function.
42
+ The resource function serves two purposes. First is that you can use (or define new) "class" methods directly accessible such as query and get to retrieve
43
+ instances from the backend rails service. The second is that it allows you to use it as a constructor to create new instances of that resource giving you access
44
+ to create, update, and delete instance methods (or any others you add).
45
+
46
+ The typical use case is to define the resource as an AngularJS factory within a module and then inject that into a controller or directive.
47
+ See [Examples](#examples) below for more information on creating and injecting the resource.
32
48
 
33
- var resource = railsResourceFactory(config);
34
49
 
35
50
  ### Config Options
36
51
  The following options are available for the config object passed to the factory function.
37
52
 
38
- * **url** - This is the base url of the service. See [Resource URLs](#resource-urls) below for more information.
53
+ * **url** - This is the url of the service. See [Resource URLs](#resource-urls) below for more information.
39
54
  * **name** - This is the name used for root wrapping when dealing with singular instances.
40
55
  * **pluralName** *(optional)* - If specified this name will be used for unwrapping query results,
41
56
  if not specified the singular name with an appended 's' will be used.
@@ -48,8 +63,20 @@ The following options are available for the config object passed to the factory
48
63
  For example, you should specify "publishingCompany" and "publishingCompanies" instead of "publishing_company" and "publishing_companies".
49
64
 
50
65
  ## Resource URLs
51
- The resource url functionality is pretty basic right now. The base url is used for query and create and the resource url
52
- is assumed to be the base url followed by the id. Therefore, a base url of '/books' will result in a resource url of '/books/123'.
66
+ The URL can be specified as one of three ways:
67
+
68
+ 1. function (context) - You can pass your own custom function that converts a context variable into a url string
69
+
70
+ 2. basic string - A string without any expression variables will be treated as a base URL and assumed that instance requests should append id to the end.
71
+
72
+ 3. AngularJS expression - An expression url is evaluated at run time based on the given context for non-instance methods or the instance itself. For example, given the url expression: /stores/{{storeId}}/items/{{id}}
73
+
74
+ Item.query({category: 'Software'}, {storeId: 123}) would generate a GET to /stores/1234/items?category=Software
75
+ Item.get({storeId: 123, id: 1}) would generate a GET to /stores/123/items/1
76
+
77
+ new Item({store: 123}).create() would generate a POST to /stores/123/items
78
+ new Item({id: 1, storeId: 123}).update() would generate a PUT to /stores/123/items/1
79
+
53
80
 
54
81
  ## Transformers / Interceptors
55
82
  The transformers and interceptors can be specified using an array containing transformer/interceptor functions or strings
@@ -93,7 +120,7 @@ Resources created using this factory have the following methods available and ea
93
120
  ### constructor
94
121
  ***
95
122
 
96
- The constructor function is to create new instances of the resource.
123
+ The constructor is the function returned by the railsResourceFactory and can be used with the "new" keyword.
97
124
 
98
125
  ####Parameters
99
126
  * **data** *(optional)* - An object containing the data to be stored in the instance.
@@ -104,7 +131,8 @@ The constructor function is to create new instances of the resource.
104
131
  A "class" method that executes a GET request against the base url with query parameters set via the params option.
105
132
 
106
133
  ####Parameters
107
- * **params** - An map of strings or objects that are passed to $http to be turned into query parameters
134
+ * **query params** - An map of strings or objects that are passed to $http to be turned into query parameters
135
+ * **context** - A context object that is used during url evaluation to resolve expression variables
108
136
 
109
137
 
110
138
  ### get
@@ -113,7 +141,7 @@ A "class" method that executes a GET request against the base url with query par
113
141
  A "class" method that executes a GET request against the resource url.
114
142
 
115
143
  ####Parameters
116
- * **id** - The id of the resource to retrieve
144
+ * **context** - A context object that is used during url evaluation to resolve expression variables. If you are using a basic url this can be an id number to append to the url.
117
145
 
118
146
 
119
147
  ### create
@@ -147,32 +175,26 @@ None
147
175
 
148
176
 
149
177
  ## Example
150
- Creating a Book resource would look something like:
178
+ For a complete working example (including the rails side), check out the [Employee Training Tracker](https://github.com/FineLinePrototyping/employee-training-tracker) application
179
+ we open sourced based on an interface we created for use internally that uses this module as well as many others.
180
+
181
+ ### Define Resource
182
+ In order to create a Book resource, we would first define the factory within a module.
151
183
 
152
184
  angular.module('book.services', ['rails']);
153
185
  angular.module('book.services').factory('Book', ['railsResourceFactory', function (railsResourceFactory) {
154
186
  return railsResourceFactory({url: '/books', name: 'book'});
155
187
  }]);
156
188
 
157
- #### Query Books
158
- var books = Book.query({title: 'The Hobbit'});
189
+ We would then inject that service into a controller:
159
190
 
160
- #### Get Book
161
- var book = Book.get(1234);
162
-
163
- #### Create Book
164
- var book = new Book({author: 'J. R. R. Tolkein', title: 'The Hobbit'});
165
- book.create().then(function (result) {
166
- // creation was successful
167
- });
191
+ angular.module('book.controllers').controller('BookShelfCtrl', ['$scope', 'Book', function ($scope, Book) {
192
+ // See following examples for using Book within your controller
193
+ }]);
168
194
 
169
- #### Update Book
170
- Book.get(1234).then(function (book) {
171
- book.author = 'J. R. R. Tolkein';
172
- book.update();
173
- });
195
+ The examples below illustrate how you would then use the Book service to get, create, update, and delete data.
174
196
 
175
- ## Extending
197
+ #### Extending
176
198
  You can add additional "class" or "instance" methods by modifying the resource returned from the factory call. For instance,
177
199
  if you wanted to add a "class" method named "findByTitle" to the Book resource you would modify the service setup as follows:
178
200
 
@@ -182,18 +204,81 @@ if you wanted to add a "class" method named "findByTitle" to the Book resource y
182
204
  resource.findByTitle = function (title) {
183
205
  return resource.query({title: title});
184
206
  };
207
+ return resource;
185
208
  }]);
186
209
 
187
- If you wanted to add an "instance" method (I couldn't come up with an example for this) you would add a new function to the resource prototype object:
210
+ If you wanted to add an "instance" method to retrieve a related object:
188
211
 
189
212
  angular.module('book.services', ['rails']);
190
- angular.module('book.services').factory('Book', ['railsResourceFactory', function (railsResourceFactory) {
213
+ angular.module('book.services').factory('Author', ['railsResourceFactory', function (railsResourceFactory) {
214
+ return railsResourceFactory({url: '/authors', name: 'author'});
215
+ }]);
216
+ angular.module('book.services').factory('Book', ['railsResourceFactory', 'Author', function (railsResourceFactory, Author) {
217
+ var resource = railsResourceFactory({url: '/books', name: 'book'});
218
+ resource.prototype.getAuthor = function () {
219
+ return Author.get(this.authorId);
220
+ };
221
+ }]);
222
+
223
+ Or say you instead had a nested "references" service call that returned a list of referenced books for a given book instance. In that case you can add your own addition method that calls $http.get and then
224
+ passes the resulting promise to the processResponse method which will perform the same transformations and handling that the get or query would use.
225
+
226
+ angular.module('book.services', ['rails']);
227
+ angular.module('book.services').factory('Book', ['railsResourceFactory', '$http', function (railsResourceFactory, $http) {
191
228
  var resource = railsResourceFactory({url: '/books', name: 'book'});
192
- resource.prototype.doSomething = function () {
193
- // do something here
229
+ resource.prototype.getReferences = function () {
230
+ var self = this;
231
+ return resource.processResponse($http.get(resource.resourceUrl(this.id) + '/references')).then(function (references) {
232
+ self.references = references;
233
+ return self.references;
234
+ });
194
235
  };
195
236
  }]);
196
237
 
238
+ ### Query Books
239
+ To query for a list of books with the title "The Hobbit" you would use the query method:
240
+
241
+ var books = Book.query({title: 'The Hobbit'});
242
+
243
+ We now have a promise in the books variable which we could then use within a template if we just wanted to do an ng-repeat over the books. A lot of times though you'll probably want to show some indicator
244
+ to your user that the search is executing so you'd want to use a then handler:
245
+
246
+ $scope.searching = true;
247
+ var books = Book.query({title: 'The Hobbit'});
248
+ books.then(function(results) {
249
+ $scope.searching = false;
250
+ }, function (error) {
251
+ $scope.searching = false;
252
+ // display error
253
+ });
254
+
255
+
256
+ ### Get Book
257
+ var book = Book.get(1234);
258
+
259
+ Again, it's important to remember that book is a promise, if instead you wanted the book data you would use the "then" function:
260
+
261
+ Book.get(1234).then(function (book) {
262
+ // book contains the data returned from the service
263
+ });
264
+
265
+ ### Create Book
266
+ var book = new Book({author: 'J. R. R. Tolkein', title: 'The Hobbit'});
267
+ book.create().then(function (result) {
268
+ // creation was successful
269
+ });
270
+
271
+ ### Update Book
272
+ Book.get(1234).then(function (book) {
273
+ book.author = 'J. R. R. Tolkein';
274
+ book.update();
275
+ });
276
+
277
+ Or, if you say the user typed in the book id into a scope variable and you wanted to update the book without having to first retrieve it:
278
+
279
+ var book = new Book({id: $scope.bookId, author: $scope.authorName, title: $scope.bookTitle});
280
+ book.update();
281
+
197
282
  ## Tests
198
283
  The tests are written using [Jasmine](http://pivotal.github.com/jasmine/) and are run using [Testacular](http://vojtajina.github.com/testacular/).
199
284
 
@@ -1,7 +1,7 @@
1
1
  module Angularjs
2
2
  module Rails
3
3
  module Resource
4
- VERSION = "0.0.2"
4
+ VERSION = "0.1.0"
5
5
  end
6
6
  end
7
7
  end
@@ -134,15 +134,11 @@ describe("js.rails", function () {
134
134
  });
135
135
  });
136
136
 
137
- describe('railsResourceFactory', function() {
138
- var $httpBackend, $rootScope, factory, Test, PluralTest,
137
+ describe('singular railsResourceFactory', function() {
138
+ var $httpBackend, $rootScope, factory, Test,
139
139
  config = {
140
140
  url: '/test',
141
141
  name: 'test'
142
- }, pluralConfig = {
143
- url: '/pluralTest',
144
- name: 'singular',
145
- pluralName: 'plural'
146
142
  };
147
143
 
148
144
  beforeEach(inject(function (_$httpBackend_, _$rootScope_, railsResourceFactory) {
@@ -150,7 +146,6 @@ describe("js.rails", function () {
150
146
  $rootScope = _$rootScope_;
151
147
  factory = railsResourceFactory;
152
148
  Test = railsResourceFactory(config);
153
- PluralTest = railsResourceFactory(pluralConfig);
154
149
  }));
155
150
 
156
151
  afterEach(function() {
@@ -222,34 +217,12 @@ describe("js.rails", function () {
222
217
  $httpBackend.flush();
223
218
  }));
224
219
 
225
- it('query should return array of resource objects when result is an array', inject(function($httpBackend) {
226
- var promise, result;
227
-
228
- $httpBackend.expectGET('/pluralTest').respond(200, {plural: [{abc: 'xyz'}, {xyz: 'abc'}]});
229
-
230
- expect(promise = PluralTest.query()).toBeDefined();
231
-
232
- promise.then(function (response) {
233
- result = response;
234
- });
235
-
236
- $httpBackend.flush();
237
-
238
- expect(angular.isArray(result)).toBe(true);
239
- angular.forEach(result, function (value) {
240
- expect(value).toBeInstanceOf(PluralTest);
241
- });
242
- expect(result[0]).toEqualData({abc: 'xyz'});
243
- expect(result[1]).toEqualData({xyz: 'abc'});
244
-
245
- }));
246
-
247
- it('query should return empty array when result is empty array', inject(function($httpBackend) {
220
+ it('get should return resource object when response is 200', inject(function($httpBackend) {
248
221
  var promise, result;
249
222
 
250
- $httpBackend.expectGET('/pluralTest').respond(200, {plural: []});
223
+ $httpBackend.expectGET('/test/123').respond(200, {test: {id: 123, abc: 'xyz'}});
251
224
 
252
- expect(promise = PluralTest.query()).toBeDefined();
225
+ expect(promise = Test.get(123)).toBeDefined();
253
226
 
254
227
  promise.then(function (response) {
255
228
  result = response;
@@ -257,17 +230,16 @@ describe("js.rails", function () {
257
230
 
258
231
  $httpBackend.flush();
259
232
 
260
- expect(angular.isArray(result)).toBe(true);
261
- expect(result.length).toBe(0);
233
+ expect(result).toBeInstanceOf(Test);
234
+ expect(result).toEqualData({id: 123, abc: 'xyz'});
262
235
  }));
263
236
 
264
-
265
- it('get should return resource object when response is 200', inject(function($httpBackend) {
237
+ it('get should work with id as string as well', inject(function($httpBackend) {
266
238
  var promise, result;
267
239
 
268
240
  $httpBackend.expectGET('/test/123').respond(200, {test: {id: 123, abc: 'xyz'}});
269
241
 
270
- expect(promise = Test.get(123)).toBeDefined();
242
+ expect(promise = Test.get('123')).toBeDefined();
271
243
 
272
244
  promise.then(function (response) {
273
245
  result = response;
@@ -340,7 +312,7 @@ describe("js.rails", function () {
340
312
  angular.copy(config, testConfig);
341
313
  testConfig.requestTransformers = [];
342
314
  testConfig.responseInterceptors = ['railsFieldRenamingInterceptor'];
343
- resource = factory(config);
315
+ resource = factory(testConfig);
344
316
 
345
317
  expect(promise = resource.get(123)).toBeDefined();
346
318
 
@@ -483,4 +455,372 @@ describe("js.rails", function () {
483
455
  }));
484
456
  });
485
457
 
458
+ describe('plural railsResourceFactory', function() {
459
+ var $httpBackend, $rootScope, factory, PluralTest,
460
+ pluralConfig = {
461
+ url: '/pluralTest',
462
+ name: 'singular',
463
+ pluralName: 'plural'
464
+ };
465
+
466
+ beforeEach(inject(function (_$httpBackend_, _$rootScope_, railsResourceFactory) {
467
+ $httpBackend = _$httpBackend_;
468
+ $rootScope = _$rootScope_;
469
+ factory = railsResourceFactory;
470
+ PluralTest = railsResourceFactory(pluralConfig);
471
+ }));
472
+
473
+ afterEach(function() {
474
+ $httpBackend.verifyNoOutstandingExpectation();
475
+ $httpBackend.verifyNoOutstandingRequest();
476
+ });
477
+
478
+ it('query should return array of resource objects when result is an array', inject(function($httpBackend) {
479
+ var promise, result;
480
+
481
+ $httpBackend.expectGET('/pluralTest').respond(200, {plural: [{abc: 'xyz'}, {xyz: 'abc'}]});
482
+
483
+ expect(promise = PluralTest.query()).toBeDefined();
484
+
485
+ promise.then(function (response) {
486
+ result = response;
487
+ });
488
+
489
+ $httpBackend.flush();
490
+
491
+ expect(angular.isArray(result)).toBe(true);
492
+ angular.forEach(result, function (value) {
493
+ expect(value).toBeInstanceOf(PluralTest);
494
+ });
495
+ expect(result[0]).toEqualData({abc: 'xyz'});
496
+ expect(result[1]).toEqualData({xyz: 'abc'});
497
+
498
+ }));
499
+
500
+ it('query should return empty array when result is empty array', inject(function($httpBackend) {
501
+ var promise, result;
502
+
503
+ $httpBackend.expectGET('/pluralTest').respond(200, {plural: []});
504
+
505
+ expect(promise = PluralTest.query()).toBeDefined();
506
+
507
+ promise.then(function (response) {
508
+ result = response;
509
+ });
510
+
511
+ $httpBackend.flush();
512
+
513
+ expect(angular.isArray(result)).toBe(true);
514
+ expect(result.length).toBe(0);
515
+ }));
516
+ });
517
+
518
+ describe('nested railsResourceFactory', function() {
519
+ var $httpBackend, $rootScope, factory, NestedTest,
520
+ nestedConfig = {
521
+ url: '/nested/{{nestedId}}/test/{{id}}',
522
+ name: 'nestedTest'
523
+ };
524
+
525
+ beforeEach(inject(function (_$httpBackend_, _$rootScope_, railsResourceFactory) {
526
+ $httpBackend = _$httpBackend_;
527
+ $rootScope = _$rootScope_;
528
+ factory = railsResourceFactory;
529
+ NestedTest = railsResourceFactory(nestedConfig);
530
+ }));
531
+
532
+ afterEach(function() {
533
+ $httpBackend.verifyNoOutstandingExpectation();
534
+ $httpBackend.verifyNoOutstandingRequest();
535
+ });
536
+
537
+ it('query should return resource object when response is single object', inject(function($httpBackend) {
538
+ var promise, result;
539
+
540
+ $httpBackend.expectGET('/nested/1234/test').respond(200, {nested_test: {abc: 'xyz'}});
541
+
542
+ expect(promise = NestedTest.query(null, {nestedId: 1234})).toBeDefined();
543
+
544
+ promise.then(function (response) {
545
+ result = response;
546
+ });
547
+
548
+ $httpBackend.flush();
549
+
550
+ expect(result).toBeInstanceOf(NestedTest);
551
+ expect(result).toEqualData({abc: 'xyz'});
552
+ }));
553
+
554
+ it('query should return no data on 204', inject(function($httpBackend) {
555
+ var promise, result;
556
+
557
+ $httpBackend.expectGET('/nested/1234/test').respond(204);
558
+ expect(promise = NestedTest.query(null, {nestedId: 1234})).toBeDefined();
559
+
560
+ promise.then(function (response) {
561
+ result = response;
562
+ });
563
+
564
+ $httpBackend.flush();
565
+
566
+ expect(result).toBeUndefined();
567
+ }));
568
+
569
+ it('query should add parameter abc=1', inject(function($httpBackend) {
570
+ var promise;
571
+
572
+ $httpBackend.expectGET('/nested/1234/test?abc=1').respond(200, {nested_test: {abc: 'xyz'}});
573
+
574
+ expect(promise = NestedTest.query({abc: '1'}, {nestedId: 1234})).toBeDefined();
575
+ $httpBackend.flush();
576
+ }));
577
+
578
+ it('query should add parameters abc=1 & xyz=2', inject(function($httpBackend) {
579
+ var promise;
580
+
581
+ $httpBackend.expectGET('/nested/1234/test?abc=1&xyz=2').respond(200, {nested_test: {abc: 'xyz'}});
582
+
583
+ expect(promise = NestedTest.query({abc: '1', xyz: 2}, {nestedId: 1234})).toBeDefined();
584
+ $httpBackend.flush();
585
+ }));
586
+
587
+ it('query with default params should add parameter abc=1', inject(function($httpBackend) {
588
+ var promise, resource, defaultParamsConfig = {};
589
+
590
+ $httpBackend.expectGET('/nested/1234/test?abc=1').respond(200, {nested_test: {abc: 'xyz'}});
591
+
592
+ angular.copy(nestedConfig, defaultParamsConfig);
593
+ defaultParamsConfig.defaultParams = {abc: '1'};
594
+
595
+ resource = factory(defaultParamsConfig);
596
+ expect(promise = resource.query(null, {nestedId: 1234})).toBeDefined();
597
+
598
+ $httpBackend.flush();
599
+ }));
600
+
601
+ it('get should return resource object when response is 200', inject(function($httpBackend) {
602
+ var promise, result;
603
+
604
+ $httpBackend.expectGET('/nested/1234/test/123').respond(200, {nested_test: {id: 123, abc: 'xyz'}});
605
+
606
+ expect(promise = NestedTest.get({nestedId: 1234, id: 123})).toBeDefined();
607
+
608
+ promise.then(function (response) {
609
+ result = response;
610
+ });
611
+
612
+ $httpBackend.flush();
613
+
614
+ expect(result).toBeInstanceOf(NestedTest);
615
+ expect(result).toEqualData({id: 123, abc: 'xyz'});
616
+ }));
617
+
618
+ it('get should call failure callback when 404', inject(function($httpBackend) {
619
+ var promise, success = false, failure = false;
620
+
621
+ $httpBackend.expectGET('/nested/1234/test/123').respond(404);
622
+
623
+ expect(promise = NestedTest.get({nestedId: 1234, id: 123})).toBeDefined();
624
+
625
+ promise.then(function () {
626
+ success = true;
627
+ }, function () {
628
+ failure = true;
629
+ });
630
+
631
+ $httpBackend.flush();
632
+
633
+ expect(success).toBe(false);
634
+ expect(failure).toBe(true);
635
+ }));
636
+
637
+ it('get with default params should add parameter abc=1', inject(function($httpBackend) {
638
+ var promise, resource, defaultParamsConfig = {};
639
+
640
+ $httpBackend.expectGET('/nested/1234/test/123?abc=1').respond(200, {nested_test: {abc: 'xyz'}});
641
+
642
+ angular.copy(nestedConfig, defaultParamsConfig);
643
+ defaultParamsConfig.defaultParams = {abc: '1'};
644
+
645
+ resource = factory(defaultParamsConfig);
646
+ expect(promise = resource.get({nestedId: 1234, id: 123})).toBeDefined();
647
+
648
+ $httpBackend.flush();
649
+ }));
650
+
651
+ it('should be able to turn off root mapping and field renaming', inject(function($httpBackend) {
652
+ var promise, result, resource;
653
+
654
+ $httpBackend.expectGET('/nested/1234/test/123').respond(200, {id: 123, nested_id: 1234, abc_def: 'xyz'});
655
+
656
+ resource = factory(nestedConfig);
657
+ resource.responseInterceptors = [];
658
+ resource.requestTransformers = [];
659
+ expect(promise = resource.get({nestedId: 1234, id: 123})).toBeDefined();
660
+
661
+ promise.then(function (response) {
662
+ result = response;
663
+ });
664
+
665
+ $httpBackend.flush();
666
+
667
+ expect(result).toBeInstanceOf(resource);
668
+ expect(result).toEqualData({id: 123, nested_id: 1234, abc_def: 'xyz'});
669
+ }));
670
+
671
+ it('should be able to turn off root mapping but keep field renaming', inject(function($httpBackend) {
672
+ var promise, result, resource, testConfig = {};
673
+
674
+ $httpBackend.expectGET('/nested/1234/test/123').respond(200, {id: 123, nested_id: 1234, abc_def: 'xyz'});
675
+
676
+ angular.copy(nestedConfig, testConfig);
677
+ testConfig.requestTransformers = [];
678
+ testConfig.responseInterceptors = ['railsFieldRenamingInterceptor'];
679
+ resource = factory(testConfig);
680
+
681
+ expect(promise = resource.get({nestedId: 1234, id: 123})).toBeDefined();
682
+
683
+ promise.then(function (response) {
684
+ result = response;
685
+ });
686
+
687
+ $httpBackend.flush();
688
+
689
+ expect(result).toBeInstanceOf(resource);
690
+ expect(result).toEqualData({id: 123, nestedId: 1234, abcDef: 'xyz'});
691
+ }));
692
+
693
+ it('should be able to create new instance and save it', inject(function($httpBackend) {
694
+ var data = new NestedTest({nestedId: 1234, abcDef: 'xyz'});
695
+
696
+ $httpBackend.expectPOST('/nested/1234/test').respond(200, {nested_test: {id: 123, nested_id: 1234, abc_def: 'xyz'}});
697
+ data.nestedId = 1234;
698
+ data.create();
699
+ $httpBackend.flush();
700
+
701
+ expect(data).toEqualData({id: 123, nestedId: 1234, abcDef: 'xyz'});
702
+ }));
703
+
704
+ it('should be able to create new instance and update it', inject(function($httpBackend) {
705
+ var data = new NestedTest({abcDef: 'xyz'});
706
+
707
+ $httpBackend.expectPOST('/nested/1234/test').respond(200, {nested_test: {id: 123, nested_id: 1234, abc_def: 'xyz'}});
708
+ data.nestedId = 1234;
709
+ data.create();
710
+ $httpBackend.flush(1);
711
+
712
+ expect(data).toEqualData({id: 123, nestedId: 1234, abcDef: 'xyz'});
713
+
714
+ $httpBackend.expectPUT('/nested/1234/test/123').respond(200, {nested_test: {id: 123, nested_id: 1234, abc_def: 'xyz', xyz: 'abc', extra: 'test'}});
715
+ data.xyz = 'abc';
716
+ data.update();
717
+ $httpBackend.flush();
718
+
719
+ expect(data).toEqualData({id: 123, nestedId: 1234, abcDef: 'xyz', xyz: 'abc', extra: 'test'});
720
+ }));
721
+
722
+ it('create with default params should add parameter abc=1', inject(function($httpBackend) {
723
+ var promise, Resource, data, defaultParamsConfig = {};
724
+
725
+ $httpBackend.expectPOST('/nested/1234/test?abc=1').respond(200, {nested_test: {abc: 'xyz'}});
726
+
727
+ angular.copy(nestedConfig, defaultParamsConfig);
728
+ defaultParamsConfig.defaultParams = {abc: '1'};
729
+
730
+ Resource = factory(defaultParamsConfig);
731
+ data = new Resource();
732
+ data.nestedId = 1234;
733
+ data.create();
734
+
735
+ $httpBackend.flush();
736
+ }));
737
+
738
+ it('should be able to get resource and update it', inject(function($httpBackend) {
739
+ var promise, result;
740
+
741
+ $httpBackend.expectGET('/nested/1234/test/123').respond(200, {nested_test: {id: 123, nested_id: 1234, abc: 'xyz'}});
742
+
743
+ expect(promise = NestedTest.get({nestedId: 1234, id: 123})).toBeDefined();
744
+
745
+ promise.then(function (response) {
746
+ result = response;
747
+ });
748
+
749
+ $httpBackend.flush();
750
+
751
+ expect(result).toBeInstanceOf(NestedTest);
752
+ expect(result).toEqualData({id: 123, nestedId: 1234, abc: 'xyz'});
753
+
754
+ $httpBackend.expectPUT('/nested/1234/test/123').respond(200, {nested_test: {id: 123, nested_id: 1234, abc_def: 'xyz', xyz: 'abc', extra: 'test'}});
755
+ result.xyz = 'abc';
756
+ result.update();
757
+ $httpBackend.flush();
758
+
759
+ // abc was originally set on the object so it should still be there after the update
760
+ expect(result).toEqualData({id: 123, nestedId: 1234, abc: 'xyz', abcDef: 'xyz', xyz: 'abc', extra: 'test'});
761
+ }));
762
+
763
+ it('update should handle 204 response', inject(function($httpBackend) {
764
+ var promise, result;
765
+
766
+ $httpBackend.expectGET('/nested/1234/test/123').respond(200, {nested_test: {id: 123, nested_id: 1234, abc: 'xyz'}});
767
+
768
+ expect(promise = NestedTest.get({nestedId: 1234, id: 123})).toBeDefined();
769
+
770
+ promise.then(function (response) {
771
+ result = response;
772
+ });
773
+
774
+ $httpBackend.flush();
775
+
776
+ expect(result).toBeInstanceOf(NestedTest);
777
+ expect(result).toEqualData({id: 123, nestedId: 1234, abc: 'xyz'});
778
+
779
+ $httpBackend.expectPUT('/nested/1234/test/123').respond(204);
780
+ result.xyz = 'abc';
781
+ result.update();
782
+ $httpBackend.flush();
783
+
784
+ expect(result).toEqualData({id: 123, nestedId: 1234, abc: 'xyz', xyz: 'abc'});
785
+ }));
786
+
787
+ it('should be able to delete instance returned from get', inject(function($httpBackend) {
788
+ var promise, result;
789
+
790
+ $httpBackend.expectGET('/nested/1234/test/123').respond(200, {nested_test: {id: 123, nestedId: 1234, abc: 'xyz'}});
791
+
792
+ expect(promise = NestedTest.get({nestedId: 1234, id: 123})).toBeDefined();
793
+
794
+ promise.then(function (response) {
795
+ result = response;
796
+ });
797
+
798
+ $httpBackend.flush();
799
+
800
+ expect(result).toBeInstanceOf(NestedTest);
801
+ expect(result).toEqualData({id: 123, nestedId: 1234, abc: 'xyz'});
802
+
803
+ $httpBackend.expectDELETE('/nested/1234/test/123').respond(204);
804
+ result.remove();
805
+ $httpBackend.flush();
806
+ }));
807
+
808
+ it('delete with default params should add parameter abc=1', inject(function($httpBackend) {
809
+ var Resource, data, defaultParamsConfig = {};
810
+
811
+ $httpBackend.expectDELETE('/nested/1234/test/123?abc=1').respond(204);
812
+
813
+ angular.copy(nestedConfig, defaultParamsConfig);
814
+ defaultParamsConfig.defaultParams = {abc: '1'};
815
+
816
+ Resource = factory(defaultParamsConfig);
817
+ data = new Resource();
818
+ data.id = 123;
819
+ data.nestedId = 1234;
820
+ data.remove();
821
+
822
+ $httpBackend.flush();
823
+ }));
824
+ });
825
+
486
826
  });
@@ -84,7 +84,30 @@
84
84
  };
85
85
  });
86
86
 
87
- angular.module('rails').factory('railsResourceFactory', ['$http', '$q', '$injector', function ($http, $q, $injector) {
87
+ angular.module('rails').factory('railsResourceFactory', ['$http', '$q', '$injector', '$interpolate', function ($http, $q, $injector, $interpolate) {
88
+ function urlBuilder(url) {
89
+ var expression;
90
+
91
+ if (angular.isFunction(url)) {
92
+ return url;
93
+ }
94
+
95
+ if (url.indexOf('{{') === -1) {
96
+ url = url + '/{{id}}';
97
+ }
98
+
99
+ expression = $interpolate(url);
100
+
101
+ return function (params) {
102
+ url = expression(params);
103
+
104
+ if (url.charAt(url.length - 1) === '/') {
105
+ url = url.substr(0, url.length - 1);
106
+ }
107
+
108
+ return url;
109
+ };
110
+ }
88
111
 
89
112
  function railsResourceFactory(config) {
90
113
  var transformers = config.requestTransformers || ['railsRootWrappingTransformer', 'railsFieldRenamingTransformer'],
@@ -94,7 +117,7 @@
94
117
  angular.extend(this, value || {});
95
118
  }
96
119
 
97
- RailsResource.url = config.url;
120
+ RailsResource.url = urlBuilder(config.url);
98
121
  RailsResource.rootName = config.name;
99
122
  RailsResource.rootPluralName = config.pluralName || config.name + 's';
100
123
  RailsResource.httpConfig = config.httpConfig || {};
@@ -159,30 +182,58 @@
159
182
  });
160
183
  };
161
184
 
162
- RailsResource.getHttpConfig = function (queryParams) {
163
- var config = angular.copy(RailsResource.httpConfig, {});
185
+ RailsResource.getParameters = function (queryParams) {
186
+ var params;
164
187
 
165
188
  if (RailsResource.defaultParams) {
166
- config.params = RailsResource.defaultParams;
189
+ params = RailsResource.defaultParams;
167
190
  }
168
191
 
169
- if (queryParams) {
170
- config.params = angular.extend(config.params || {}, queryParams);
192
+ if (angular.isObject(queryParams)) {
193
+ params = angular.extend(params || {}, queryParams);
194
+ }
195
+
196
+ return params;
197
+ };
198
+
199
+ RailsResource.getHttpConfig = function (queryParams) {
200
+ var params = RailsResource.getParameters(queryParams);
201
+
202
+ if (params) {
203
+ return angular.extend({params: params}, RailsResource.httpConfig);
171
204
  }
172
205
 
173
- return config;
206
+ return RailsResource.httpConfig;
174
207
  };
175
208
 
176
- RailsResource.resourceUrl = function (id) {
177
- return RailsResource.url + '/' + id;
209
+ /**
210
+ * Returns a URL from the given parameters. You can override this method on your resource definitions to provide
211
+ * custom logic for building your URLs or you can utilize the parameterized url strings to substitute values in the
212
+ * URL string.
213
+ *
214
+ * The parameters in the URL string follow the normal Angular binding expression using {{ and }} for the start/end symbols.
215
+ *
216
+ * If the context is a number and the URL string does not contain an id parameter then the number is appended
217
+ * to the URL string.
218
+ *
219
+ * If the context is a number and the URL string does
220
+ * @param context
221
+ * @return {string}
222
+ */
223
+ RailsResource.resourceUrl = function (context) {
224
+ if (!angular.isObject(context)) {
225
+ context = {id: context};
226
+ }
227
+
228
+ return RailsResource.url(context || {});
178
229
  };
179
230
 
180
- RailsResource.query = function (queryParams) {
181
- return RailsResource.processResponse($http.get(RailsResource.url, RailsResource.getHttpConfig(queryParams)));
231
+ RailsResource.query = function (queryParams, context) {
232
+ return RailsResource.processResponse($http.get(RailsResource.resourceUrl(context), RailsResource.getHttpConfig(queryParams)));
182
233
  };
183
234
 
184
- RailsResource.get = function (id) {
185
- return RailsResource.processResponse($http.get(RailsResource.resourceUrl(id), RailsResource.getHttpConfig()));
235
+ RailsResource.get = function (context, queryParams) {
236
+ return RailsResource.processResponse($http.get(RailsResource.resourceUrl(context), RailsResource.getHttpConfig(queryParams)));
186
237
  };
187
238
 
188
239
  RailsResource.prototype.processResponse = function (promise) {
@@ -207,17 +258,17 @@
207
258
  RailsResource.prototype.create = function () {
208
259
  // clone so we can manipulate w/o modifying our instance
209
260
  var data = RailsResource.transformData(angular.copy(this, {}));
210
- return this.processResponse($http.post(RailsResource.url, data, RailsResource.getHttpConfig()));
261
+ return this.processResponse($http.post(RailsResource.resourceUrl(this), data, RailsResource.getHttpConfig()));
211
262
  };
212
263
 
213
264
  RailsResource.prototype.update = function () {
214
265
  // clone so we can manipulate w/o modifying our instance
215
266
  var data = RailsResource.transformData(angular.copy(this, {}));
216
- return this.processResponse($http.put(RailsResource.resourceUrl(this.id), data, RailsResource.getHttpConfig()));
267
+ return this.processResponse($http.put(RailsResource.resourceUrl(this), data, RailsResource.getHttpConfig()));
217
268
  };
218
269
 
219
- RailsResource.prototype.remove = RailsResource.prototype.delete = function (id) {
220
- return this.processResponse($http.delete(RailsResource.resourceUrl(this.id), RailsResource.getHttpConfig()));
270
+ RailsResource.prototype.remove = RailsResource.prototype.delete = function () {
271
+ return this.processResponse($http.delete(RailsResource.resourceUrl(this), RailsResource.getHttpConfig()));
221
272
  };
222
273
 
223
274
  return RailsResource;
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: angularjs-rails-resource
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-01 00:00:00.000000000 Z
12
+ date: 2013-02-11 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: A small AngularJS add-on for integrating with Rails via JSON more easily.
15
15
  email: